# test/test_dput_main.py
# Part of ‘dput’, a Debian package upload toolkit.
#
# This is free software, and you are welcome to redistribute it under
# certain conditions; see the end of this file for copyright
# information, grant of license, and disclaimer of warranty.

""" Unit tests for ‘dput’ module `main` function. """

import contextlib
import doctest
import itertools
import os
import sys
import tempfile
import textwrap
import types
import unittest.mock

import pkg_resources
import testscenarios
import testtools
import testtools.matchers

import dput.dput
from dput.helper import dputhelper

from .helper import (
        EXIT_STATUS_FAILURE,
        EXIT_STATUS_SUCCESS,
        FakeSystemExit,
        make_options_namespace,
        patch_os_environ,
        patch_os_getuid,
        patch_os_isatty,
        patch_pwd_getpwuid,
        patch_sys_argv,
        patch_system_interfaces,
        make_unique_slug,
        )
from .test_changesfile import (
        make_changes_file_path,
        set_changes_file_scenario,
        setup_changes_file_fixtures,
        )
from .test_configfile import (
        patch_active_config_files,
        patch_get_delayed_days,
        patch_get_fqdn_for_host,
        patch_get_incoming_for_host,
        patch_get_login_for_host,
        patch_get_port_for_host,
        patch_get_progress_indicator_for_host,
        patch_get_upload_method_name_for_host,
        patch_print_default_upload_method,
        patch_print_host_list,
        patch_runtime_config_options,
        patch_verify_config_upload_methods,
        set_config,
        )
from .test_dputhelper import (
        patch_get_username_from_system,
        patch_getopt,
        patch_pkg_resources_get_distribution,
        )


def patch_parse_changes(testcase):
    """ Patch the `parse_changes` function for the test case. """
    func_patcher = unittest.mock.patch.object(
            dput.dput, "parse_changes", autospec=True)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


def patch_read_configs(testcase):
    """ Patch the `read_configs` function for the test case. """
    def fake_read_configs(*args, **kwargs):
        return testcase.runtime_config_parser

    func_patcher = unittest.mock.patch.object(
            dput.dput, "read_configs", autospec=True,
            side_effect=fake_read_configs)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


def patch_print_config(testcase):
    """ Patch the `print_config` function for the test case. """
    func_patcher = unittest.mock.patch.object(
            dput.dput, "print_config", autospec=True)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


def set_upload_methods(testcase):
    """ Set the `upload_methods` value for the test case. """
    if not hasattr(testcase, 'upload_methods'):
        upload_methods = dput.dput.import_upload_functions()
        testcase.upload_methods = {
                name: unittest.mock.MagicMock(method)
                for (name, method) in upload_methods.items()}


def patch_import_upload_functions(testcase):
    """ Patch the `import_upload_functions` function for the test case. """
    func_patcher = unittest.mock.patch.object(
            dput.dput, "import_upload_functions", autospec=True,
            return_value=testcase.upload_methods)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


class make_usage_message_TestCase(testtools.TestCase):
    """ Test cases for `make_usage_message` function. """

    def test_returns_text_with_program_name(self):
        """ Should return text with expected program name. """
        result = dput.dput.make_usage_message()
        expected_result = textwrap.dedent("""\
                Usage: dput ...
                ...
                """)
        self.expectThat(
                result,
                testtools.matchers.DocTestMatches(
                    expected_result, flags=doctest.ELLIPSIS))


class make_delayed_queue_path_TestCase(
        testscenarios.WithScenarios,
        testtools.TestCase):
    """ Test cases for `make_delayed_queue_path` function. """

    scenarios = [
            ('days-unspecified', {
                'test_days': None,
                'expected_result': "",
                }),
            ('days-zero', {
                'test_days': 0,
                'expected_result': "DELAYED/0-day",
                }),
            ('days-five', {
                'test_days': 5,
                'expected_result': "DELAYED/5-day",
                }),
            ]

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        self.set_test_args()

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(
                days=self.test_days,
                )

    def test_returns_expected_result(self):
        """ Should return expected path value. """
        result = dput.dput.make_delayed_queue_path(**self.test_args)
        self.assertEqual(self.expected_result, result)


def patch_make_delayed_queue_path(testcase):
    """ Patch the `make_delayed_queue_path` function for `testcase`. """
    if not hasattr(testcase, 'delayed_queue_path'):
        testcase.delayed_queue_path = os.path.basename(tempfile.mktemp())
    func_patcher = unittest.mock.patch.object(
            dput.dput, 'make_delayed_queue_path', autospec=True,
            return_value=testcase.delayed_queue_path)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


class upload_files_via_BaseTestCase(testtools.TestCase):
    """ Base for test cases for `upload_files_via_*` functions. """

    placeholder_args = {
            'fqdn': object(),
            'login': object(),
            'incoming': object(),
            'files_to_upload': object(),
            'debug': object(),
            }

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()
        patch_system_interfaces(self)

        set_config(self, 'exist-simple')
        patch_runtime_config_options(self)

        set_upload_methods(self)
        self.test_method = self.runtime_config_parser.get(
               self.test_host, 'method')
        self.test_method_func = self.upload_methods[self.test_method]

        if not hasattr(self, 'omit_kwargs'):
            self.omit_kwargs = []
        self.set_test_args()
        self.set_expected_method_and_args()

        patch_get_port_for_host(self)

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        raise NotImplementedError

    def set_expected_method_and_args(self):
        """ Set the expected upload method and its arguments. """
        self.expected_method_func = self.test_method_func
        self.expected_args_list = [
                self.placeholder_args['fqdn'],
                self.placeholder_args['login'],
                self.placeholder_args['incoming'],
                self.placeholder_args['files_to_upload'],
                self.placeholder_args['debug'],
                ]
        self.expected_args = tuple(self.expected_args_list)


def patch_upload_files_via(testcase):
    """ Patch the `upload_files_via_*` functions for `testcase`. """
    for func_name in [
            'upload_files_via_{}'.format(suffix)
            for suffix in [
                'simulate',
                'method', 'method_local', 'method_ftp', 'method_scp']
            ]:
        func_patcher = unittest.mock.patch.object(
                dput.dput, func_name, autospec=True)
        func_patcher.start()
        testcase.addCleanup(func_patcher.stop)


class upload_files_via_simulate_TestCase(
        testscenarios.WithScenarios,
        upload_files_via_BaseTestCase):
    """ Test cases for `upload_files_via_simulate` function. """

    function_to_test = staticmethod(dput.dput.upload_files_via_simulate)

    scenarios = [
            ('files-zero', {
                'files_to_upload': [],
                }),
            ('files-one', {
                'files_to_upload': [tempfile.mktemp()],
                }),
            ('files-three', {
                'files_to_upload': [tempfile.mktemp() for __ in range(3)],
                }),
            ]

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(self.placeholder_args)
        self.test_args.update({
                'method': make_unique_slug(self),
                'files_to_upload': self.files_to_upload,
                'config': self.runtime_config_parser,
                'host': self.test_host,
                })

    def set_expected_method_and_args(self):
        """ Set the expected upload method and its arguments. """
        # Upload method is called multiple times with different arguments.
        pass

    def test_emits_expected_information_message(self):
        """ Should emit expected information message. """
        self.function_to_test(**self.test_args)
        expected_stderr_output = (
                "Not performing upload: ‘simulate’ option specified.")
        self.assertIn(expected_stderr_output, sys.stderr.getvalue())

    def test_emits_diagnostic_message_for_each_file_to_upload(self):
        """ Should emit diagnostic message for each file to upload. """
        self.function_to_test(**self.test_args)
        for path in self.files_to_upload:
            expected_output = textwrap.dedent("""\
                    Uploading with {method}: {path} to {fqdn}:{incoming}
                    """).format(
                        method=self.test_args['method'],
                        path=path,
                        fqdn=self.test_args['fqdn'],
                        incoming=self.test_args['incoming'])
            self.expectThat(
                    sys.stderr.getvalue(),
                    testtools.matchers.Contains(expected_output))


def patch_upload_files_via_simulate(testcase):
    """ Patch the `upload_files_via_simulate` function for the `testcase`. """
    func_patcher = unittest.mock.patch.object(
            dput.dput, "upload_files_via_simulate", autospec=True)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


class upload_files_via_method_TestCase(
        testscenarios.WithScenarios,
        upload_files_via_BaseTestCase):
    """ Test cases for `upload_files_via_method` function. """

    placeholder_args = dict(upload_files_via_BaseTestCase.placeholder_args)
    placeholder_args.update({
            'files_to_upload': object(),
            'debug': object(),
            'dummy': object(),
            'progress': object(),
            })

    scenarios = [
            ('method-http', {
                'config_method': "http",
                'expected_kwargs': {
                    'dummy': placeholder_args['dummy'],
                    'progress': placeholder_args['progress'],
                    },
                }),
            ('method-http default-dummy', {
                'config_method': "http",
                'omit_kwargs': ['dummy'],
                'expected_kwargs': {
                    'dummy': 0,
                    'progress': placeholder_args['progress'],
                    },
                }),
            ('method-http default-progress', {
                'config_method': "http",
                'omit_kwargs': ['progress'],
                'expected_kwargs': {
                    'dummy': placeholder_args['dummy'],
                    'progress': 0,
                    },
                }),
            ('method-rsync', {
                'config_method': "rsync",
                'expected_kwargs': {
                    'dummy': placeholder_args['dummy'],
                    'progress': placeholder_args['progress'],
                    },
                }),
            ]

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(
                (name, self.placeholder_args[name])
                for name in [
                    'fqdn', 'login', 'incoming', 'files_to_upload', 'debug',
                    'dummy', 'progress']
                if name not in self.omit_kwargs)
        self.test_args.update({
                'method_name': self.test_method,
                'method': self.test_method_func,
                'config': self.runtime_config_parser,
                'host': self.test_host,
                })
        if hasattr(self, 'test_args_extra'):
            self.test_args.update(self.test_args_extra)

    def test_calls_upload_method_with_expected_args(self):
        """ Should call upload method function with expected args. """
        dput.dput.upload_files_via_method(**self.test_args)
        self.expected_method_func.assert_called_with(
                *self.expected_args, **self.expected_kwargs)


