import os
import shutil
import unittest
from pathlib import Path

import yaml

import mock
from taskcat._cfn_lint import Lint
from taskcat._config import Config


class MockClientConfig(object):
    def __init__(self):
        self.region_name = "us-east-2"


class MockClient(object):
    def __init__(self):
        self._client_config = MockClientConfig()


test_two_path = str(
    Path(
        "/tmp/lint_test/test-config-two/templates/taskcat_test_" "template_test1"
    ).resolve()
)
test_cases = [
    {
        "config": {
            "project": {"name": "test-config", "regions": ["eu-west-1"]},
            "tests": {"test1": {}},
        },
        "templates": {"test1": """{"Resources": {}}"""},
        "expected_lints": {
            "test1": {
                "regions": ["eu-west-1"],
                "template": Path(
                    "/tmp/lint_test/test-config/templates/taskcat_test_template_test1"
                ).resolve(),
                "results": {
                    str(
                        Path(
                            "/tmp/lint_test/test-config/templates/taskcat_test_template"
                            "_test1"
                        ).resolve()
                    ): []
                },
            }
        },
    },
    {
        "config": {
            "project": {"name": "test-config-two", "regions": ["eu-west-1"]},
            "tests": {"test1": {}},
        },
        "templates": {
            "test1": """{"Resources": {"Name": {"Type":"AWS::Not::Exist"}}}"""
        },
        "expected_lints": {
            "test1": {
                "regions": ["eu-west-1"],
                "results": {
                    str(
                        Path(
                            "/tmp/lint_test/test-config-two/templates/taskcat_test_"
                            "template_test1"
                        ).resolve()
                    ): [
                        f"[E3001: Basic CloudFormation Resource Check] (Invalid or "
                        f"unsupported Type AWS::Not::Exist for resource Name in "
                        f"eu-west-1) matched "
                        f"{test_two_path}:1"
                    ]
                },
                "template": Path(
                    "/tmp/lint_test/test-config-two/templates/taskcat_test_template"
                    "_test1"
                ).resolve(),
            }
        },
    },
    {
        "config": {
            "project": {"name": "test-config-three", "regions": ["eu-west-1"]},
            "tests": {"test1": {}},
        },
        "templates": {
            "test1": '{"Resources": {"Name": {"Type":"AWS::CloudFormation::Stack",'
            '"Properties":{"TemplateURL": "broken"}}}}'
        },
        "expected_lints": {
            "test1": {
                "regions": ["eu-west-1"],
                "template": Path(
                    "/tmp/lint_test/test-config-three/templates"
                    "/taskcat_test_template_test1"
                ).resolve(),
                "results": {
                    str(
                        Path(
                            "/tmp/lint_test/test-config-three/templates"
                            "/taskcat_test_template_test1"
                        ).resolve()
                    ): []
                },
            }
        },
    },
]


def mkdir(path, ignore_exists=True):
    os.makedirs(path, exist_ok=ignore_exists)


def flatten_rule(lints):
    for test in lints.keys():
        for result in lints[test]["results"].keys():
            for i in range(0, len(lints[test]["results"][result])):
                lints[test]["results"][result][i] = str(
                    lints[test]["results"][result][i]
                )
    return lints


def build_test_case(base_path, test_case):
    qs_path = base_path + test_case["config"]["project"]["name"] + "/"
    mkdir(qs_path)
    mkdir(qs_path + "ci")
    mkdir(qs_path + "templates")
    os.chdir(qs_path)
    template_path = "taskcat_test_template"
    config_path = "./ci/taskcat_test_config"
    for test in test_case["config"]["tests"].keys():
        template_file = template_path + "_" + test
        test_case["config"]["tests"][test]["template"] = "templates/" + template_file
        with open("./templates/" + template_file, "w") as f:
            f.write(test_case["templates"][test])
    with open(config_path, "w") as f:
        f.write(yaml.safe_dump(test_case["config"]))
    return config_path


