"""
A series of tests to establish that the command-line management tools work as
advertised - especially with regards to the handling of the
SOCON_SETTINGS_MODULE and default settings.py files.
"""
import os
import shutil
import subprocess
import sys

from argparse import Namespace
from pathlib import Path

import pytest

from socon import conf, get_version
from socon.core.management.base import Config


class AdminScriptTestCase:
    def run_test(self, args, test_dir, settings_file=None, projects=None):
        """Start a subprocess using either the manage.py or the socon module"""
        base_dir = os.path.dirname(test_dir)
        # The base dir for Socon's tests is one level up.
        testing_dir = os.path.dirname(os.path.dirname(__file__))
        # The base dir for Socon is one level above the test dir. We don't use
        # `import socon` to figure that out, so we don't pick up a Socon
        # from site-packages or similar.
        socon_dir = os.path.dirname(testing_dir)

        # Define a temporary environment for the subprocess
        test_environ = os.environ.copy()

        # Set the test environment
        if settings_file:
            test_environ["SOCON_SETTINGS_MODULE"] = settings_file
        elif "SOCON_SETTINGS_MODULE" in test_environ:
            del test_environ["SOCON_SETTINGS_MODULE"]
        python_path = [base_dir, socon_dir, testing_dir]
        test_environ["PYTHONPATH"] = os.pathsep.join(python_path)
        test_environ["PYTHONWARNINGS"] = ""

        p = subprocess.run(
            [sys.executable, *args],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            cwd=test_dir,
            env=test_environ,
            universal_newlines=True,
        )
        return p.stdout, p.stderr

    def run_socon_admin(self, args, test_dir, settings_file=None):
        """Start a subprocess using the socon module"""
        return self.run_test(["-m", "socon", *args], test_dir, settings_file)

    def write_settings(
        self, filename, test_dir, projects=None, is_dir=False, sdict=None, extra=None
    ):
        """Write a temporary settings to installed projects for example"""
        if is_dir:
            settings_dir = os.path.join(test_dir, filename)
            os.mkdir(settings_dir)
            settings_file_path = os.path.join(settings_dir, "__init__.py")
        else:
            settings_file_path = os.path.join(test_dir, filename)

        with open(settings_file_path, "w") as settings_file:
            settings_file.write(
                "# Settings file automatically generated by admin_scripts test case\n"
            )
            if extra:
                settings_file.write("%s\n" % extra)

            if projects is None:
                projects = ["admin_scripts"]

            settings_file.write("INSTALLED_PROJECTS = %s\n" % projects)

            if sdict:
                for k, v in sdict.items():
                    settings_file.write("%s = %s\n" % (k, v))

    def run_manage(self, args, test_dir, settings_file=None, manage_py=None):
        """Start a subprocess using the manage.py script"""
        template_manage_py = (
            os.path.join(os.path.dirname(__file__), manage_py)
            if manage_py
            else os.path.join(
                os.path.dirname(conf.__file__), "container_template", "manage.py-tpl"
            )
        )
        test_manage_py = os.path.join(test_dir, "manage.py")
        shutil.copyfile(template_manage_py, test_manage_py)

        with open(test_manage_py) as fp:
            manage_py_contents = fp.read()
        manage_py_contents = manage_py_contents.replace(
            "{{ container_name }}", "test_project"
        )
        with open(test_manage_py, "w") as fp:
            fp.write(manage_py_contents)

        return self.run_test(["./manage.py", *args], test_dir, settings_file)


# ---------------------------------------------------------------------------- #
#                                Settings Tests                                #
# ---------------------------------------------------------------------------- #


class NoSettingsTests(AdminScriptTestCase):
    "A series of tests for socon-admin when there is no settings.py file."

    def test_commands_with_invalid_settings(self, test_dir):
        """ "
        Commands that don't require settings succeed if the settings file
        doesn't exist.
        """
        args = ["createcontainer"]
        out, err = self.run_socon_admin(args, test_dir, settings_file="bad_settings")
        assert out == ""
        assert "You must provide a container name" in err