class upload_files_via_method_local_TestCase(
        testscenarios.WithScenarios,
        upload_files_via_BaseTestCase):
    """ Test cases for `upload_files_via_method_local` function. """

    placeholder_args = dict(upload_files_via_BaseTestCase.placeholder_args)
    placeholder_args.update({
            'files_to_upload': object(),
            'debug': object(),
            'compress': object(),
            'progress': object(),
            })

    scenarios = [
            ('method-local', {
                'config_method': "local",
                'expected_kwargs': {
                    'compress': placeholder_args['compress'],
                    'progress': placeholder_args['progress'],
                    },
                }),
            ('method-local default-compress', {
                'config_method': "local",
                'omit_kwargs': ['compress'],
                'expected_kwargs': {
                    'compress': False,
                    'progress': placeholder_args['progress'],
                    },
                }),
            ('method-local default-progress', {
                'config_method': "local",
                'omit_kwargs': ['progress'],
                'expected_kwargs': {
                    'compress': placeholder_args['compress'],
                    'progress': 0,
                    },
                }),
            ]

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(
                (name, self.placeholder_args[name])
                for name in [
                    'fqdn', 'login', 'incoming', 'files_to_upload', 'debug',
                    'compress', 'progress']
                if name not in self.omit_kwargs)
        self.test_args.update({
                'method_name': self.test_method,
                'method': self.test_method_func,
                'config': self.runtime_config_parser,
                'host': self.test_host,
                })
        if hasattr(self, 'test_args_extra'):
            self.test_args.update(self.test_args_extra)

    def test_calls_upload_method_with_expected_args(self):
        """ Should call upload method function with expected args. """
        dput.dput.upload_files_via_method_local(**self.test_args)
        self.expected_method_func.assert_called_with(
                *self.expected_args, **self.expected_kwargs)


class upload_files_via_method_ftp_TestCase(
        testscenarios.WithScenarios,
        upload_files_via_BaseTestCase):
    """ Test cases for `upload_files_via_method_ftp` function. """

    placeholder_args = dict(upload_files_via_BaseTestCase.placeholder_args)
    placeholder_args.update({
            'files_to_upload': object(),
            'debug': False,
            'commandline_passive': False,
            'progress': object(),
            })

    scenarios = [
            ('method-ftp', {
                'config_method': "ftp",
                'get_port_for_host_return_value': None,
                'expected_kwargs': {
                    'ftp_mode': False,
                    'progress': placeholder_args['progress'],
                    'port': 21,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 21",
                    "D: Using active ftp",
                    ]),
                }),
            ('method-ftp default-passive', {
                'config_method': "ftp",
                'omit_kwargs': ['commandline_passive'],
                'get_port_for_host_return_value': None,
                'expected_kwargs': {
                    'ftp_mode': False,
                    'progress': placeholder_args['progress'],
                    'port': 21,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 21",
                    "D: Using active ftp",
                    ]),
                }),
            ('method-ftp default-progress', {
                'config_method': "ftp",
                'omit_kwargs': ['progress'],
                'get_port_for_host_return_value': None,
                'expected_kwargs': {
                    'ftp_mode': False,
                    'progress': 0,
                    'port': 21,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 21",
                    "D: Using active ftp",
                    ]),
                }),
            ('method-ftp config-port-custom', {
                'config_method': "ftp",
                'get_port_for_host_return_value': 555,
                'expected_kwargs': {
                    'ftp_mode': False,
                    'progress': placeholder_args['progress'],
                    'port': 555,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 555",
                    "D: Using active ftp",
                    ]),
                }),
            ('method-ftp commandline-passive', {
                'config_method': "ftp",
                'get_port_for_host_return_value': None,
                'test_args_extra': {
                    'commandline_passive': True,
                    },
                'expected_kwargs': {
                    'ftp_mode': True,
                    'progress': placeholder_args['progress'],
                    'port': 21,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 21",
                    "D: Using passive ftp",
                    ]),
                }),
            ('method-ftp config-passive', {
                'config_method': "ftp",
                'config_extras': {
                    'host': {
                        'passive_ftp': "True",
                        },
                    },
                'omit_kwargs': ['commandline_passive'],
                'get_port_for_host_return_value': None,
                'expected_kwargs': {
                    'ftp_mode': True,
                    'progress': placeholder_args['progress'],
                    'port': 21,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 21",
                    "D: Using passive ftp",
                    ]),
                }),
            ('method-ftp config-passive-false commandline-passive', {
                'config_method': "ftp",
                'config_extras': {
                    'host': {
                        'passive_ftp': "False",
                        },
                    },
                'test_args_extra': {
                    'commandline_passive': True,
                    },
                'get_port_for_host_return_value': None,
                'expected_kwargs': {
                    'ftp_mode': True,
                    'progress': placeholder_args['progress'],
                    'port': 21,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 21",
                    "D: Using passive ftp",
                    ]),
                }),
            ('method-ftp config-passive commandline-passive-false', {
                'config_method': "ftp",
                'config_extras': {
                    'host': {
                        'passive_ftp': "True",
                        },
                    },
                'test_args_extra': {
                    'commandline_passive': False,
                    },
                'get_port_for_host_return_value': None,
                'expected_kwargs': {
                    'ftp_mode': False,
                    'progress': placeholder_args['progress'],
                    'port': 21,
                    },
                'expected_stdout_output': "\n".join([
                    "D: FTP port: 21",
                    "D: Using active ftp",
                    ]),
                }),
            ]

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(
                (name, self.placeholder_args[name])
                for name in [
                    'fqdn', 'login', 'incoming', 'files_to_upload', 'debug',
                    'commandline_passive', 'progress']
                if name not in self.omit_kwargs)
        self.test_args.update({
                'method_name': self.test_method,
                'method': self.test_method_func,
                'config': self.runtime_config_parser,
                'host': self.test_host,
                })
        if hasattr(self, 'test_args_extra'):
            self.test_args.update(self.test_args_extra)

    def test_emits_expected_debug_message(self):
        """ Should emit expected debug message. """
        self.test_args['debug'] = True
        dput.dput.upload_files_via_method_ftp(**self.test_args)
        if hasattr(self, 'expected_stdout_output'):
            self.assertIn(self.expected_stdout_output, sys.stdout.getvalue())

    def test_calls_upload_method_with_expected_args(self):
        """ Should call upload method function with expected args. """
        dput.dput.upload_files_via_method_ftp(**self.test_args)
        self.expected_method_func.assert_called_with(
                *self.expected_args, **self.expected_kwargs)


class upload_files_via_method_scp_TestCase(
        testscenarios.WithScenarios,
        upload_files_via_BaseTestCase):
    """ Test cases for `upload_files_via_method_scp` function. """

    placeholder_args = dict(upload_files_via_BaseTestCase.placeholder_args)
    placeholder_args.update({
            'files_to_upload': object(),
            'debug': False,
            'compress': False,
            'ssh_config_options': None,
            })

    scenarios = [
            ('method-scp', {
                'config_method': "scp",
                'expected_kwargs': {
                    'compress': placeholder_args['compress'],
                    'ssh_config_options': [],
                    },
                'expected_stdout_output': "\n".join([
                    "D: ssh config options:",
                    "  ",
                    ]),
                }),
            ('method-scp default-compress', {
                'config_method': "scp",
                'omit_kwargs': ['compress'],
                'expected_kwargs': {
                    'compress': False,
                    'ssh_config_options': [],
                    },
                'expected_stdout_output': "\n".join([
                    "D: ssh config options:",
                    "  ",
                    ]),
                }),
            ('method-scp default-ssh-options', {
                'config_method': "scp",
                'omit_kwargs': ['ssh_config_options'],
                'expected_kwargs': {
                    'compress': placeholder_args['compress'],
                    'ssh_config_options': [],
                    },
                'expected_stdout_output': "\n".join([
                    "D: ssh config options:",
                    "  ",
                    ]),
                }),
            ('method-scp scp-compress', {
                'config_method': "scp",
                'test_args_extra': {
                    'compress': True,
                    },
                'expected_kwargs': {
                    'compress': True,
                    'ssh_config_options': [],
                    },
                'expected_stdout_output': "\n".join([
                    "D: Setting compression for scp",
                    "D: ssh config options:",
                    "  ",
                    ]),
                }),
            ('method-scp ssh-options-one', {
                'config_method': "scp",
                'test_args_extra': {
                    'compress': placeholder_args['compress'],
                    'ssh_config_options': ["lorem ipsum"],
                    },
                'expected_kwargs': {
                    'compress': placeholder_args['compress'],
                    'ssh_config_options': ["lorem ipsum"],
                    },
                'expected_stdout_output': "\n".join([
                    "D: ssh config options:",
                    "  ",
                    "  lorem ipsum",
                    "  ",
                    ]),
                }),
            ('method-scp ssh-options-three', {
                'config_method': "scp",
                'test_args_extra': {
                    'compress': placeholder_args['compress'],
                    'ssh_config_options': ["spam", "eggs", "beans"],
                    },
                'expected_kwargs': {
                    'compress': placeholder_args['compress'],
                    'ssh_config_options': ["spam", "eggs", "beans"],
                    },
                'expected_stdout_output': "\n".join([
                    "D: ssh config options:",
                    "  ",
                    "  spam",
                    "  eggs",
                    "  beans",
                    "  ",
                    ]),
                }),
            ]

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(
                (name, self.placeholder_args[name])
                for name in [
                    'fqdn', 'login', 'incoming', 'files_to_upload', 'debug',
                    'compress', 'ssh_config_options']
                if name not in self.omit_kwargs)
        self.test_args.update({
                'method_name': self.test_method,
                'method': self.test_method_func,
                'config': self.runtime_config_parser,
                'host': self.test_host,
                })
        if hasattr(self, 'test_args_extra'):
            self.test_args.update(self.test_args_extra)

    def test_emits_expected_debug_message(self):
        """ Should emit expected debug message. """
        self.test_args['debug'] = True
        dput.dput.upload_files_via_method_scp(**self.test_args)
        if hasattr(self, 'expected_stdout_output'):
            self.assertIn(self.expected_stdout_output, sys.stdout.getvalue())

    def test_calls_upload_method_with_expected_args(self):
        """ Should call upload method function with expected args. """
        dput.dput.upload_files_via_method_scp(**self.test_args)
        self.expected_method_func.assert_called_with(
                *self.expected_args, **self.expected_kwargs)


