from unittest import TestCase
from unittest.mock import patch

from pathlib import Path
from cookiecutter.exceptions import CookiecutterException, RepositoryNotFound
from parameterized import parameterized

from samcli.lib.init import generate_project, InvalidLocationError, _create_default_samconfig
from samcli.lib.init import GenerateProjectFailedError
from samcli.lib.init import RUNTIME_DEP_TEMPLATE_MAPPING
from samcli.lib.utils.packagetype import ZIP


class TestInit(TestCase):
    def setUp(self):
        self.location = None
        self.runtime = "python3.9"
        self.dependency_manager = "pip"
        self.output_dir = "mydir"
        self.name = "testing project"
        self.no_input = True
        self.extra_context = {"project_name": "testing project", "runtime": self.runtime}
        self.template = RUNTIME_DEP_TEMPLATE_MAPPING["python"][0]["init_location"]

    @patch("samcli.lib.init.cookiecutter")
    @patch("samcli.lib.init._create_default_samconfig")
    def test_init_successful(self, default_samconfig_mock, cookiecutter_patch):
        # GIVEN generate_project successfully created a project
        # WHEN a project name has been passed
        generate_project(
            location=self.location,
            runtime=self.runtime,
            package_type=ZIP,
            dependency_manager=self.dependency_manager,
            output_dir=self.output_dir,
            name=self.name,
            no_input=self.no_input,
        )

        # THEN we should receive no errors
        cookiecutter_patch.assert_called_once_with(
            no_input=self.no_input, output_dir=self.output_dir, template=self.template
        )

    @patch("samcli.lib.init.cookiecutter")
    @patch("samcli.lib.init._create_default_samconfig")
    def test_init_successful_with_no_dep_manager(self, default_samconfig_mock, cookiecutter_patch):
        generate_project(
            location=self.location,
            runtime=self.runtime,
            package_type=ZIP,
            dependency_manager=None,
            output_dir=self.output_dir,
            name=self.name,
            no_input=self.no_input,
        )

        # THEN we should receive no errors
        cookiecutter_patch.assert_called_once_with(
            no_input=self.no_input, output_dir=self.output_dir, template=self.template
        )

    @patch("samcli.lib.init._create_default_samconfig")
    def test_init_error_with_non_compatible_dependency_manager(self, default_samconfig_mock):
        with self.assertRaises(GenerateProjectFailedError) as ctx:
            generate_project(
                location=self.location,
                runtime=self.runtime,
                package_type=ZIP,
                dependency_manager="gradle",
                output_dir=self.output_dir,
                name=self.name,
                no_input=self.no_input,
            )
        self.assertEqual(
            "An error occurred while generating this project "
            "testing project: Lambda Runtime python3.9 "
            "does not support dependency manager: gradle",
            str(ctx.exception),
        )

    @patch("samcli.lib.init.cookiecutter")
    @patch("samcli.lib.init._create_default_samconfig")
    def test_when_generate_project_returns_error(self, default_samconfig_mock, cookiecutter_patch):
        # GIVEN generate_project fails to create a project
        ex = CookiecutterException("something is wrong")
        cookiecutter_patch.side_effect = ex

        expected_msg = str(GenerateProjectFailedError(project=self.name, provider_error=ex))

        # WHEN generate_project returns an error
        # THEN we should receive a GenerateProjectFailedError Exception
        with self.assertRaises(GenerateProjectFailedError) as ctx:
            generate_project(
                location=self.location,
                runtime=self.runtime,
                package_type=ZIP,
                dependency_manager=self.dependency_manager,
                output_dir=self.output_dir,
                name=self.name,
                no_input=self.no_input,
            )

        self.assertEqual(expected_msg, str(ctx.exception))

    @patch("samcli.lib.init._create_default_samconfig")
    @patch("samcli.lib.init.cookiecutter")
    def test_must_set_cookiecutter_context_when_location_and_extra_context_is_provided(
        self, cookiecutter_patch, default_samconfig_mock
    ):
        cookiecutter_context = {"key1": "value1", "key2": "value2"}
        custom_location = "mylocation"
        generate_project(
            location=custom_location, output_dir=self.output_dir, no_input=False, extra_context=cookiecutter_context
        )

        # THEN we should receive no errors
        cookiecutter_patch.assert_called_once_with(
            extra_context=cookiecutter_context, template=custom_location, no_input=False, output_dir=self.output_dir
        )

    @patch("samcli.lib.init.cookiecutter")
    @patch("samcli.lib.init._create_default_samconfig")
    def test_must_set_cookiecutter_context_when_app_template_is_provided(
        self, default_samconfig_mock, cookiecutter_patch
    ):
        cookiecutter_context = {"key1": "value1", "key2": "value2"}
        generate_project(
            runtime=self.runtime,
            package_type=ZIP,
            dependency_manager=self.dependency_manager,
            output_dir=self.output_dir,
            name=self.name,
            no_input=self.no_input,
            extra_context=cookiecutter_context,
        )

        # THEN we should receive no errors
        cookiecutter_patch.assert_called_once_with(
            extra_context=cookiecutter_context,
            no_input=self.no_input,
            output_dir=self.output_dir,
            template=self.template,
        )

    @patch("samcli.lib.init._create_default_samconfig")
    @patch("samcli.lib.init.cookiecutter")
    @patch("samcli.lib.init.generate_non_cookiecutter_project")
    def test_init_arbitrary_project_with_location_is_not_cookiecutter(
        self, generate_non_cookiecutter_project_mock, cookiecutter_mock, default_samconfig_mock
    ):
        cookiecutter_mock.side_effect = RepositoryNotFound("msg")

        generate_project(location=self.location, output_dir=self.output_dir)

        generate_non_cookiecutter_project_mock.assert_called_with(location=self.location, output_dir=self.output_dir)

    @patch("samcli.lib.init.cookiecutter")
    @patch("samcli.lib.init.generate_non_cookiecutter_project")
    @patch("samcli.lib.init._create_default_samconfig")
    def test_init_arbitrary_project_with_named_folder(
        self, default_samconfig_mock, generate_non_cookiecutter_project_mock, cookiecutter_mock
    ):
        cookiecutter_mock.side_effect = RepositoryNotFound("msg")

        generate_project(location=self.location, output_dir=self.output_dir, name=self.name)

        expected_output_dir = str(Path(self.output_dir, self.name))
        generate_non_cookiecutter_project_mock.assert_called_with(
            location=self.location, output_dir=expected_output_dir
        )

    @parameterized.expand(["https://example.com", "https://nonexist-domain.com"])
    @patch("samcli.lib.init._create_default_samconfig")
    def test_when_generate_project_with_invalid_template_location(self, invalid_location, default_samconfig_mock):
        expected_msg = str(InvalidLocationError(template=invalid_location))

        # WHEN the --location is not valid
        # THEN we should receive a InvalidLocationError Exception
        with self.assertRaises(InvalidLocationError) as ctx:
            generate_project(
                location=invalid_location,
                runtime=self.runtime,
                package_type=ZIP,
                dependency_manager=self.dependency_manager,
                output_dir=self.output_dir,
                name=self.name,
                no_input=self.no_input,
            )

        self.assertEqual(expected_msg, str(ctx.exception))

    @patch("samcli.lib.init.cookiecutter")
    def test_when_generate_project_with_invalid_zip_location(self, cookiecutter_patch):
        # INVALID ZIP PATH
        invalid_zip_path = Path("invalid_dir").joinpath("invalid.zip")

        # GIVEN generate_project fails to create a project
        ex = OSError(f"No such file or directory: {str(invalid_zip_path)}")
        cookiecutter_patch.side_effect = ex

        expected_msg = str(GenerateProjectFailedError(project=self.name, provider_error=ex))

        # WHEN the --location is not a valid local zip path
        # THEN we should receive a GenerateProjectFailedError Exception
        with self.assertRaises(GenerateProjectFailedError) as ctx:
            generate_project(
                location=invalid_zip_path,
                runtime=self.runtime,
                package_type=ZIP,
                dependency_manager=self.dependency_manager,
                output_dir=self.output_dir,
                name=self.name,
                no_input=self.no_input,
            )

        self.assertEqual(expected_msg, str(ctx.exception))

    @patch("samcli.lib.init.DefaultSamconfig")
    @patch("samcli.lib.init.Path.is_file")
    def test_create_default_samconfig(self, is_file_mock, samconfig_mock):
        is_file_mock.return_value = False
        _create_default_samconfig("zip", "outdir", "sam-app")
        samconfig_mock.assert_called_once_with(Path("outdir/sam-app"), "zip", "sam-app")

    @patch("samcli.lib.init.DefaultSamconfig")
    @patch("samcli.lib.init.Path.is_file")
    def test_doesnt_modify_samconfig_already_exists(self, is_file_mock, samconfig_mock):
        is_file_mock.return_value = True
        _create_default_samconfig("zip", "outdir", "sam-app")
        samconfig_mock.assert_not_called()