class TestCfnLint(unittest.TestCase):
    @mock.patch("taskcat._client_factory.Boto3Cache", autospec=True)
    def test_lint(self, m_boto):
        cwd = os.getcwd()
        base_path = "/tmp/lint_test/"
        mkdir(base_path)
        try:
            for test_case in test_cases:
                config_path = Path(build_test_case(base_path, test_case)).resolve()
                project_root = config_path.parent.parent
                config = Config.create(
                    project_config_path=config_path, project_root=project_root
                )
                templates = config.get_templates()
                lint = Lint(config=config, templates=templates)
                self.assertEqual(
                    test_case["expected_lints"], flatten_rule(lint.lints[0])
                )
        finally:
            shutil.rmtree(base_path)
            os.chdir(cwd)
            pass

    def test_filter_unsupported_regions(self):
        regions = ["us-east-1", "us-east-2", "eu-central-1"]
        supported = Lint._filter_unsupported_regions(regions)
        self.assertCountEqual(supported, regions)
        supported = Lint._filter_unsupported_regions(regions + ["non-exist-1"])
        self.assertCountEqual(supported, regions)

    @mock.patch("taskcat._cfn_lint.LOG.info")
    @mock.patch("taskcat._cfn_lint.LOG.warning")
    @mock.patch("taskcat._cfn_lint.LOG.error")
    @mock.patch("taskcat._client_factory.Boto3Cache", autospec=True)
    def test_output_results(
        self, m_boto, mock_log_error, mock_log_warning, mock_log_info
    ):
        cwd = os.getcwd()
        try:
            config_path = Path(
                build_test_case("/tmp/lint_test_output/", test_cases[0])
            ).resolve()
            project_root = config_path.parent.parent
            config = Config.create(
                project_config_path=config_path, project_root=project_root
            )
            templates = config.get_templates()
            lint = Lint(config=config, templates=templates)
            lint.output_results()
            self.assertTrue(
                mock_log_info.call_args[0][0].startswith(
                    f"Linting passed for file: {str(templates['test1'].template_path)}"
                )
            )
            self.assertEqual(mock_log_error.called, False)
            self.assertEqual(mock_log_warning.called, False)

            mock_log_info.reset_mock()
            lint_key = list(lint.lints[0])[0]
            result_key = list(lint.lints[0][lint_key]["results"])[0]
            test = lint.lints[0][lint_key]["results"][result_key]
            rule = mock.Mock(return_val="[W0001] some warning")
            rule.rule.id = "W0001"
            rule.linenumber = 123
            rule.rule.shortdesc = "short warning"
            rule.message = "some warning"
            test.append(rule)
            lint.output_results()
            self.assertTrue(
                mock_log_warning.call_args_list[1][0][0].startswith(
                    f"Linting detected issues in: {str(templates['test1'].template_path)}"
                )
            )
            mock_log_warning.assert_has_calls(
                [mock.call("    line 123 [0001] [short warning] some warning")]
            )
            self.assertEqual(mock_log_info.called, False)
            self.assertEqual(mock_log_error.called, False)

            mock_log_warning.reset_mock()
            test.pop(0)
            rule = mock.Mock(return_val="[E0001] some error")
            rule.rule.id = "E0001"
            rule.linenumber = 123
            rule.rule.shortdesc = "short error"
            rule.message = "some error"
            test.append(rule)
            lint.output_results()
            self.assertTrue(
                mock_log_warning.call_args[0][0].startswith(
                    f"Linting detected issues in: {str(templates['test1'].template_path)}"
                )
            )
            mock_log_error.assert_called_once_with(
                "    line 123 [0001] [short error] some error"
            )
            self.assertEqual(mock_log_info.called, False)
        finally:
            shutil.rmtree("/tmp/lint_test_output/")
            os.chdir(cwd)
            pass

    @mock.patch("taskcat._client_factory.Boto3Cache", autospec=True)
    def test_passed(self, m_boto):
        cwd = os.getcwd()
        try:
            config_path = Path(
                build_test_case("/tmp/lint_test_output/", test_cases[0])
            ).resolve()
            project_root = config_path.parent.parent
            config = Config.create(
                project_config_path=config_path, project_root=project_root
            )
            templates = config.get_templates()
            lint = Lint(config=config, templates=templates)
            self.assertEqual(lint.passed, True)

            lint_key = list(lint.lints[0])[0]
            result_key = list(lint.lints[0][lint_key]["results"])[0]
            test = lint.lints[0][lint_key]["results"][result_key]
            rule = mock.Mock(return_val="[E0001] some error")
            rule.rule.id = "E0001"
            rule.linenumber = 123
            rule.rule.shortdesc = "short error"
            rule.message = "some error"
            test.append(rule)
            lint.strict = True
            self.assertEqual(lint.passed, False)
        finally:
            shutil.rmtree("/tmp/lint_test_output/")
            os.chdir(cwd)
            pass