class upload_files_TestCase(
        testscenarios.WithScenarios,
        testtools.TestCase):
    """ Test cases for `upload_files` function. """

    files_scenarios = [
            ('files-none', {
                'files_to_upload': [],
                }),
            ('files-one', {
                'files_to_upload': [tempfile.mktemp()],
                }),
            ('files-three', {
                'files_to_upload': [tempfile.mktemp() for __ in range(3)],
                }),
            ]

    method_scenarios = [
            ('method-http', {
                'method_name': "http",
                'config_method': "http",
                'get_progress_indicator_for_host_return_value': 23,
                'expected_func_name': 'upload_files_via_method',
                'expected_kwargs_extra': {
                    'dummy': 0,
                    'progress': 23,
                    },
                }),
            ('method-http', {
                'method_name': "https",
                'config_method': "https",
                'get_progress_indicator_for_host_return_value': 23,
                'expected_func_name': 'upload_files_via_method',
                'expected_kwargs_extra': {
                    'dummy': 0,
                    'progress': 23,
                    },
                }),
            ('method-http', {
                'method_name': "rsync",
                'config_method': "rsync",
                'get_progress_indicator_for_host_return_value': 23,
                'expected_func_name': 'upload_files_via_method',
                'expected_kwargs_extra': {
                    'dummy': 0,
                    'progress': 23,
                    },
                }),
            ('method-local', {
                'method_name': "local",
                'config_method': "local",
                'get_progress_indicator_for_host_return_value': 23,
                'expected_func_name': 'upload_files_via_method_local',
                'expected_kwargs_extra': {
                    'compress': 0,
                    'progress': 23,
                    },
                }),
            ('method-ftp', {
                'method_name': "ftp",
                'config_method': "ftp",
                'get_progress_indicator_for_host_return_value': 23,
                'test_kwargs_extra': {
                    'commandline_passive': True,
                    },
                'expected_func_name': 'upload_files_via_method_ftp',
                'expected_kwargs_extra': {
                    'commandline_passive': True,
                    'progress': 23,
                    },
                }),
            ('method-scp ssh-config-options-none', {
                'method_name': "scp",
                'config_method': "scp",
                'config_extras': {
                    'host': {
                        'scp_compress': "True",
                        'ssh_config_options': "",
                        },
                    },
                'get_progress_indicator_for_host_return_value': 23,
                'expected_func_name': 'upload_files_via_method_scp',
                'expected_kwargs_extra': {
                    'compress': True,
                    'ssh_config_options': [],
                    },
                }),
            ('method-scp ssh-config-options-three', {
                'method_name': "scp",
                'config_method': "scp",
                'config_extras': {
                    'host': {
                        'scp_compress': "True",
                        'ssh_config_options': "spam\n\teggs\n    beans\n",
                        },
                    },
                'get_progress_indicator_for_host_return_value': 23,
                'expected_func_name': 'upload_files_via_method_scp',
                'expected_kwargs_extra': {
                    'compress': True,
                    'ssh_config_options': ["spam", "eggs", "beans"],
                    },
                }),
            ]

    delayed_scenarios = [
            ('delayed-none', {
                'delayed_queue_path': "",
                'expected_queue_annotation': None,
                }),
            ('delayed-arbitrary', {
                'delayed_queue_path': "delayed-path",
                'expected_queue_annotation': "[delayed-path]",
                }),
            ]

    scenarios = testscenarios.multiply_scenarios(
            files_scenarios, method_scenarios, delayed_scenarios)

    for (scenario_name, scenario) in scenarios:
        incoming_root = tempfile.mktemp()
        scenario['get_incoming_for_host_return_value'] = incoming_root
        expected_incoming = incoming_root
        if scenario['delayed_queue_path'] is not None:
            expected_incoming = os.path.join(
                    incoming_root,
                    scenario['delayed_queue_path'])
        scenario['expected_incoming'] = expected_incoming
        del incoming_root, expected_incoming
    del scenario_name, scenario

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()
        patch_system_interfaces(self)

        set_config(self, 'exist-simple')
        patch_runtime_config_options(self)

        self.set_files_to_upload()
        set_upload_methods(self)

        self.set_test_args()

        patch_determine_delay_days(self)
        patch_make_delayed_queue_path(self)
        patch_get_login_for_host(self)
        self.expected_login = dput.dput.get_login_for_host.return_value
        patch_get_fqdn_for_host(self)
        self.expected_fqdn = dput.dput.get_fqdn_for_host.return_value

        patch_get_incoming_for_host(self)
        self.expected_incoming = dput.dput.get_incoming_for_host.return_value
        if dput.dput.make_delayed_queue_path.return_value:
            self.expected_incoming = os.path.join(
                    dput.dput.get_incoming_for_host.return_value,
                    dput.dput.make_delayed_queue_path.return_value)

        patch_get_progress_indicator_for_host(self)
        patch_upload_files_via(self)

    def set_files_to_upload(self):
        """ Set the `files_to_upload` collection for this instance. """
        if not hasattr(self, 'files_to_upload'):
            self.files_to_upload = []

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict(
                upload_methods=self.upload_methods,
                config=self.runtime_config_parser,
                host=self.test_host,
                files_to_upload=self.files_to_upload,
                debug=False,
                )
        if hasattr(self, 'test_kwargs_extra'):
            self.test_args.update(self.test_kwargs_extra)
        if hasattr(self, 'test_simulate'):
            self.test_args['simulate'] = self.test_simulate

    def test_emits_expected_queue_message(self):
        """ Should emit expected message for the upload queue. """
        dput.dput.upload_files(**self.test_args)
        if self.expected_queue_annotation is not None:
            expected_destination_template = "{host} {annotation}"
        else:
            expected_destination_template = "{host}"
        expected_destination = expected_destination_template.format(
                host=self.test_host,
                annotation=self.expected_queue_annotation)
        expected_method_name = self.method_name
        expected_output = (
                "Uploading to {destination}"
                " (via {method} to {fqdn})"
                ).format(
                    destination=expected_destination,
                    method=expected_method_name, fqdn=self.expected_fqdn)
        self.assertIn(expected_output, sys.stdout.getvalue())

    def test_emits_expected_diagnostic_message(self):
        """ Should emit expected diagnostic debug message. """
        self.test_args['debug'] = True
        dput.dput.upload_files(**self.test_args)
        self.expected_fqdn = dput.dput.get_fqdn_for_host.return_value
        self.expected_login = dput.dput.get_login_for_host.return_value
        expected_output = textwrap.dedent("""\
                D: FQDN: {fqdn}
                D: Login: {login}
                D: Incoming: {incoming}
                """).format(
                    fqdn=self.expected_fqdn,
                    login=self.expected_login,
                    incoming=self.expected_incoming)
        self.assertIn(expected_output, sys.stdout.getvalue())

    def test_calls_expected_upload_function_with_expected_args(self):
        """ Should call expected upload function with expected args. """
        dput.dput.upload_files(**self.test_args)
        expected_func = getattr(dput.dput, self.expected_func_name)
        expected_method_func = self.upload_methods[self.method_name]
        expected_args_list = [
                self.method_name,
                expected_method_func,
                self.runtime_config_parser,
                self.test_host,
                self.expected_fqdn,
                self.expected_login,
                self.expected_incoming,
                self.files_to_upload,
                self.test_args['debug'],
                ]
        self.expected_args = tuple(expected_args_list)
        self.expected_kwargs = {}
        if hasattr(self, 'expected_kwargs_extra'):
            self.expected_kwargs.update(self.expected_kwargs_extra)
        expected_func.assert_called_with(
                *self.expected_args, **self.expected_kwargs)

    def test_omits_upload_method_function_when_simulate(self):
        """ Should omit call to upload method function when ‘simulate’. """
        self.test_args['simulate'] = True
        dput.dput.upload_files(**self.test_args)
        expected_func = getattr(dput.dput, self.expected_func_name)
        self.assertFalse(
                expected_func.called,
                "should not call {func}".format(func=expected_func))


