# -*- coding: utf-8 -*-

import json
import os
import threading

import pytest
import yaml
from botocore.exceptions import ClientError
from freezegun import freeze_time
from mock import patch, sentinel, Mock

import awsscripter.common.template
from awsscripter.common.connection_manager import ConnectionManager
from awsscripter.common.exceptions import TemplateSceptreHandlerError
from awsscripter.common.exceptions import UnsupportedTemplateFileTypeError
from awsscripter.common.template import Template


class TestTemplate(object):

    def setup_method(self, test_method):
        self.region = "region"
        self.bucket_name = "bucket_name"
        self.environment_path = "environment_path"
        self.stack_name = "stack_name"

        connection_manager = Mock(spec=ConnectionManager)
        connection_manager.create_bucket_lock = threading.Lock()

        self.template = Template(
            path="/folder/template.py",
            sceptre_user_data={},
            connection_manager=connection_manager
        )

    def test_initialise_template(self):
        assert self.template.path == "/folder/template.py"
        assert self.template.name == "template"
        assert self.template.sceptre_user_data == {}
        assert self.template._body is None

    def test_repr(self):
        representation = self.template.__repr__()
        assert representation == "awsscripter.template.Template(" \
            "name='template', path='/folder/template.py'"\
            ", sceptre_user_data={}, s3_details=None)"

    def test_body_with_cache(self):
        self.template._body = sentinel.body
        body = self.template.body
        assert body == sentinel.body

    @freeze_time("2012-01-01")
    @patch("awsscripter.template.Template._bucket_exists")
    def test_upload_to_s3_with_valid_s3_details(self, mock_bucket_exists):
        self.template._body = '{"template": "mock"}'
        mock_bucket_exists.return_value = True
        self.template.s3_details = {
            "bucket_name": "bucket-name",
            "bucket_key": "bucket-key"
        }

        self.template.upload_to_s3()

        self.template.connection_manager.call.assert_called_once_with(
            service="s3",
            command="put_object",
            kwargs={
                "Bucket": "bucket-name",
                "Key": "bucket-key",
                "Body": '{"template": "mock"}',
                "ServerSideEncryption": "AES256"
            }
        )

    def test_bucket_exists_with_bucket_that_exists(self):
        # connection_manager.call doesn't raise an exception, mimicing the
        # behaviour when head_bucket successfully executes.
        self.template.s3_details = {
            "bucket_name": "bucket-name",
            "bucket_key": "bucket-key"
        }

        assert self.template._bucket_exists() is True

    def test_create_bucket_with_unreadable_bucket(self):
        self.template.connection_manager.region = "eu-west-1"
        self.template.s3_details = {
            "bucket_name": "bucket-name",
            "bucket_key": "bucket-key"
        }

        self.template.connection_manager.call.side_effect = ClientError(
                {
                    "Error": {
                        "Code": 500,
                        "Message": "Bucket Unreadable"
                    }
                },
                sentinel.operation
            )
        with pytest.raises(ClientError) as e:
            self.template._create_bucket()
            assert e.value.response["Error"]["Code"] == 500
            assert e.value.response["Error"]["Message"] == "Bucket Unreadable"

    def test_bucket_exists_with_non_existent_bucket(self):
        # connection_manager.call is called twice, and should throw the
        # Not Found ClientError only for the first call.
        self.template.s3_details = {
            "bucket_name": "bucket-name",
            "bucket_key": "bucket-key"
        }

        self.template.connection_manager.call.side_effect = [
            ClientError(
                {
                    "Error": {
                        "Code": 404,
                        "Message": "Not Found"
                    }
                },
                sentinel.operation
            ),
            None
        ]

        existance = self.template._bucket_exists()

        assert existance is False

    def test_create_bucket_in_us_east_1(self):
        # connection_manager.call is called twice, and should throw the
        # Not Found ClientError only for the first call.
        self.template.connection_manager.region = "us-east-1"
        self.template.s3_details = {
            "bucket_name": "bucket-name",
            "bucket_key": "bucket-key"
        }

        self.template._create_bucket()

        self.template.connection_manager.call.assert_any_call(
            service="s3",
            command="create_bucket",
            kwargs={"Bucket": "bucket-name"}
        )

    @patch("awsscripter.template.Template.upload_to_s3")
    def test_get_boto_call_parameter_with_s3_details(self, mock_upload_to_s3):
        # self.stack._template = Mock(spec=Template)
        mock_upload_to_s3.return_value = sentinel.template_url
        self.template.s3_details = {
            "bucket_name": sentinel.bucket_name,
            "bucket_key": sentinel.bucket_key
        }

        boto_parameter = self.template.get_boto_call_parameter()

        assert boto_parameter == {"TemplateURL": sentinel.template_url}

    def test_get_template_details_without_upload(self):
        self.template.s3_details = None
        self.template._body = sentinel.body
        boto_parameter = self.template.get_boto_call_parameter()

        assert boto_parameter == {"TemplateBody": sentinel.body}

    @patch("awsscripter.template.Template.get_boto_call_parameter")
    def test_validate_sends_correct_request(
        self, mock_get_boto_call_parameter
    ):
        mock_get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }

        self.template.validate()
        self.template.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="validate_template",
            kwargs={"Template": sentinel.template}
        )

    def test_body_with_json_template(self):
        self.template.name = "vpc"
        self.template.path = os.path.join(
            os.getcwd(),
            "tests/fixtures/templates/vpc.json"
        )
        output = self.template.body
        output_dict = json.loads(output)
        with open("tests/fixtures/templates/compiled_vpc.json", "r") as f:
            expected_output_dict = json.loads(f.read())
        assert output_dict == expected_output_dict

    def test_body_with_yaml_template(self):
        self.template.name = "vpc"
        self.template.path = os.path.join(
            os.getcwd(),
            "tests/fixtures/templates/vpc.yaml"
        )
        output = self.template.body
        output_dict = yaml.load(output)
        with open("tests/fixtures/templates/compiled_vpc.json", "r") as f:
            expected_output_dict = json.loads(f.read())
        assert output_dict == expected_output_dict

    def test_body_with_missing_file(self):
        self.template.path = "incorrect/template/path.py"
        with pytest.raises(IOError):
            self.template.body

    def test_body_with_python_template(self):
        self.template.sceptre_user_data = None
        self.template.name = "vpc"
        self.template.path = os.path.join(
            os.getcwd(),
            "tests/fixtures/templates/vpc.py"
        )
        actual_output = json.loads(self.template.body)
        with open("tests/fixtures/templates/compiled_vpc.json", "r") as f:
            expected_output = json.loads(f.read())
        assert actual_output == expected_output

    def test_body_with_python_template_with_sgt(self):
        self.template.sceptre_user_data = None
        self.template.name = "vpc_sgt"
        self.template.path = os.path.join(
            os.getcwd(),
            "tests/fixtures/templates/vpc_sgt.py"
        )
        actual_output = json.loads(self.template.body)
        with open("tests/fixtures/templates/compiled_vpc.json", "r") as f:
            expected_output = json.loads(f.read())
        assert actual_output == expected_output

    def test_body_injects_sceptre_user_data(self):
        self.template.sceptre_user_data = {
            "cidr_block": "10.0.0.0/16"
        }
        self.template.name = "vpc_sud"
        self.template.path = os.path.join(
            os.getcwd(),
            "tests/fixtures/templates/vpc_sud.py"
        )

        actual_output = json.loads(self.template.body)
        with open("tests/fixtures/templates/compiled_vpc_sud.json", "r") as f:
            expected_output = json.loads(f.read())
        assert actual_output == expected_output

    def test_body_injects_sceptre_user_data_incorrect_function(self):
        self.template.sceptre_user_data = {
            "cidr_block": "10.0.0.0/16"
        }
        self.template.name = "vpc_sud_incorrect_function"
        self.template.path = os.path.join(
            os.getcwd(),
            "tests/fixtures/templates/vpc_sud_incorrect_function.py"
        )
        with pytest.raises(TemplateSceptreHandlerError):
            self.template.body

    def test_body_injects_sceptre_user_data_incorrect_handler(self):
        self.template.sceptre_user_data = {
            "cidr_block": "10.0.0.0/16"
        }
        self.template.name = "vpc_sud_incorrect_handler"
        self.template.path = os.path.join(
            os.getcwd(),
            "tests/fixtures/templates/vpc_sud_incorrect_handler.py"
        )
        with pytest.raises(TypeError):
            self.template.body

    def test_body_with_incorrect_filetype(self):
        self.template.path = (
            "path/to/something.ext"
        )
        with pytest.raises(UnsupportedTemplateFileTypeError):
            self.template.body


