import os
import tempfile

from pathlib import Path
from unittest import TestCase, skipIf
from unittest.mock import MagicMock, patch

import tomlkit

from samcli.commands.exceptions import ConfigException
from samcli.cli.cli_config_file import ConfigProvider, configuration_option, configuration_callback, get_ctx_defaults
from samcli.lib.config.exceptions import SamConfigFileReadException, SamConfigVersionException
from samcli.lib.config.samconfig import DEFAULT_ENV

from tests.testing_utils import IS_WINDOWS


class MockContext:
    def __init__(self, info_name, parent, params=None, command=None, default_map=None):
        self.info_name = info_name
        self.parent = parent
        self.params = params
        self.command = command
        self.default_map = default_map


class TestConfigProvider(TestCase):
    def setUp(self):
        self.config_provider = ConfigProvider()
        self.config_env = "config_env"
        self.parameters = "parameters"
        self.cmd_name = "topic"

    def test_toml_valid_with_section(self):
        config_dir = tempfile.gettempdir()
        config_path = Path(config_dir, "samconfig.toml")
        config_path.write_text("version=0.1\n[config_env.topic.parameters]\nword='clarity'\n")
        self.assertEqual(
            ConfigProvider(section=self.parameters)(config_path, self.config_env, [self.cmd_name]), {"word": "clarity"}
        )

    def test_toml_valid_with_no_version(self):
        config_dir = tempfile.gettempdir()
        config_path = Path(config_dir, "samconfig.toml")
        config_path.write_text("[config_env.topic.parameters]\nword='clarity'\n")
        with self.assertRaises(SamConfigVersionException):
            ConfigProvider(section=self.parameters)(config_path, self.config_env, [self.cmd_name])

    def test_toml_valid_with_invalid_version(self):
        config_dir = tempfile.gettempdir()
        config_path = Path(config_dir, "samconfig.toml")
        config_path.write_text("version='abc'\n[config_env.topic.parameters]\nword='clarity'\n")
        with self.assertRaises(SamConfigVersionException):
            ConfigProvider(section=self.parameters)(config_path, self.config_env, [self.cmd_name])

    def test_toml_invalid_empty_dict(self):
        config_dir = tempfile.gettempdir()
        config_path = Path(config_dir, "samconfig.toml")
        config_path.write_text("[topic]\nword=clarity\n")

        with self.assertRaises(SamConfigFileReadException):
            self.config_provider(config_path, self.config_env, [self.cmd_name])

    def test_toml_invalid_file_name(self):
        config_dir = tempfile.gettempdir()
        config_path = Path(config_dir, "mysamconfig.toml")
        config_path.write_text("version=0.1\n[config_env.topic.parameters]\nword='clarity'\n")
        config_path_invalid = Path(config_dir, "samconfig.toml")

        with self.assertRaises(SamConfigFileReadException):
            self.config_provider(config_path_invalid, self.config_env, [self.cmd_name])

    def test_toml_invalid_syntax(self):
        config_dir = tempfile.gettempdir()
        config_path = Path(config_dir, "samconfig.toml")
        config_path.write_text("version=0.1\n[config_env.topic.parameters]\nword=_clarity'\n")

        with self.assertRaises(SamConfigFileReadException):
            self.config_provider(config_path, self.config_env, [self.cmd_name])