def patch_upload_files(testcase):
    """ Patch the `upload_files` function for `testcase`. """
    func_patcher = unittest.mock.patch.object(
            dput.dput, "upload_files", autospec=True)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


class parse_commandline_BaseTestCase(
        testtools.TestCase):
    """ Base class for test cases for function `parse_commandline`. """

    function_to_test = staticmethod(dput.dput.parse_commandline)

    progname = "lorem"
    dput_version = "ipsum"
    dput_usage_message = "Lorem ipsum, dolor sit amet."

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()
        patch_system_interfaces(self)

        setup_changes_file_fixtures(self)
        set_changes_file_scenario(self, 'exist-minimal')
        self.set_files_to_upload()

        self.patch_make_usage_message()
        self.patch_distribution()
        patch_get_login_for_host(self)
        patch_verify_config_upload_methods(self)

        self.options_defaults = dict(self.function_to_test.options_defaults)
        self.patch_getopt()

        if not hasattr(self, 'test_argv'):
            self.test_argv = self.make_test_argv()
        self.test_args = dict(
                argv=self.test_argv,
                )
        if hasattr(self, 'expected_options'):
            self.set_expected_options_mapping()
            self.set_expected_args()

    def set_files_to_upload(self):
        """ Set the `files_to_upload` collection for this test case. """
        if not hasattr(self, 'files_to_upload'):
            self.files_to_upload = [self.changes_file_double]

    def make_test_argv(self):
        """ Make the `argv` value for this test case. """
        test_argv = [self.progname] + [
                fake_file.path for fake_file in self.files_to_upload]
        return test_argv

    def patch_make_usage_message(self):
        """ Patch the `make_usage_message` function. """
        if not hasattr(self, 'dput_usage_message'):
            self.dput_usage_message = self.getUniqueString()
        func_patcher = unittest.mock.patch.object(
                dput.dput, "make_usage_message", autospec=True,
                return_value=self.dput_usage_message)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_distribution(self):
        """ Patch the Python distribution for this test case. """
        self.fake_distribution = unittest.mock.MagicMock(
                pkg_resources.Distribution)
        if hasattr(self, 'dput_version'):
            self.fake_distribution.version = self.dput_version
        patch_pkg_resources_get_distribution(self)

    def set_expected_args(self):
        """ Set the `expected_args` collection for this test case. """
        if not hasattr(self, 'expected_args'):
            self.expected_args = self.getopt_args

    def set_expected_options_mapping(self):
        """ Set the `expected_options_mapping` for this test case. """
        if not hasattr(self, 'expected_options_mapping'):
            self.expected_options_mapping = self.options_defaults.copy()
            self.expected_options_mapping.update(self.expected_options)

    def patch_getopt(self):
        """ Patch the `dputhelper.getopt` function. """
        if not hasattr(self, 'getopt_opts'):
            self.getopt_opts = []
        if not hasattr(self, 'getopt_args'):
            self.getopt_args = [
                    fake_file.path for fake_file in self.files_to_upload]

        patch_getopt(self)


class parse_commandline_TestCase(
        testscenarios.WithScenarios,
        parse_commandline_BaseTestCase):
    """ Test cases for function `parse_commandline`. """

    scenarios = [
            ('host-omitted files-one options-none', {
                'test_argv': ["commandname"] + [
                    make_changes_file_path(tempfile.mktemp())],
                'expected_options': {},
                }),
            ('host-omitted files-three options-none', {
                'test_argv': ["commandname"] + [
                    make_changes_file_path(tempfile.mktemp())
                    for __ in range(3)],
                'expected_options': {},
                }),
            ('host-named files-one options-none', {
                'test_argv': ["commandname"] + ["lorem"] + [
                    make_changes_file_path(tempfile.mktemp())],
                'expected_options': {},
                }),
            ('host-named files-three options-none', {
                'test_argv': ["commandname"] + ["lorem"] + [
                    make_changes_file_path(tempfile.mktemp())
                    for __ in range(3)],
                'expected_options': {},
                }),
            ]

    def test_calls_getopt_with_expected_args(self):
        """ Should call `getopt` with expected arguments. """
        self.function_to_test(**self.test_args)
        dputhelper.getopt.assert_called_with(
                self.test_argv[1:],
                unittest.mock.ANY, unittest.mock.ANY)

    def test_returns_expected_values(self):
        """ Should return expected 2-tuple of (`options`, `args`). """
        (options, args) = self.function_to_test(**self.test_args)
        options_mapping = {
                name: getattr(options, name)
                for name in dir(options)
                if not name.startswith("_")}
        self.assertEqual(self.expected_options_mapping, options_mapping)
        self.assertEqual(self.expected_args, args)

    @unittest.mock.patch.object(dput.dput, 'debug', new=object())
    def test_sets_debug_flag_when_debug_option(self):
        """ Should set `debug` when ‘--debug’ option. """
        self.getopt_opts = [("--debug", None)]
        self.function_to_test(**self.test_args)
        self.assertEqual(dput.dput.debug, True)


class parse_commandline_ValidOptionsTestCase(
        testscenarios.WithScenarios,
        parse_commandline_BaseTestCase):
    """ Test cases for function `parse_commandline`, valid options. """

    args_scenarios = [
            ('host-omitted files-one', {
                'test_argv_command': "commandname",
                'test_argv_args': [
                    make_changes_file_path(tempfile.mktemp())],
                }),
            ('host-omitted files-three', {
                'test_argv_command': "commandname",
                'test_argv_args': [
                    make_changes_file_path(tempfile.mktemp())
                    for __ in range(3)],
                }),
            ('host-named files-one', {
                'test_argv_command': "commandname",
                'test_argv_args': ["lorem"] + [
                    make_changes_file_path(tempfile.mktemp())],
                }),
            ('host-named files-three', {
                'test_argv_command': "commandname",
                'test_argv_args': ["lorem"] + [
                    make_changes_file_path(tempfile.mktemp())
                    for __ in range(3)],
                }),
            ]

    options_scenarios = [
            ('options-debug-short', {
                'test_argv_options': ["-d"],
                'expected_options': {
                    'debug': True,
                    },
                }),
            ('options-debug-long', {
                'test_argv_options': ["--debug"],
                'expected_options': {
                    'debug': True,
                    },
                }),
            ('options-dinstall-short', {
                'test_argv_options': ["-D"],
                'expected_options': {
                    'run_dinstall': True,
                    },
                }),
            ('options-dinstall-long', {
                'test_argv_options': ["--dinstall"],
                'expected_options': {
                    'run_dinstall': True,
                    },
                }),
            ('options-config-short', {
                'test_argv_options': ["-c", "/tmp/nonexistent"],
                'expected_options': {
                    'config_file_path': "/tmp/nonexistent",
                    },
                }),
            ('options-config-long', {
                'test_argv_options': ["--config", "/tmp/nonexistent"],
                'expected_options': {
                    'config_file_path': "/tmp/nonexistent",
                    },
                }),
            ('options-force-short', {
                'test_argv_options': ["-f"],
                'expected_options': {
                    'force_upload': True,
                    },
                }),
            ('options-force-long', {
                'test_argv_options': ["--force"],
                'expected_options': {
                    'force_upload': True,
                    },
                }),
            ('options-host-list-short', {
                'test_argv_options': ["-H"],
                'expected_options': {
                    'config_host_list': True,
                    },
                }),
            ('options-host-list-long', {
                'test_argv_options': ["--host-list"],
                'expected_options': {
                    'config_host_list': True,
                    },
                }),
            ('options-lintian-short', {
                'test_argv_options': ["-l"],
                'expected_options': {
                    'run_lintian': True,
                    },
                }),
            ('options-lintian-long', {
                'test_argv_options': ["--lintian"],
                'expected_options': {
                    'run_lintian': True,
                    },
                }),
            ('options-no-upload-log-short', {
                'test_argv_options': ["-U"],
                'expected_options': {
                    'upload_log': False,
                    },
                }),
            ('options-no-upload-log-long', {
                'test_argv_options': ["--no-upload-log"],
                'expected_options': {
                    'upload_log': False,
                    },
                }),
            ('options-check-only-short', {
                'test_argv_options': ["-o"],
                'expected_options': {
                    'check_only': True,
                    },
                }),
            ('options-check-only-long', {
                'test_argv_options': ["--check-only"],
                'expected_options': {
                    'check_only': True,
                    },
                }),
            ('options-print-short', {
                'test_argv_options': ["-p"],
                'expected_options': {
                    'config_print': True,
                    },
                }),
            ('options-print-long', {
                'test_argv_options': ["--print"],
                'expected_options': {
                    'config_print': True,
                    },
                }),
            ('options-passive-short', {
                'test_argv_options': ["-P"],
                'expected_options': {
                    'commandline_passive': True,
                    },
                }),
            ('options-passive-long', {
                'test_argv_options': ["--passive"],
                'expected_options': {
                    'commandline_passive': True,
                    },
                }),
            ('options-simulate-short', {
                'test_argv_options': ["-s"],
                'expected_options': {
                    'simulate': True,
                    },
                }),
            ('options-simulate-long', {
                'test_argv_options': ["--simulate"],
                'expected_options': {
                    'simulate': True,
                    },
                }),
            ('options-unchecked-short', {
                'test_argv_options': ["-u"],
                'expected_options': {
                    'allow_unsigned_uploads': True,
                    },
                }),
            ('options-unchecked-long', {
                'test_argv_options': ["--unchecked"],
                'expected_options': {
                    'allow_unsigned_uploads': True,
                    },
                }),
            ('options-delayed-short', {
                'test_argv_options': ["-e", "7"],
                'expected_options': {
                    'delay_days_text': "7",
                    },
                }),
            ('options-delayed-long', {
                'test_argv_options': ["--delayed", "7"],
                'expected_options': {
                    'delay_days_text': "7",
                    },
                }),
            ('options-check-version-short', {
                'test_argv_options': ["-V"],
                'expected_options': {
                    'check_version': True,
                    },
                }),
            ('options-check-version-long', {
                'test_argv_options': ["--check-version"],
                'expected_options': {
                    'check_version': True,
                    },
                }),
            ('options-check-version-no-upload-log-lintian-long', {
                'test_argv_options': [
                    "--check-version",
                    "--no-upload-log",
                    "--lintian",
                    ],
                'getopt_opts': [
                    ("--check-version", None),
                    ("--no-upload-log", None),
                    ("--lintian", None),
                    ],
                'expected_options': {
                    'check_version': True,
                    'run_lintian': True,
                    'upload_log': False,
                    },
                }),
            # Legacy mis-spelled option name.
            ('options-check_version-long-legacy', {
                'test_argv_options': ["--check_version"],
                'getopt_opts': [
                    # The parameters to `getopt` are such that the parsed
                    # options will never contain the option spelled this way.
                    # This scenario is designed only to exercise the legacy
                    # code.
                    ("--check_version", None),
                    ],
                'expected_options': {
                    'check_version': True,
                    },
                }),
            ('options-check_version-no-upload-log-lintian-long', {
                'test_argv_options': [
                    "--check_version",
                    "--no-upload-log",
                    "--lintian",
                    ],
                'getopt_opts': [
                    # The parameters to `getopt` are such that the parsed
                    # options will never contain the option spelled this way.
                    # This scenario is designed only to exercise the legacy
                    # code.
                    ("--check_version", None),
                    ("--no-upload-log", None),
                    ("--lintian", None),
                    ],
                'expected_options': {
                    'check_version': True,
                    'run_lintian': True,
                    'upload_log': False,
                    },
                }),
            ]

    scenarios = testscenarios.multiply_scenarios(
            args_scenarios, options_scenarios)
    for (scenario_name, scenario) in scenarios:
        scenario['test_argv'] = [scenario['test_argv_command']]
        scenario['test_argv'].extend(scenario['test_argv_options'])
        scenario['test_argv'].extend(scenario['test_argv_args'])
        scenario['expected_arguments'] = scenario['test_argv_args']
        scenario['getopt_args'] = scenario['test_argv_args']
        if 'getopt_opts' not in scenario:
            fake_parsed_option = scenario['test_argv_options'][0]
            fake_parsed_option_value = (
                    scenario['test_argv_options'][1] if len(
                        scenario['test_argv_options']) > 1
                    else None)
            scenario['getopt_opts'] = [
                    (fake_parsed_option, fake_parsed_option_value),
                    ]
    del scenario_name, scenario

    def test_calls_getopt_with_expected_args(self):
        """ Should call `getopt` with expected arguments. """
        self.function_to_test(**self.test_args)
        dputhelper.getopt.assert_called_with(
                self.test_argv[1:],
                unittest.mock.ANY, unittest.mock.ANY)

    def test_returns_expected_values(self):
        """ Should return expected 2-tuple of (`options`, `args`). """
        (options, args) = self.function_to_test(**self.test_args)
        options_mapping = {
                name: getattr(options, name)
                for name in dir(options)
                if not name.startswith("_")}
        self.assertEqual(self.expected_options_mapping, options_mapping)
        self.assertEqual(self.expected_args, args)