class DefaultSettingsTests(AdminScriptTestCase):
    """A series of tests for socon-admin when using a settings.py file that
    contains the test project.
    """

    @pytest.fixture(autouse=True)
    def setup(self, test_dir):
        self.write_settings("settings.py", test_dir)

    def test_custom_command_manage(self, test_dir):
        "default: manage.py can execute user commands when default settings are appropriate"
        args = ["noargs_command", "--project=admin_scripts"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "EXECUTE: noargs_command" in out

    def test_custom_command_admin(self, test_dir):
        "default: manage.py can execute user commands when default settings are appropriate"
        args = ["noargs_command"]
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert "No Socon settings specified" in err
        assert "Unknown command: 'noargs_command'" in err

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_settings(self, func, test_dir):
        "default: socon-admin can execute user commands if settings are provided as argument"
        args = [
            "noargs_command",
            "--settings=test_project.settings",
            "--project=admin_scripts",
        ]
        out, err = getattr(self, func)(args, test_dir)
        assert err == ""
        assert "EXECUTE: noargs_command" in out

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_environment(self, func, test_dir):
        "default: socon-admin can execute user commands if settings are provided in environment"
        args = ["noargs_command", "--project=admin_scripts"]
        out, err = getattr(self, func)(args, test_dir, "test_project.settings")
        assert err == ""
        assert "EXECUTE: noargs_command" in out


class MinimalSettingsTests(AdminScriptTestCase):
    """A series of tests for socon-admin when using a settings.py file that
    doesn't contain the test application.
    """

    @pytest.fixture(autouse=True)
    def setup(self, test_dir):
        self.write_settings("settings.py", test_dir, projects=[])

    def test_custom_command_admin(self, test_dir):
        "minimal: socon-admin can't execute user commands unless settings are provided"
        args = ["noargs_command"]
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert "No Socon settings specified" in err
        assert "Unknown command: 'noargs_command'" in err

    def test_custom_command_manage(self, test_dir):
        "minimal: manage.py can't execute user commands without appropriate settings"
        args = ["noargs_command"]
        out, err = self.run_manage(args, test_dir)
        assert out == ""
        assert "Unknown command: 'noargs_command'" in err

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_settings(self, func, test_dir):
        "minimal: socon-admin can't execute user commands, even if settings are provided as argument"
        args = ["noargs_command", "--settings=test_project.settings"]
        out, err = getattr(self, func)(args, test_dir)
        assert out == ""
        assert "Unknown command: 'noargs_command'" in err

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_environment(self, func, test_dir):
        "minimal: socon-admin can't execute user commands, even if settings are provided in environment"
        args = ["noargs_command"]
        out, err = getattr(self, func)(args, test_dir, "test_project.settings")
        assert out == ""
        assert "Unknown command: 'noargs_command'" in err


class AlternateSettingsTests(AdminScriptTestCase):
    """A series of tests for socon-admin when using a settings file
    with a name other than 'settings.py'.
    """

    @pytest.fixture(autouse=True)
    def setup(self, test_dir):
        self.write_settings("alternate_settings.py", test_dir)

    def test_custom_command_admin(self, test_dir):
        "alternate: socon-admin can't execute user commands unless settings are provided"
        args = ["noargs_command"]
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert "No Socon settings specified" in err
        assert "Unknown command: 'noargs_command'" in err

    def test_custom_command_manage(self, test_dir):
        "alternate: manage.py can't execute user commands without settings"
        args = ["noargs_command"]
        out, err = self.run_manage(args, test_dir)
        assert out == ""
        assert "No module named 'test_project.settings'" in err

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_settings(self, func, test_dir):
        "alternate: socon-admin can execute user commands if settings are provided as argument"
        args = [
            "noargs_command",
            "--settings=test_project.alternate_settings",
            "--project=admin_scripts",
        ]
        out, err = getattr(self, func)(args, test_dir)
        assert err == ""
        assert (
            "EXECUTE: noargs_command options=["
            "('settings', 'test_project.alternate_settings'), "
            "('traceback', False), ('verbosity', 1)]"
        ) in out

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_environment(self, func, test_dir):
        "alternate: socon-admin can execute user commands if settings are provided in environment"
        args = ["noargs_command", "--project=admin_scripts"]
        out, err = getattr(self, func)(
            args, test_dir, "test_project.alternate_settings"
        )
        assert err == ""
        assert (
            "EXECUTE: noargs_command options=[('settings', None), "
            "('traceback', False), ('verbosity', 1)]"
        ) in out


class MultipleSettingsTests(AdminScriptTestCase):
    """A series of tests for socon-admin when multiple settings files
    (including the default 'settings.py') are available. The default settings
    file is insufficient for performing the operations described, so the
    alternate settings must be used by the running script.
    """

    @pytest.fixture(autouse=True)
    def setup(self, test_dir):
        self.write_settings("settings.py", test_dir, projects=[])
        self.write_settings("alternate_settings.py", test_dir)

    def test_custom_command_admin(self, test_dir):
        "alternate: socon-admin can't execute user commands unless settings are provided"
        args = ["noargs_command"]
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert "No Socon settings specified" in err
        assert "Unknown command: 'noargs_command'" in err

    def test_custom_command_manage(self, test_dir):
        "alternate: socon-admin can't execute user commands unless settings are provided"
        args = ["noargs_command"]
        out, err = self.run_manage(args, test_dir)
        assert out == ""
        assert "Unknown command: 'noargs_command'" in err

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_settings(self, func, test_dir):
        "alternate: socon-admin can execute user commands if settings are provided as argument"
        args = [
            "noargs_command",
            "--settings=test_project.alternate_settings",
            "--project=admin_scripts",
        ]
        out, err = getattr(self, func)(args, test_dir)
        assert err == ""
        assert "EXECUTE: noargs_command" in out

    @pytest.mark.parametrize("func", ("run_manage", "run_socon_admin"))
    def test_custom_command_with_environment(self, func, test_dir):
        "alternate: socon-admin can execute user commands if settings are provided in environment"
        args = ["noargs_command", "--project=admin_scripts"]
        out, err = getattr(self, func)(
            args, test_dir, "test_project.alternate_settings"
        )
        assert err == ""
        assert "EXECUTE: noargs_command" in out


class ManageManuallyConfiguredSettingsTests(AdminScriptTestCase):
    """Customized manage.py calling settings.configure()."""

    def test_non_existent_command_output(self, test_dir):
        out, err = self.run_manage(
            ["invalid_command"], test_dir, manage_py="configured_settings_manage.py"
        )
        assert out == ""
        assert "Unknown command: 'invalid_command'" in err
        assert "No Socon settings specified" not in err


class CommandTypesTests(AdminScriptTestCase):
    @pytest.fixture(autouse=True)
    def setup(self, request, test_dir):
        if "noautofixt" in request.keywords:
            return
        self.write_settings("settings.py", test_dir)

    def test_version(self, test_dir):
        "version is handled as a special case"
        args = ["version"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert get_version() in out

    def test_version_alternative(self, test_dir):
        "--version is equivalent to version"
        args1, args2 = ["version"], ["--version"]
        # It's possible one outputs on stderr and the other on stdout, hence the set
        assert set(self.run_manage(args1, test_dir)) == set(
            self.run_manage(args2, test_dir)
        )

    @pytest.mark.parametrize("command", (["commandnameendwith", "commandwithoutname"]))
    def test_commands_name(self, test_dir, command):
        """The the automatic name attribution"""
        args = [command, "--project=admin_scripts"]
        out, _ = self.run_manage(args, test_dir)
        assert command in out

    def test_socon_core_commands_only(self, test_dir):
        """Socon show an error message when settings is not loaded correctly
        on help commands
        """
        args = ["help"]
        out, err = self.run_manage(args, test_dir, settings_file="Not exist")
        assert err == ""
        assert "createcontainer" in out
        assert (
            "Note that only Socon core commands are listed "
            "as settings are not properly configured"
        ) in out

    @pytest.mark.parametrize(
        "func, cmd_name",
        [("run_manage", "manage.py"), ("run_socon_admin", "python -m socon")],
    )
    @pytest.mark.parametrize("args", ([], ["help"]), ids=["no-args", "help"])
    def test_help(self, test_dir, args, func, cmd_name):
        "help is handled as a special case"
        out, err = getattr(self, func)(args, test_dir)
        assert err == ""
        assert (
            f"Type '{cmd_name} help <subcommand>' for help on a specific "
            "general (G) subcommand. If this command \n"
            "is a project (P) subcommand. Add --project <label> to the arguments."
        ) in out
        assert "createcontainer (G)" in out
        assert "createproject (G)" in out
        assert "createplugin (G)" in out

    def test_help_multiple_project(self, test_dir):
        """help when we have multiple projects"""
        self.write_settings(
            "settings.py",
            test_dir,
            projects=["admin_scripts", "admin_scripts.project_with_command"],
        )
        args = ["help"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "createcontainer (G)" in out
        assert "[project_with_command]" in out
        assert "[admin_scripts]" in out
        assert "simple_command (P)" in out

    def test_help_alternative(self, test_dir):
        "--help is equivalent to help"
        args1, args2 = ["help"], ["--help"]
        assert self.run_manage(args1, test_dir), self.run_manage(args2 == test_dir)

    def test_help_short_altert(self, test_dir):
        "-h is handled as a short form of --help"
        args1, args2 = ["--help"], ["-h"]
        assert self.run_manage(args1, test_dir), self.run_manage(args2 == test_dir)

    @pytest.mark.parametrize(
        "options", (["createcontainer", "--help"], ["help", "createcontainer"])
    )
    def test_specific_help(self, test_dir, options):
        "--help can be used on a specific command"
        out, err = self.run_manage(options, test_dir)
        assert err == ""
        # Command-specific options like --tag appear before options common to
        # all commands like --version.
        version_location = out.find("--version")
        assert version_location != -1
        assert (
            "Creates a Socon container directory structure for the given container name in"
            in out
        )
        assert "the current directory or optionally in the given directory." in out

    def test_ignore_command_error_for_default_options(self, test_dir):
        """IF we pass a wrong args for default socon options (not subcommand)
        it should not matter and the script should continue"""
        args = ["version", "--settings", "--nothing"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert get_version() in out

    @pytest.mark.noautofixt
    def test_show_unregistered_configs_in_help(self, test_dir):
        """Show unregistered projects in the help if projects are ready"""
        args = ["help"]
        project = "registry.default_config.projects.NotAConfig"
        self.write_settings("settings.py", test_dir, projects=[project])
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "Project loading error" in out
        assert project in out

    def test_base_command_handle_not_implemented(self, test_dir):
        """Check that every base command implement the handle method"""
        args = ["no_handle_base_cmd", "--project", "admin_scripts"]
        _, err = self.run_manage(args, test_dir)
        assert "NotImplementedError" in err

    def _test_base_command(
        self, args, test_dir, labels, option_a="'1'", option_b="'2'"
    ):
        out, err = self.run_manage(args, test_dir)

        expected_out = (
            "EXECUTE:BaseCommand labels=%s, "
            "options=[('option_a', %s), ('option_b', %s), ('option_c', '3'), "
            "('project', 'admin_scripts'), ('settings', None), "
            "('traceback', False), ('verbosity', 1)]"
        ) % (labels, option_a, option_b)
        assert err == ""
        assert expected_out in out

    def test_base_command(self, test_dir):
        "User BaseCommands can execute when a label is provided"
        args = ["base_command", "testlabel", "--project=admin_scripts"]
        expected_labels = "('testlabel',)"
        self._test_base_command(args, test_dir, expected_labels)

    def test_base_command_no_label(self, test_dir):
        "User BaseCommands can execute when no labels are provided"
        args = ["base_command", "--project=admin_scripts"]
        expected_labels = "()"
        self._test_base_command(args, test_dir, expected_labels)

    def test_base_command_multiple_label(self, test_dir):
        "User BaseCommands can execute when no labels are provided"
        args = ["base_command", "testlabel", "anotherlabel", "--project=admin_scripts"]
        expected_labels = "('testlabel', 'anotherlabel')"
        self._test_base_command(args, test_dir, expected_labels)

    def test_base_command_with_option(self, test_dir):
        "User BaseCommands can execute with options when a label is provided"
        args = ["base_command", "testlabel", "--option_a=x", "--project=admin_scripts"]
        expected_labels = "('testlabel',)"
        self._test_base_command(args, test_dir, expected_labels, option_a="'x'")

    def test_base_command_with_options(self, test_dir):
        "User BaseCommands can execute with multiple options when a label is provided"
        args = [
            "base_command",
            "testlabel",
            "-a",
            "x",
            "--option_b=y",
            "--project=admin_scripts",
        ]
        expected_labels = "('testlabel',)"
        self._test_base_command(
            args, test_dir, expected_labels, option_a="'x'", option_b="'y'"
        )

    def test_base_command_with_wrong_option(self, test_dir):
        "User BaseCommands outputs command usage when wrong option is specified"
        args = ["base_command", "--invalid", "--project=admin_scripts"]
        out, err = self.run_manage(args, test_dir)
        assert out == ""
        assert "usage: manage.py base_command" in err
        assert "error: unrecognized arguments: --invalid" in err

    def test_noargs(self, test_dir):
        "NoArg Commands can be executed"
        args = ["noargs_command", "--project=admin_scripts"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert (
            "EXECUTE: noargs_command options=[('settings', None), "
            "('traceback', False), ('verbosity', 1)]"
        ) in out

    def test_noargs_with_args(self, test_dir):
        "NoArg Commands raise an error if an argument is provided"
        args = ["noargs_command", "argument", "--project=admin_scripts"]
        _, err = self.run_manage(args, test_dir)
        assert "error: unrecognized arguments: argument" in err

    def test_keep_extras_args(self, test_dir):
        "Keep the extra args"
        args = ["extras_args_command", "argument", "--project=admin_scripts"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "['argument']" in out

    def test_print_output_on_command_string_return(self, test_dir):
        "Keep the extra args"
        args = ["return_output", "--project=admin_scripts"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "foo" in out

    def test_call_socon_main(self):
        """Dummy test for coverage. Nothing should be call if we don't
        run socon.__main__ directly"""
        import socon.__main__  # noqa

    def test_unknown_commands_on_missing_project_arg(self, test_dir):
        """Show list of projects that hold the commands asked. By default
        we look into the general commands. If we don't find it, we mark
        it as unknown. But the true is that this command can exist for
        a specific project"""
        args = ["custom_createcontainer"]
        out, err = self.run_manage(args, test_dir)
        assert out == ""
        assert (
            "Unknown command: 'custom_createcontainer'. You might have missed "
            "to specify the project. You must either define the environment variable "
            "SOCON_ACTIVE_PROJECT or add the --project argument to the command.\n"
            "You can find below the list of the projects available: "
        ) in err

    def test_get_command_project_not_exist(self, test_dir):
        """Get a command from a project that does not exist"""
        args = ["custom_createcontainer", "--project", "foo"]
        out, _ = self.run_manage(args, test_dir)
        assert out == ""
        assert (
            "You are looking for 'custom_createcontainer' command in  "
            "'foo' project that is not installed. Please "
            "check your INSTALLED_PROJECTS"
        )


class ConfigCommandTests(AdminScriptTestCase):
    """Test the config attribute that is present in every commands"""

    @pytest.fixture(scope="class")
    def config(self):
        yield Config(Namespace(a="a"), [])

    def test_get_config_option(self, config):
        """Get an option from the config"""
        assert config.getoption("a") == "a"
        msg = "No option named b"
        with pytest.raises(ValueError, match=msg):
            config.getoption("b", skip=False)
        assert config.getoption("c", default="a") == "a"
        assert config.options.a == "a"

    def test_get_config_tmp_directory(self, config):
        """Check that the config tmp directory exist"""
        assert config.tmpdir.exists()

    def test_access_to_terminal_from_config(self, config):
        """Confirm access to terminal"""
        config.terminal.write("foo")
        assert config.terminal._current_line == "foo"


class ProjectCommandTests(AdminScriptTestCase):
    "Tests for the various types of project command types that can be defined."

    @pytest.fixture(autouse=True)
    def setup(self, test_dir):
        self.write_settings("settings.py", test_dir)

    def test_project_command_in_helper(self, test_dir):
        """Check that the command is in the help"""
        out, err = self.run_manage(["help"], test_dir)
        assert err == ""
        assert (
            "Type 'manage.py help <subcommand>' for help on a specific "
            "general (G) subcommand. If this command \n"
            "is a project (P) subcommand. Add --project <label> to the arguments."
        ) in out

        assert "Projects commands" in out
        assert "simple_pc (P)" in out

    def test_project_command_project_config_access(self, test_dir):
        "Access project config from project command"
        args = ["simple_pc", "--project", "admin_scripts"]
        out, _ = self.run_manage(args, test_dir)
        assert out == "admin_scripts\n"

    @pytest.mark.parametrize(
        "traceback", (None, "--traceback"), ids=("no-traceback", "traceback")
    )
    def test_project_command_only_for_some_projects(self, test_dir, traceback):
        "Test the access to some ProjectCommand by specific projects"
        args = ["restricted_pc", "--project", "admin_scripts"]
        if traceback:
            args.append(traceback)
        _, err = self.run_manage(args, test_dir)
        assert (
            "CommandError: 'admin_scripts' project does not have access to this command.\n"
            "List of authorized projects:\n"
            ""
        ) in err

    def test_project_command_handle_not_implemented(self, test_dir):
        """Check that every project command implement the handle method"""
        args = ["no_handle_pc_cmd", "--project", "admin_scripts"]
        _, err = self.run_manage(args, test_dir)
        assert "NotImplementedError" in err


class MutlipleProjectsCommandTests(AdminScriptTestCase):
    @pytest.fixture(autouse=True)
    def setup(self, test_dir):
        self.write_settings(
            "settings.py",
            test_dir,
            projects=["admin_scripts", "admin_scripts.simple_project"],
        )

    def test_project_with_no_commands(self, test_dir):
        """Projects without commands should not appear in the helps"""
        args = ["help"]
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "simple_project" not in out


class CreateContainerTests(AdminScriptTestCase):
    def test_wrong_args(self, test_dir):
        "Make sure passing the wrong kinds of arguments outputs an error and prints usage"
        out, err = self.run_socon_admin(["createcontainer"], test_dir)
        assert out == ""
        assert "usage:" in err
        assert "You must provide a container name." in err

    def test_simple_container(self, test_dir):
        "Make sure the createcontainer management command creates a container"
        args = ["createcontainer", "testcontainer"]
        testcontainer_dir = Path(test_dir, "testcontainer")

        out, err = self.run_socon_admin(args, test_dir)
        assert err == ""
        assert testcontainer_dir.is_dir() is True

        # running again..
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert (
            "CommandError: 'testcontainer' conflicts with the name of an "
            "existing Python module and cannot be used as a container name. "
            "Please try another name."
        ) in err

    def test_create_container_with_verbosity(self, test_dir):
        """Create a container with verbose = 2"""
        args = ["createcontainer", "testcontainer", "--verbosity=2"]
        out, _ = self.run_socon_admin(args, test_dir)
        assert "Rendering container template files" in out
        assert "Creating " in out

    @pytest.mark.parametrize("bad_name", ["7testcontainer", "../testcontainer"])
    def test_invalid_container_name(self, test_dir, bad_name):
        "Make sure the createcontainer management command validates a container name"
        args = ["createcontainer", bad_name]
        testcontainer_dir = Path(test_dir, bad_name)

        _, err = self.run_socon_admin(args, test_dir)
        assert (
            "Error: '{}' is not a valid container name. Please make "
            "sure the name is a valid identifier.".format(bad_name)
        ) in err
        assert testcontainer_dir.is_dir() is False

    def test_importable_container_name(self, test_dir):
        """
        createcontainer validates that container name doesn't clash with existing
        Python modules.
        """
        bad_name = "os"
        args = ["createcontainer", bad_name]
        testcontainer_dir = Path(test_dir, bad_name)

        _, err = self.run_socon_admin(args, test_dir)
        assert (
            "CommandError: 'os' conflicts with the name of an existing "
            "Python module and cannot be used as a container name. Please try "
            "another name."
        ) in err
        assert testcontainer_dir.is_dir() is False

    def test_simple_container_different_directory(self, test_dir):
        "Make sure the createcontainer management command creates a container in a specific directory"
        args = ["createcontainer", "testcontainer", "--target", "othertestcontainer"]
        testcontainer_dir = Path(test_dir, "othertestcontainer")
        os.mkdir(testcontainer_dir)

        out, err = self.run_socon_admin(args, test_dir)
        assert err == ""
        assert Path(testcontainer_dir, "manage.py").exists() is True

        # running again..
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert (
            "already exists. Overlaying a container into an existing directory "
            "won't replace conflicting files."
        ) in err

    def test_container_in_target_that_does_not_exist(self, test_dir):
        args = ["createcontainer", "testcontainer", "--target", "othertestcontainer"]
        testcontainer_dir = Path(test_dir, "othertestcontainer")

        out, err = self.run_socon_admin(args, test_dir)
        assert err == ""
        assert (
            f"Destination directory '{testcontainer_dir}' does not "
            "exist, we will create it for you."
        ) in out
        assert Path(testcontainer_dir, "manage.py").exists() is True

    def test_container_template_context_variables(self, test_dir):
        "Make sure template context variables are rendered with proper values"
        args = ["createcontainer", "another_project", "--target", "project_dir"]
        testproject_dir = Path(test_dir, "project_dir")
        os.mkdir(testproject_dir)
        _, err = self.run_socon_admin(args, test_dir)
        assert err == ""
        test_manage_py = Path(testproject_dir, "manage.py")
        with open(test_manage_py) as fp:
            content = fp.read()
            assert "another_project.settings" in content


class CreateProjectTests(AdminScriptTestCase):
    def _create_container(self, test_dir):
        args = ["createcontainer", "testcontainer"]
        testcontainer_dir = Path(test_dir, "testcontainer")
        self.run_socon_admin(args, test_dir)
        return testcontainer_dir

    @pytest.mark.parametrize("bad_name", ["7testcontainer", "../testcontainer"])
    def test_invalid_container_name(self, test_dir, bad_name):
        "Make sure the createproject management command validates a project name"
        args = ["createproject", bad_name]
        testproject_dir = Path(test_dir, bad_name)

        _, err = self.run_socon_admin(args, test_dir)
        assert (
            "Error: '{}' is not a valid project name. Please make "
            "sure the name is a valid identifier.".format(bad_name)
        ) in err
        assert testproject_dir.is_dir() is False

    @pytest.mark.parametrize(
        "params, name_or_dir",
        (
            (["createproject", "os"], "name"),
            (["createproject", "project", "--target"], "directory"),
        ),
    )
    def test_importable_container_name_or_dir(self, test_dir, params, name_or_dir):
        """
        createproject validates that project name doesn't clash with existing
        Python modules.
        """
        testproject_dir = Path(test_dir, "os")
        container = self._create_container(test_dir)
        if name_or_dir == "directory":
            params.append(container.joinpath("projects/os"))
        _, err = self.run_socon_admin(params, test_dir)
        assert (
            "CommandError: 'os' conflicts with the name of an existing "
            "Python module and cannot be used as a project {}. Please try "
            "another {}.".format(name_or_dir, name_or_dir)
        ) in err
        assert testproject_dir.is_dir() is False

    @pytest.mark.parametrize(
        "bad_target", ["invalid.dir_name", "7invalid_dir_name", ".invalid_dir_name"]
    )
    def test_invalid_target_name(self, bad_target, test_dir):
        """Check that the target is a valid name"""
        container = self._create_container(test_dir)
        target = container.joinpath(f"projects/{bad_target}")
        _, err = self.run_socon_admin(
            ["createproject", "project", "--target", target], test_dir
        )
        assert (
            "CommandError: '{}' is not a valid project directory. Please "
            "make sure the directory is a valid identifier.".format(bad_target)
        ) in err

    def test_create_project_only_in_container(self, test_dir):
        """Create project command sould only work at the root of a container"""
        # Check that we cannot create the container
        _, err = self.run_socon_admin(["createproject", "project"], test_dir)
        assert err == (
            "CommandError: Can only create project at the root of a container"
        )

        # Create a container and re-create the project
        testcontainer_dir = self._create_container(test_dir)

        args = ["createproject", "project"]
        out, err = self.run_socon_admin(args, testcontainer_dir)
        assert err == out == ""
        assert Path(testcontainer_dir, "projects/project").exists() is True

        # Rerun again... project directory already exist
        out, err = self.run_socon_admin(args, testcontainer_dir)
        assert "already exists" in err

    def test_create_project_with_invalid_target(self, test_dir):
        """Target should point to a container"""
        _, err = self.run_socon_admin(
            ["createproject", "project", "--target", "unknown"], test_dir
        )
        assert (
            "Target should be either the root of a container, "
            "or a directory that exist or not in the projects container "
            "directory."
        ) in err

    @pytest.mark.parametrize(
        "target",
        ("", "projects", "projects/project"),
        ids=["root", "projects-root", "new-project"],
    )
    def test_create_project_with_valid_target(self, test_dir, target):
        """Check that the taget point to a valid target. (A container)"""
        container = self._create_container(test_dir)

        # target point to the root container
        target_dir = container.joinpath(target)
        args = ["createproject", "project", "--target", target_dir]
        _, err = self.run_socon_admin(args, test_dir)
        assert err == ""
        assert Path(container, "projects/project").exists() is True

        # running again...
        _, err = self.run_socon_admin(args, test_dir)
        assert (
            "already exists. Overlaying a project into an existing directory "
            "won't replace conflicting files."
        ) in err


class CreatePluginTests(AdminScriptTestCase):
    """Should be same tests as container as it act the same way. So we won't
    write all the tests"""

    def test_wrong_args(self, test_dir):
        "Make sure passing the wrong kinds of arguments outputs an error and prints usage"
        out, err = self.run_socon_admin(["createplugin"], test_dir)
        assert out == ""
        assert "usage:" in err
        assert "You must provide a plugin name." in err

    def test_simple_plugin(self, test_dir):
        "Make sure the createcontainer management command creates a container"
        args = ["createplugin", "testplugin"]
        plugin_dir = Path(test_dir, "testplugin")

        out, err = self.run_socon_admin(args, test_dir)
        assert err == ""
        assert plugin_dir.is_dir() is True

        # running again..
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert (
            "CommandError: 'testplugin' conflicts with the name of an "
            "existing Python module and cannot be used as a plugin name. "
            "Please try another name."
        ) in err


class ManagerCheckTests(AdminScriptTestCase):
    def test_settings_not_configugure(self, test_dir):
        """Check output when settings are not configured"""
        args = ["check"]
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert "CommandError: Settings are not configured" in err

    def test_check_no_issue(self, test_dir):
        """Check message when there is no issue"""
        args = ["check"]
        self.write_settings(
            "settings.py",
            test_dir,
            projects=["admin_scripts", "admin_scripts.simple_project"],
        )
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "Nothing to report. All projects loaded successfully." in out
        assert "Nothing to report. All managers loaded successfully." in out

    def test_broken_project(self, test_dir):
        """Check ImportError when project is broken"""
        args = ["check"]
        self.write_settings(
            "settings.py",
            test_dir,
            projects=["admin_scripts", "admin_scripts.project_with_error"],
        )
        out, err = self.run_manage(args, test_dir)
        assert "admin_scripts.project_with_error" in out
        assert "ImportError: Test import error" in err

    def test_broken_manager(self, test_dir):
        """Check broken manager in a project"""
        args = ["check"]
        self.write_settings(
            "settings.py",
            test_dir,
            projects=["admin_scripts", "admin_scripts.project_with_manager"],
        )
        out, err = self.run_manage(args, test_dir)
        assert "admin_scripts.project_with_manager" in out
        assert "ImportError: Test manager import error" in err

    def test_show_all_issues(self, test_dir):
        """Show all issues at once"""
        args = ["check", "--show_all"]
        self.write_settings(
            "settings.py",
            test_dir,
            projects=[
                "admin_scripts",
                "admin_scripts.project_with_error",
                "admin_scripts.project_with_manager",
            ],
        )
        out, err = self.run_manage(args, test_dir)
        assert err == ""
        assert "admin_scripts.project_with_error" in out
        assert "ImportError: Test import error" in out
        assert "ImportError: Test manager import error" in out

    def test_skip_error_on_project_import_is_false(self, test_dir):
        """
        Check that we still check manager if all projects load correctly.
        If SKIP_ERROR_ON_PROJECTS_IMPORT is False, the command won't even
        start because Socon will raise an error before
        """
        args = ["check"]
        self.write_settings(
            "settings.py",
            test_dir,
            projects=["admin_scripts", "admin_scripts.project_with_manager"],
            sdict={"SKIP_ERROR_ON_PROJECTS_IMPORT": False},
        )
        out, err = self.run_manage(args, test_dir)
        assert "admin_scripts.project_with_manager" in out
        assert "ImportError: Test manager import error" in err


class MainModuleTests(AdminScriptTestCase):
    """python -m socon works like socon admin."""

    def test_program_name_in_help(self, test_dir):
        out, _ = self.run_test(["-m", "socon", "help"], test_dir)
        assert (
            "Type 'python -m socon help <subcommand>' for help on a specific "
            "general (G) subcommand. If this command \n"
            "is a project (P) subcommand. Add --project <label> to the arguments."
        ) in out


class SuggestionsTests(AdminScriptTestCase):
    def test_suggestions(self, test_dir):
        args = ["createcontiner", "connectivity"]
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert "Unknown command: 'createcontiner'. Did you mean createcontainer?" in err

    def test_no_suggestions(self, test_dir):
        args = ["abcdef"]
        out, err = self.run_socon_admin(args, test_dir)
        assert out == ""
        assert "Did you mean" not in err