class TestCliConfiguration(TestCase):
    def setUp(self):
        self.cmd_name = "test_cmd"
        self.option_name = "test_option"
        # No matter what config-env is passed in, default is chosen.
        self.config_env = "test_config_env"
        self.saved_callback = MagicMock()
        self.provider = MagicMock()
        self.ctx = MagicMock()
        self.param = MagicMock()
        self.value = MagicMock()
        self.config_file = "otherconfig.toml"
        self.config_file_pipe = "config"

    class Dummy:
        pass

    def test_callback_with_valid_config_env(self):
        mock_context1 = MockContext(info_name="sam", parent=None)
        mock_context2 = MockContext(info_name="local", parent=mock_context1)
        mock_context3 = MockContext(info_name="start-api", parent=mock_context2)
        self.ctx.parent = mock_context3
        self.ctx.info_name = "test_info"
        self.ctx.params = {}
        setattr(self.ctx, "samconfig_dir", None)
        configuration_callback(
            cmd_name=self.cmd_name,
            option_name=self.option_name,
            saved_callback=self.saved_callback,
            provider=self.provider,
            ctx=self.ctx,
            param=self.param,
            value=self.value,
        )
        self.assertEqual(self.saved_callback.call_count, 1)
        for arg in [self.ctx, self.param, DEFAULT_ENV]:
            self.assertIn(arg, self.saved_callback.call_args[0])
        self.assertNotIn(self.value, self.saved_callback.call_args[0])

    def test_callback_with_invalid_config_file(self):
        mock_context1 = MockContext(info_name="sam", parent=None)
        mock_context2 = MockContext(info_name="local", parent=mock_context1)
        mock_context3 = MockContext(info_name="start-api", parent=mock_context2)
        self.ctx.parent = mock_context3
        self.ctx.info_name = "test_info"
        self.ctx.params = {"config_file": "invalid_config_file"}
        self.ctx._parameter_source.__get__ = "COMMANDLINE"
        setattr(self.ctx, "samconfig_dir", None)
        with self.assertRaises(ConfigException):
            configuration_callback(
                cmd_name=self.cmd_name,
                option_name=self.option_name,
                saved_callback=self.saved_callback,
                provider=self.provider,
                ctx=self.ctx,
                param=self.param,
                value=self.value,
            )

    def test_callback_with_valid_config_file_path(self):
        mock_context1 = MockContext(info_name="sam", parent=None)
        mock_context2 = MockContext(info_name="local", parent=mock_context1)
        mock_context3 = MockContext(info_name="start-api", parent=mock_context2)
        self.ctx.parent = mock_context3
        self.ctx.info_name = "test_info"
        # Create a temporary directory.
        temp_dir = tempfile.mkdtemp()
        # Create a new config file path that is one layer above the temporary directory.
        config_file_path = Path(temp_dir).parent.joinpath(self.config_file)
        with open(config_file_path, "wb"):
            # Set the `samconfig_dir` to be temporary directory that was created.
            setattr(self.ctx, "samconfig_dir", temp_dir)
            # set a relative path for the config file from `samconfig_dir`.
            self.ctx.params = {"config_file": os.path.join("..", self.config_file)}
            configuration_callback(
                cmd_name=self.cmd_name,
                option_name=self.option_name,
                saved_callback=self.saved_callback,
                provider=self.provider,
                ctx=self.ctx,
                param=self.param,
                value=self.value,
            )
        self.assertEqual(self.saved_callback.call_count, 1)
        for arg in [self.ctx, self.param, DEFAULT_ENV]:
            self.assertIn(arg, self.saved_callback.call_args[0])
        self.assertNotIn(self.value, self.saved_callback.call_args[0])

    @skipIf(IS_WINDOWS, "os.mkfifo doesn't exist on windows")
    def test_callback_with_config_file_from_pipe(self):
        mock_context1 = MockContext(info_name="sam", parent=None)
        mock_context2 = MockContext(info_name="local", parent=mock_context1)
        mock_context3 = MockContext(info_name="start-api", parent=mock_context2)
        self.ctx.parent = mock_context3
        self.ctx.info_name = "test_info"
        # Create a temporary directory.
        temp_dir = tempfile.mkdtemp()
        # Create a new config file path that is one layer above the temporary directory.
        config_file_pipe_path = Path(temp_dir).parent.joinpath(self.config_file_pipe)
        try:
            # Create a new pipe
            os.mkfifo(config_file_pipe_path)
            # Set the `samconfig_dir` to be temporary directory that was created.
            setattr(self.ctx, "samconfig_dir", temp_dir)
            # set a relative path for the config file from `samconfig_dir`.
            self.ctx.params = {"config_file": os.path.join("..", self.config_file_pipe)}
            configuration_callback(
                cmd_name=self.cmd_name,
                option_name=self.option_name,
                saved_callback=self.saved_callback,
                provider=self.provider,
                ctx=self.ctx,
                param=self.param,
                value=self.value,
            )
        finally:
            os.remove(config_file_pipe_path)
        self.assertEqual(self.saved_callback.call_count, 1)
        for arg in [self.ctx, self.param, DEFAULT_ENV]:
            self.assertIn(arg, self.saved_callback.call_args[0])
        self.assertNotIn(self.value, self.saved_callback.call_args[0])

    def test_configuration_option(self):
        config_provider = ConfigProvider()
        click_option = configuration_option(provider=config_provider)
        clc = click_option(self.Dummy())
        self.assertEqual(clc.__click_params__[0].is_eager, True)
        self.assertEqual(
            clc.__click_params__[0].help,
            "This is a hidden click option whose callback function loads configuration parameters.",
        )
        self.assertEqual(clc.__click_params__[0].hidden, True)
        self.assertEqual(clc.__click_params__[0].expose_value, False)
        self.assertEqual(clc.__click_params__[0].callback.args, (None, None, None, config_provider))

    def test_get_ctx_defaults_non_nested(self):
        provider = MagicMock()

        mock_context1 = MockContext(info_name="sam", parent=None)
        mock_context2 = MockContext(info_name="local", parent=mock_context1)
        mock_context3 = MockContext(info_name="start-api", parent=mock_context2)

        get_ctx_defaults("start-api", provider, mock_context3, "default")

        provider.assert_called_with(None, "default", ["local", "start-api"])

    def test_get_ctx_defaults_nested(self):
        provider = MagicMock()

        mock_context1 = MockContext(info_name="sam", parent=None)
        mock_context2 = MockContext(info_name="local", parent=mock_context1)
        mock_context3 = MockContext(info_name="generate-event", parent=mock_context2)
        mock_context4 = MockContext(info_name="alexa-skills-kit", parent=mock_context3)

        get_ctx_defaults("intent-answer", provider, mock_context4, "default")

        provider.assert_called_with(None, "default", ["local", "generate-event", "alexa-skills-kit", "intent-answer"])