class parse_commandline_ErrorTestCase(parse_commandline_BaseTestCase):
    """ Test cases for `parse_commandline`, error conditions. """

    def test_calls_sys_exit_when_no_nonoption_args(self):
        """ Should call `sys.exit` when no non-option args. """
        self.getopt_args = []
        for option in [
                "--debug", "--force", "--simulate", "--lintian"]:
            self.getopt_opts = [(option, None)]
            with self.subTest(test_opts=self.getopt_opts):
                with testtools.ExpectedException(FakeSystemExit):
                    self.function_to_test(**self.test_args)
                sys.exit.assert_called_with(os.EX_USAGE)

    def test_calls_sys_exit_when_unknown_option(self):
        """ Should call `sys.exit` when an unknown option. """
        self.getopt_args.insert(1, "--b0gUs")
        dputhelper.getopt.side_effect = dputhelper.DputException
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        sys.exit.assert_called_with(os.EX_USAGE)


class parse_commandline_PrintAndEndTestCase(parse_commandline_BaseTestCase):
    """ Test cases for `parse_commandline` that print and end. """

    def test_emits_usage_message_when_option_help(self):
        """ Should emit usage message when ‘--help’ option. """
        self.getopt_opts = [("--help", None)]
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_output = self.dput_usage_message
        self.assertIn(expected_output, sys.stdout.getvalue())

    def test_calls_sys_exit_when_option_help(self):
        """ Should call `sys.exit` when ‘--help’ option. """
        self.getopt_opts = [("--help", None)]
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        sys.exit.assert_called_with(EXIT_STATUS_SUCCESS)

    def test_emits_version_message_when_option_version(self):
        """ Should emit version message when ‘--version’ option. """
        self.getopt_opts = [("--version", None)]
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_progname = self.progname
        expected_version = self.fake_distribution.version
        expected_output = "{progname} {version}".format(
                progname=expected_progname, version=expected_version)
        self.assertIn(expected_output, sys.stdout.getvalue())

    def test_calls_sys_exit_when_option_version(self):
        """ Should call `sys.exit` when ‘--version’ option. """
        self.getopt_opts = [("--version", None)]
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        sys.exit.assert_called_with(EXIT_STATUS_SUCCESS)

    def test_returns_options_normally_when_option_print_config(self):
        """ Should set ‘config_print’ when option ‘--print’ is present. """
        self.getopt_opts = [("--print", None)]
        self.getopt_args = []
        (options, args) = self.function_to_test(**self.test_args)
        self.assertEqual(True, options.config_print)

    def test_returns_options_normally_when_option_host_list(self):
        """ Should set ‘config_host_list’ when option ‘--host-list’. """
        self.getopt_opts = [("--host-list", None)]
        self.getopt_args = []
        (options, args) = self.function_to_test(**self.test_args)
        self.assertEqual(True, options.config_host_list)


class determine_delay_days_TestCase(
        testscenarios.WithScenarios,
        testtools.TestCase):
    """ Test cases for `determine_delay_days` function. """

    function_to_test = staticmethod(dput.dput.determine_delay_days)

    commandline_scenarios = [
            ('commandline-unspecified', {}),
            ('commandline-specified-empty-string', {
                'test_commandline_text_value': "",
                'expected_exception': TypeError,
                }),
            ('commandline-specified-non-numeric-string', {
                'test_commandline_text_value': "DEADB33F",
                'expected_exception': TypeError,
                }),
            ('commandline-specified-numerals', {
                'test_commandline_text_value': "3",
                'expected_result': 3,
                }),
            ('commandline-specified-numerals-out-of-range', {
                'test_commandline_text_value': "16",
                'expected_exception': ValueError,
                }),
            ('commandline-specified-negative-number', {
                'test_commandline_text_value': "-3",
                'expected_exception': ValueError,
                }),
            ]

    config_scenarios = [
            ('config-unspecified', {
                'config_delayed': None,
                }),
            ('config-specified-empty-string', {
                'config_delayed': "",
                'expected_value_if_host_config_used': None,
                }),
            ('config-specified-numerals', {
                'config_delayed': "9",
                'expected_value_if_host_config_used': 9,
                }),
            ]

    config_default_scenarios = [
            ('config-default-unspecified', {
                'config_default_delayed': None,
                'expected_value_if_config_default_used': None,
                }),
            ('config-default-specified-empty-string', {
                'config_default_delayed': "",
                'expected_value_if_config_default_used': None,
                }),
            ('config-default-specified-numerals', {
                'config_default_delayed': "9",
                'expected_value_if_config_default_used': 9,
                }),
            ]

    scenarios = testscenarios.multiply_scenarios(
            commandline_scenarios, config_scenarios, config_default_scenarios)

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        set_config(self, 'exist-simple')
        patch_runtime_config_options(self)

        self.set_test_args()
        self.set_expected_result()

    def set_test_args(self):
        """ Set the ‘test_args’ value for this test case. """
        test_options = types.SimpleNamespace()
        if hasattr(self, 'test_commandline_text_value'):
            test_options.delay_days_text = (
                    self.test_commandline_text_value)
        test_config_params = self.config_scenario['configs_by_name']['runtime']
        test_config_parser = test_config_params['config']
        self.test_args = {
                'options': test_options,
                'host_config': test_config_parser[self.test_host],
                }

    def set_expected_result(self):
        """ Set the ‘expected_result’ value for this test case. """
        if not hasattr(self, 'expected_result'):
            if not hasattr(self, 'expected_value_if_host_config_used'):
                if not hasattr(self, 'expected_value_if_config_default_used'):
                    self.expected_result = None
                else:
                    self.expected_result = (
                            self.expected_value_if_config_default_used)
            else:
                self.expected_result = self.expected_value_if_host_config_used

    def test_returns_expected_result(self):
        """ Should give expected result for inputs. """
        if hasattr(self, 'expected_exception'):
            with testtools.ExpectedException(self.expected_exception):
                self.function_to_test(**self.test_args)
        else:
            result = self.function_to_test(**self.test_args)
            self.assertEqual(self.expected_result, result)