@pytest.mark.parametrize("filename,sceptre_user_data,expected", [
    (
        "vpc.j2",
        {"vpc_id": "10.0.0.0/16"},
        """Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
Outputs:
  VpcId:
    Value:
      Ref: VPC"""
    ),
    (
        "vpc.yaml.j2",
        {"vpc_id": "10.0.0.0/16"},
        """Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
Outputs:
  VpcId:
    Value:
      Ref: VPC"""
    ),
    (
        "sg.j2",
        [
            {"name": "sg_a", "inbound_ip": "10.0.0.0"},
            {"name": "sg_b", "inbound_ip": "10.0.0.1"}
        ],
        """Resources:
    sg_a:
        Type: "AWS::EC2::SecurityGroup"
        Properties:
            InboundIp: 10.0.0.0
    sg_b:
        Type: "AWS::EC2::SecurityGroup"
        Properties:
            InboundIp: 10.0.0.1
"""
    )
])
def test_render_jinja_template(filename, sceptre_user_data, expected):
    jinja_template_dir = os.path.join(
        os.getcwd(),
        "tests/fixtures/templates"
    )
    result = awsscripter.common.template.Template._render_jinja_template(
        template_dir=jinja_template_dir,
        filename=filename,
        jinja_vars={"sceptre_user_data": sceptre_user_data}
    )
    expected_yaml = yaml.safe_load(expected)
    result_yaml = yaml.safe_load(result)
    assert expected_yaml == result_yaml
