# 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 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_fake_distribution,
        make_options_namespace,
        make_unique_slug,
        patch_importlib_metadata_distribution,
        patch_os_environ,
        patch_os_getuid,
        patch_os_isatty,
        patch_pwd_getpwuid,
        patch_sys_argv,
        patch_system_interfaces,
        )
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_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,
        )


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)


def patch_distribution(testcase):
    """ Patch the “dput” installed distribution for the `testcase`. """
    testcase.fake_distribution = make_fake_distribution(
            name="dput",
            version=getattr(testcase, 'dput_version', None),
    )
    patch_importlib_metadata_distribution(
            testcase, fake_distribution=testcase.fake_distribution)


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()
        patch_distribution(self)
        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 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()
        patch_distribution(self)

        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 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)
        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-force', {
                'fake_options_mapping': {'force_upload': True},
                'expected_force_upload': True,
                }),
            ('command-line-options-two', {
                'fake_options_mapping': {
                    'force_upload': True,
                    'check_only': True,
                    },
                'expected_check_only': 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_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 :