def patch_determine_delay_days(testcase):
    """ Patch the `determine_delay_days` function for the `testcase`. """
    if not hasattr(testcase, 'determine_delay_days_return_value'):
        testcase.determine_delay_days_return_value = None
    func_patcher = unittest.mock.patch.object(
            dput.dput, "determine_delay_days", autospec=True,
            return_value=testcase.determine_delay_days_return_value)
    func_patcher.start()
    testcase.addCleanup(func_patcher.stop)


class main_TestCase(
        testtools.TestCase):
    """ Base for test cases for `main` function. """

    function_to_test = staticmethod(dput.dput.main)

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()
        patch_system_interfaces(self)

        set_config(
                self,
                getattr(self, 'config_scenario_name', 'exist-simple'))
        patch_runtime_config_options(self)

        setup_changes_file_fixtures(self)
        set_changes_file_scenario(self, 'exist-minimal')

        patch_os_isatty(self)
        patch_os_environ(self)
        patch_os_getuid(self)
        patch_pwd_getpwuid(self)
        patch_sys_argv(self)

        self.set_files_to_upload()
        set_upload_methods(self)

        self.set_test_args()

        self.patch_make_usage_message()
        self.patch_distribution()

        self.options_defaults = dict(
                dput.dput.parse_commandline.options_defaults)
        self.patch_parse_commandline()
        patch_active_config_files(self)
        patch_read_configs(self)
        patch_print_config(self)
        patch_get_username_from_system(self)
        patch_get_login_for_host(self)
        patch_import_upload_functions(self)
        patch_get_upload_method_name_for_host(self)
        patch_make_delayed_queue_path(self)
        patch_upload_files(self)
        patch_get_progress_indicator_for_host(self)
        patch_get_fqdn_for_host(self)
        patch_get_incoming_for_host(self)
        patch_determine_delay_days(self)
        patch_print_default_upload_method(self)
        patch_print_host_list(self)
        self.patch_guess_upload_host()
        self.patch_check_upload_logfile()
        self.patch_verify_files()
        self.patch_run_lintian_test()
        self.patch_execute_command()
        self.patch_create_upload_file()
        self.patch_dinstall_caller()

    def set_files_to_upload(self):
        """ Set the `files_to_upload` collection for this instance. """
        if not hasattr(self, 'files_to_upload'):
            self.files_to_upload = []

    def set_test_args(self):
        """ Set the arguments for the test call to the function. """
        self.test_args = dict()

    def patch_make_usage_message(self):
        """ Patch the `make_usage_message` function. """
        if not hasattr(self, 'dput_usage_message'):
            self.dput_usage_message = self.getUniqueString()
        func_patcher = unittest.mock.patch.object(
                dput.dput, "make_usage_message", autospec=True,
                return_value=self.dput_usage_message)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_distribution(self):
        """ Patch the Python distribution for this test case. """
        self.fake_distribution = unittest.mock.MagicMock(
                pkg_resources.Distribution)
        if hasattr(self, 'dput_version'):
            self.fake_distribution.version = self.dput_version
        patch_pkg_resources_get_distribution(self)

    def set_fake_options(self, options_mapping=None):
        """ Set the `fake_options` object, updated with `options_mapping`. """
        if options_mapping is None:
            options_mapping = self.fake_options_mapping
        full_options_mapping = dict(itertools.chain(
                self.options_defaults.items(),
                options_mapping.items(),
                ))
        self.fake_options = make_options_namespace(full_options_mapping)

    def patch_parse_commandline(self):
        """ Patch the `parse_commandline` function. """
        if not hasattr(self, 'fake_options_mapping'):
            self.fake_options_mapping = {}
        self.set_fake_options(self.fake_options_mapping)
        if not hasattr(self, 'fake_args'):
            self.fake_args = [self.changes_file_double.path]

        def fake_parse_commandline(*args, **kwargs):
            return (self.fake_options, self.fake_args)

        func_patcher = unittest.mock.patch.object(
                dput.dput, "parse_commandline",
                side_effect=fake_parse_commandline)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_guess_upload_host(self):
        """ Patch the `guess_upload_host` function. """
        if not hasattr(self, 'guess_upload_host_return_value'):
            self.guess_upload_host_return_value = self.test_host
        func_patcher = unittest.mock.patch.object(
                dput.dput, "guess_upload_host", autospec=True,
                return_value=self.guess_upload_host_return_value)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_check_upload_logfile(self):
        """ Patch the `check_upload_logfile` function. """
        func_patcher = unittest.mock.patch.object(
                dput.dput, "check_upload_logfile", autospec=True)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_verify_files(self):
        """ Patch the `verify_files` function. """
        func_patcher = unittest.mock.patch.object(
                dput.dput, "verify_files", autospec=True,
                return_value=self.files_to_upload)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_run_lintian_test(self):
        """ Patch the `run_lintian_test` function. """
        func_patcher = unittest.mock.patch.object(
                dput.dput, "run_lintian_test", autospec=True)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_execute_command(self):
        """ Patch the `execute_command` function. """
        func_patcher = unittest.mock.patch.object(
                dput.dput, "execute_command", autospec=True)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_create_upload_file(self):
        """ Patch the `create_upload_file` function. """
        func_patcher = unittest.mock.patch.object(
                dput.dput, "create_upload_file", autospec=True)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)

    def patch_dinstall_caller(self):
        """ Patch the `dinstall_caller` function. """
        func_patcher = unittest.mock.patch.object(
                dput.dput, "dinstall_caller", autospec=True)
        func_patcher.start()
        self.addCleanup(func_patcher.stop)


class main_CommandLineProcessingTestCase(main_TestCase):
    """ Test cases for `main` command-line processing. """

    sys_argv = ["lorem"] + [
            make_changes_file_path(tempfile.mktemp())
            for __ in range(3)]

    def test_calls_parse_commandline_with_expected_args(self):
        """ Should call `parse_commandline` with expected arguments. """
        self.function_to_test(**self.test_args)
        dput.dput.parse_commandline.assert_called_with(self.sys_argv)

    def test_returns_exit_status_when_systemexit(self):
        """ Should return exit status from `SystemExit` exception. """
        exit_status = self.getUniqueInteger()
        dput.dput.parse_commandline.side_effect = SystemExit(exit_status)
        result = self.function_to_test(**self.test_args)
        self.assertEqual(result, exit_status)


class main_CommandLineProcessing_ErrorTestCase(main_TestCase):
    """ Test cases for `main` command-line processing, error conditions. """

    fake_args = [
            make_changes_file_path(tempfile.mktemp())
            for __ in range(3)]

    def test_emits_error_message_when_filename_not_match_glob(self):
        """ Should emit an error message when filename does not match glob. """
        self.fake_args[2] = tempfile.mktemp()
        self.patch_parse_commandline()
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        self.expected_stderr_output = (
                "Filename does not match ‘*.changes’: {}".format(
                    self.fake_args[2]))
        self.assertIn(self.expected_stderr_output, sys.stderr.getvalue())

    def test_calls_sys_exit_when_filename_not_match_glob(self):
        """ Should call `sys.exit` when filename does not match glob. """
        self.fake_args[2] = tempfile.mktemp()
        self.patch_parse_commandline()
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)

    def test_emits_error_when_delayed_invalid(self):
        """ Should emit error message when ‘--delayed’ invalid. """
        days_error = TypeError("wrong")
        dput.dput.determine_delay_days.side_effect = days_error
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_output = textwrap.dedent("""\
                Incorrect delayed argument: {exc_type}: ...
                """.format(
                    exc_type=type(days_error).__name__))
        self.assertThat(
                sys.stdout.getvalue(),
                testtools.matchers.DocTestMatches(
                    expected_output, doctest.ELLIPSIS))

    def test_emits_error_when_delayed_value_out_of_range(self):
        """ Should emit error message when ‘--delayed’ value out of range. """
        days_error = ValueError("bad")
        dput.dput.determine_delay_days.side_effect = days_error
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_output = textwrap.dedent("""\
                Incorrect delayed argument: {exc_type}: ...
                """.format(
                    exc_type=type(days_error).__name__))
        self.assertThat(
                sys.stdout.getvalue(),
                testtools.matchers.DocTestMatches(
                    expected_output, doctest.ELLIPSIS))

    def test_calls_sys_exit_when_invalid_delayed_value(self):
        """ Should call `sys.exit` when ‘--delayed’ invalid. """
        days_error = ValueError("bad")
        dput.dput.determine_delay_days.side_effect = days_error
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        sys.exit.assert_called_with(os.EX_USAGE)


class main_DebugMessageTestCase(main_TestCase):
    """ Test cases for `main` debug messages. """

    progname = "lorem"
    dput_version = "ipsum"

    @unittest.mock.patch.object(dput.dput, 'debug', new=True)
    def test_emits_debug_message_with_dput_version(self):
        """ Should emit debug message showing Dput version string. """
        self.fake_options.debug = True
        self.function_to_test(**self.test_args)
        expected_progname = self.progname
        expected_version = self.fake_distribution.version
        expected_output = textwrap.dedent("""\
                D: {progname} {version}
                """).format(
                    progname=expected_progname,
                    version=expected_version)
        self.assertIn(expected_output, sys.stdout.getvalue())


class main_DefaultFunctionCallTestCase(main_TestCase):
    """ Test cases for `main` defaults calling other functions. """

    def test_calls_import_upload_functions_with_expected_args(self):
        """ Should call `import_upload_functions` with expected arguments. """
        self.function_to_test(**self.test_args)
        dput.dput.import_upload_functions.assert_called_with()


class main_ConfigFileTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` specification of configuration file. """

    scenarios = [
            ('default', {
                'expected_config_path': "",
                }),
            ('config-from-command-line', {
                'fake_options_mapping': {
                    'config_file_path': "lorem.conf",
                    },
                'expected_config_path': "lorem.conf",
                }),
            ]

    def test_calls_active_config_files_with_expected_args(self):
        """ Should call `active_config_files` with expected arguments. """
        expected_args = (self.expected_config_path, unittest.mock.ANY)
        self.function_to_test(**self.test_args)
        dput.configfile.active_config_files.assert_called_with(*expected_args)

    def test_calls_read_configs_with_expected_args(self):
        """ Should call `read_configs` with expected arguments. """
        expected_config_files = self.fake_config_files
        expected_args = (expected_config_files, unittest.mock.ANY)
        self.function_to_test(**self.test_args)
        dput.dput.read_configs.assert_called_with(*expected_args)

    def test_calls_sys_exit_if_configuration_error_when_opening_files(self):
        """
        Should call `sys.exit` if `ConfigurationError` from opening files.
        """
        set_config(self, 'all-not-exist')
        self.set_test_args()
        fake_error_text = "Could not open any configfile, tried ‘foo’, ‘bar’"
        fake_error = dput.configfile.ConfigurationError(fake_error_text)
        dput.configfile.active_config_files.side_effect = fake_error
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_output = textwrap.dedent("""\
                ...{error_text}...
                """.format(error_text=fake_error_text))
        self.assertThat(
                sys.stderr.getvalue(),
                testtools.matchers.DocTestMatches(
                    expected_output, flags=doctest.ELLIPSIS))
        sys.exit.assert_called_with(EXIT_STATUS_FAILURE)

    def test_calls_sys_exit_if_configuration_error_when_reading_files(self):
        """
        Should call `sys.exit` if `ConfigurationError` from reading files.
        """
        fake_error_text = "Hidden Illuminati messages in config file"
        fake_error = dput.configfile.ConfigurationError(fake_error_text)
        dput.dput.read_configs.side_effect = fake_error
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_output = textwrap.dedent("""\
                ...{error_text}...
                """.format(error_text=fake_error_text))
        self.assertThat(
                sys.stderr.getvalue(),
                testtools.matchers.DocTestMatches(
                    expected_output, flags=doctest.ELLIPSIS))
        sys.exit.assert_called_with(EXIT_STATUS_FAILURE)


class main_PrintAndEndTestCase(main_TestCase):
    """ Test cases for `main` that print and end. """

    progname = "lorem"
    dput_version = "ipsum"
    dput_usage_message = "Lorem ipsum, dolor sit amet."

    def test_print_config_when_option_print(self):
        """ Should call `print_config` when ‘--print’. """
        self.fake_options_mapping.update({'config_print': True})
        self.set_fake_options()
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        dput.dput.print_config.assert_called_with(
                self.runtime_config_parser, dput.dput.debug)

    def test_print_config_then_sys_exit_when_option_print(self):
        """ Should call `print_config`, then `sys.exit`, when ‘--print’. """
        self.fake_options_mapping.update({'config_print': True})
        self.set_fake_options()
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        sys.exit.assert_called_with(EXIT_STATUS_SUCCESS)

    def test_calls_print_default_upload_method_when_option_host_list(self):
        """ Should call `print_default_upload_method` when ‘--host-list’. """
        self.fake_options_mapping.update({'config_host_list': True})
        self.set_fake_options()
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        dput.dput.print_default_upload_method.assert_called_with(
                self.runtime_config_parser)

    def test_calls_print_host_list_when_option_host_list(self):
        """ Should call `print_host_list` when option ‘--host-list’. """
        self.fake_options_mapping.update({'config_host_list': True})
        self.set_fake_options()
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        dput.dput.print_host_list.assert_called_with(
                self.runtime_config_parser)

    def test_calls_sys_exit_when_option_host_list(self):
        """ Should call `sys.exit` when option ‘--host-list’. """
        self.fake_options_mapping.update({'config_host_list': True})
        self.set_fake_options()
        with testtools.ExpectedException(FakeSystemExit):
            self.function_to_test(**self.test_args)
        sys.exit.assert_called_with(EXIT_STATUS_SUCCESS)


class main_NamedHost_TestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function, named host processing. """

    scenarios = [
            ('host-from-command-line', {
                'config_scenario_name': "exist-simple-host-three",
                'fake_args': ["bar", "lorem.changes", "ipsum.changes"],
                'expected_host': "bar",
                'expected_debug_output': "D: Host bar found in config",
                }),
            ('host-from-command-line check-only', {
                'config_scenario_name': "exist-simple-host-three",
                'fake_options_mapping': {'check_only': True},
                'fake_args': ["bar", "lorem.changes", "ipsum.changes"],
                'expected_host': "bar",
                'expected_debug_output': "D: Host bar found in config",
                }),
            ('host-from-command-line force-upload', {
                'config_scenario_name': "exist-simple-host-three",
                'fake_options_mapping': {'force_upload': True},
                'fake_args': ["bar", "lorem.changes", "ipsum.changes"],
                'expected_host': "bar",
                'expected_debug_output': "D: Host bar found in config",
                }),
            ('only-changes-filenames', {
                'config_scenario_name': "exist-simple-host-three",
                'fake_args': ["lorem.changes", "ipsum.changes"],
                'expected_host': "foo",
                'expected_debug_output': "D: No host named on command line.",
                }),
            ('only-changes-filenames check-only', {
                'config_scenario_name': "exist-simple-host-three",
                'fake_options_mapping': {
                    'debug': True,
                    'check_only': True,
                },
                'fake_args': ["lorem.changes", "ipsum.changes"],
                'expected_host': "foo",
                'expected_debug_output': "D: No host named on command line.",
                'follow_the_goddamned_branches': True,
                }),
            ]

    def test_emits_expected_debug_message(self):
        """ Should emit expected debug message. """
        self.fake_options.debug = True
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_output = self.expected_debug_output.format(
                host=self.expected_host)
        self.assertIn(expected_output, sys.stdout.getvalue())

    def test_check_upload_logfile_called_with_expected_host(self):
        """ Should call `check_upload_logfile` with expected host. """
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_fqdn = dput.dput.get_fqdn_for_host.return_value
        expected_args = (
                unittest.mock.ANY, self.expected_host, expected_fqdn,
                unittest.mock.ANY, unittest.mock.ANY,
                unittest.mock.ANY, unittest.mock.ANY)
        dput.dput.check_upload_logfile.assert_called_with(*expected_args)

    def test_verify_files_called_with_expected_host(self):
        """ Should call `verify_files` with expected host. """
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        expected_args = (
                unittest.mock.ANY, unittest.mock.ANY, self.expected_host,
                unittest.mock.ANY, unittest.mock.ANY,
                unittest.mock.ANY, unittest.mock.ANY, unittest.mock.ANY)
        dput.dput.verify_files.assert_called_with(*expected_args)


class main_NamedHostNotInConfigTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function, named host not in config. """

    scenarios = [
            ('host-from-command-line', {
                'config_scenario_name': "exist-simple-host-three",
                'fake_args': ["b0gUs", "lorem.changes", "ipsum.changes"],
                'expected_stderr_output': "No host b0gUs found in config.",
                'expected_exception': FakeSystemExit,
                'expected_exit_status': EXIT_STATUS_FAILURE,
                }),
            ('host-from-command-line check-only', {
                'config_scenario_name': "exist-simple-host-three",
                'fake_options_mapping': {'check_only': True},
                'fake_args': ["b0gUs", "lorem.changes", "ipsum.changes"],
                'expected_stderr_output': "No host b0gUs found in config.",
                }),
            ]

    def test_emits_expected_debug_message(self):
        """ Should emit expected debug message. """
        self.fake_options.debug = True
        with contextlib.suppress(FakeSystemExit):
            self.function_to_test(**self.test_args)
        if hasattr(self, 'expected_stdout_output'):
            self.assertIn(self.expected_stdout_output, sys.stdout.getvalue())
        if hasattr(self, 'expected_stderr_output'):
            self.assertIn(self.expected_stderr_output, sys.stderr.getvalue())

    def test_calls_sys_exit_when_host_not_in_config(self):
        """ Should call `sys.exit` when named host not in config. """
        if not hasattr(self, 'expected_exception'):
            self.skipTest("No expected exception for this scenario")
        with testtools.ExpectedException(self.expected_exception):
            self.function_to_test(**self.test_args)
        sys.exit.assert_called_with(self.expected_exit_status)


class main_check_upload_logfile_CallTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function calling `check_upload_logfile`. """

    scenarios = [
            ('default', {}),
            ('command-line-option-check-only', {
                'fake_options_mapping': {'check_only': True},
                'expected_check_only': True,
                }),
            ('command-line-option-lintian', {
                'fake_options_mapping': {'run_lintian': True},
                'expected_lintian': True,
                }),
            ('command-line-option-force', {
                'fake_options_mapping': {'force_upload': True},
                'expected_force_upload': True,
                }),
            ('command-line-options-three', {
                'fake_options_mapping': {
                    'force_upload': True,
                    'run_lintian': True,
                    'check_only': True,
                    },
                'expected_check_only': True,
                'expected_lintian': True,
                'expected_force_upload': True,
                }),
            ]

    def test_calls_check_upload_logfile_with_expected_args(self):
        """ Should invoke `check_upload_logfile` with expected args. """
        self.function_to_test(**self.test_args)
        expected_fqdn = dput.dput.get_fqdn_for_host.return_value
        expected_args = (
                self.changes_file_double.path,
                self.test_host, expected_fqdn,
                getattr(self, 'expected_check_only', False),
                getattr(self, 'expected_lintian', False),
                getattr(self, 'expected_force_upload', False),
                unittest.mock.ANY)
        dput.dput.check_upload_logfile.assert_called_with(*expected_args)


class main_verify_files_CallTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function calling `verify_files`. """

    scenarios = [
            ('default', {}),
            ('command-line-option-check-only', {
                'fake_options_mapping': {'check_only': True},
                'expected_check_only': True,
                }),
            ('command-line-option-check-version', {
                'fake_options_mapping': {'check_version': True},
                'expected_check_version': True,
                }),
            ('command-line-option-unchecked', {
                'fake_options_mapping': {'allow_unsigned_uploads': True},
                'expected_allow_unsigned_uploads': True,
                }),
            ('command-line-options-three', {
                'fake_options_mapping': {
                    'check_only': True,
                    'check_version': True,
                    'allow_unsigned_uploads': True,
                    },
                'expected_check_only': True,
                'expected_check_version': True,
                'expected_allow_unsigned_uploads': True,
                }),
            ]

    def test_calls_verify_files_with_expected_args(self):
        """ Should invoke `verify_files` with expected args. """
        self.function_to_test(**self.test_args)
        expected_args = (
                os.path.dirname(self.changes_file_double.path),
                os.path.basename(self.changes_file_double.path),
                self.test_host,
                self.runtime_config_parser,
                getattr(self, 'expected_check_only', False),
                getattr(self, 'expected_check_version', False),
                getattr(self, 'expected_allow_unsigned_uploads', False),
                unittest.mock.ANY)
        dput.dput.verify_files.assert_called_with(*expected_args)


class main_run_lintian_test_CallTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function calling `run_lintian_test`. """

    scenarios = [
            ('option-from-command-line', {
                'fake_options_mapping': {'run_lintian': True},
                }),
            ('option-from-config', {
                'config_run_lintian': True,
                }),
            ]

    def test_calls_run_lintian_test_with_expected_args(self):
        """ Should invoke `run_lintian_test` with expected args. """
        self.function_to_test(**self.test_args)
        expected_args = (self.changes_file_double.path,)
        dput.dput.run_lintian_test.assert_called_with(*expected_args)


class main_run_lintian_test_NoCallTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function omitting `run_lintian_test`. """

    def test_omits_run_lintian_test(self):
        """ Should omit `run_lintian_test` when not requested. """
        self.function_to_test(**self.test_args)
        self.assertFalse(dput.dput.run_lintian_test.called)


class main_UploadHookCommandsTestCase(main_TestCase):
    """ Test cases for `main` function upload hook commands. """

    def test_calls_execute_command_with_pre_upload_command(self):
        """ Should invoke `execute_command` when `pre_upload_command`. """
        test_command = self.getUniqueString()
        self.runtime_config_parser.set(
                self.test_host, 'pre_upload_command', test_command)
        self.function_to_test(**self.test_args)
        expected_command = test_command
        dput.dput.execute_command.assert_called_with(
                expected_command, "pre", unittest.mock.ANY)

    def test_calls_execute_command_with_post_upload_command(self):
        """ Should invoke `execute_command` when `post_upload_command`. """
        test_command = self.getUniqueString()
        self.runtime_config_parser.set(
                self.test_host, 'post_upload_command', test_command)
        self.function_to_test(**self.test_args)
        expected_command = test_command
        dput.dput.execute_command.assert_called_with(
                expected_command, "post", unittest.mock.ANY)


class main_UploadFilesTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function, call `upload_files`. """

    debug_scenarios = [
            ('debug-default', {
                'expected_kwarg_debug': False,
                }),
            ('debug-true', {
                'fake_option_debug': True,
                'expected_kwarg_debug': True,
                }),
            ]

    simulate_scenarios = [
            ('simulate-default', {
                'expected_kwarg_simulate': False,
                }),
            ('simulate-true', {
                'fake_option_simulate': True,
                'expected_kwarg_simulate': True,
                }),
            ]

    passive_scenarios = [
            ('passive-default', {
                'expected_kwarg_commandline_passive': None,
                }),
            ('passive-true', {
                'fake_option_commandline_passive': True,
                'expected_kwarg_commandline_passive': True,
                }),
            ]

    delay_scenarios = [
            ('delay-unspecified', {
                'determine_delay_days_return_value': None,
                'expected_kwarg_delay_days': None,
                }),
            ('delay-specified-none', {
                'determine_delay_days_return_value': None,
                'expected_kwarg_delay_days': None,
                }),
            ('delay-specified-integer', {
                'determine_delay_days_return_value': 555,
                'expected_kwarg_delay_days': 555,
                }),
            ]

    scenarios = testscenarios.multiply_scenarios(
            debug_scenarios, simulate_scenarios,
            passive_scenarios, delay_scenarios)

    def setUp(self):
        """ Set up fixtures for this test case. """
        super().setUp()

        self.set_expected_kwargs()

    def set_fake_options(self, options_mapping=None):
        """ Set the `fake_option_mapping` based on scenario attributes. """
        for option_name in [
                'debug',
                'simulate',
                'commandline_passive',
                ]:
            scenario_attribute_name = "fake_option_{}".format(option_name)
            if hasattr(self, scenario_attribute_name):
                fake_option_value = getattr(self, scenario_attribute_name)
                options_mapping[option_name] = fake_option_value
        super().set_fake_options(options_mapping)

    def set_expected_kwargs(self):
        """ Set the `expected_kwargs` based on scenario attributes. """
        self.expected_kwargs = {}
        for arg_name in [
                'debug',
                'simulate',
                'commandline_passive',
                'delay_days',
                ]:
            scenario_attribute_name = "expected_kwarg_{}".format(arg_name)
            if hasattr(self, scenario_attribute_name):
                expected_kwarg_value = getattr(self, scenario_attribute_name)
                self.expected_kwargs[arg_name] = expected_kwarg_value

    def test_calls_upload_files_with_expected_args(self):
        """ Should call `upload_files` function with expected args. """
        self.expected_args = (
                self.upload_methods,
                self.runtime_config_parser,
                self.test_host,
                self.files_to_upload,
                )
        self.function_to_test(**self.test_args)
        dput.dput.upload_files.assert_called_with(
                *self.expected_args, **self.expected_kwargs)


class main_UploadLogTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function, creating upload log file. """

    scenarios = [
            ('default', {
                'expect_call': True,
                }),
            ('command-line-disable-option', {
                'fake_options_mapping': {'upload_log': False},
                'expect_call': False,
                }),
            ('command-line-simulate-option', {
                'fake_options_mapping': {'simulate': True},
                'expect_call': False,
                }),
            ]

    def test_calls_create_upload_file_if_specified(self):
        """ Should call `create_upload_file` if specified. """
        self.function_to_test(**self.test_args)
        if self.expect_call:
            expected_args = (
                    os.path.basename(self.changes_file_double.path),
                    self.test_host, dput.dput.get_fqdn_for_host.return_value,
                    os.path.dirname(self.changes_file_double.path),
                    self.files_to_upload,
                    unittest.mock.ANY)
            dput.dput.create_upload_file.assert_called_with(*expected_args)
        else:
            self.assertFalse(dput.dput.create_upload_file.called)


class main_DinstallTestCase(
        testscenarios.WithScenarios,
        main_TestCase):
    """ Test cases for `main` function, invoking ‘dinstall’ command. """

    scenarios = [
            ('option-from-command-line', {
                'fake_options_mapping': {'run_dinstall': True},
                'expected_output': "D: run_dinstall: True",
                }),
            ('option-from-config', {
                'config_run_dinstall': True,
                'expected_output': "D: Host Config: True",
                }),
            ]

    def test_emits_debug_message_for_options(self):
        """ Should emit debug message for options. """
        self.fake_options.debug = True
        self.function_to_test(**self.test_args)
        self.assertIn(self.expected_output, sys.stdout.getvalue())

    def test_calls_dinstall_caller_with_expected_args(self):
        """ Should call `dinstall_caller` with expected args. """
        expected_args = (
                os.path.basename(self.changes_file_double.path),
                self.test_host, dput.dput.get_fqdn_for_host.return_value,
                unittest.mock.ANY, unittest.mock.ANY, unittest.mock.ANY)
        self.function_to_test(**self.test_args)
        dput.dput.dinstall_caller.assert_called_with(*expected_args)

    def test_emits_expected_information_message(self):
        """ Should emit expected information message. """
        self.fake_options.simulate = True
        self.function_to_test(**self.test_args)
        expected_stderr_output = (
                "Not running ‘dinstall’: ‘simulate’ option specified.")
        self.assertIn(expected_stderr_output, sys.stderr.getvalue())


# Copyright © 2015–2024 Ben Finney <bignose@debian.org>
#
# This is free software: you may copy, modify, and/or distribute this work
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 3 of that license or any later version.
# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.


# Local variables:
# coding: utf-8
# mode: python
# End:
# vim: fileencoding=utf-8 filetype=python :
