import boto3
import json
import requests
import sure  # noqa # pylint: disable=unused-import
import time
import pytest

from botocore.exceptions import ClientError
from moto import mock_lambda, mock_cloudformation, mock_logs, mock_s3, settings
from unittest import SkipTest
from uuid import uuid4
from tests.test_awslambda.utilities import wait_for_log_msg
from .fixtures.custom_lambda import get_template, get_template_for_unknown_lambda
from ..markers import requires_docker


def get_lambda_code():
    return f"""
def lambda_handler(event, context):
    # Need to print this, one of the tests verifies the correct input
    print(event)
    response = dict()
    response["Status"] = "SUCCESS"
    response["StackId"] = event["StackId"]
    response["RequestId"] = event["RequestId"]
    response["LogicalResourceId"] = event["LogicalResourceId"]
    response["PhysicalResourceId"] = "CustomResource{str(uuid4())[0:6]}"
    response_data = dict()
    response_data["info_value"] = "special value"
    if event["RequestType"] == "Create":
        response["Data"] = response_data
    import cfnresponse
    cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
"""


@mock_cloudformation
@mock_lambda
@mock_logs
@mock_s3
def test_create_custom_lambda_resource():
    #########
    # Integration test using a Custom Resource
    # Create a Lambda
    # CF will call the Lambda
    # The Lambda should call CF, to indicate success (using the cfnresponse-module)
    # This HTTP request will include any outputs that are now stored against the stack
    # TEST: verify that this output is persisted
    ##########
    if not settings.TEST_SERVER_MODE:
        raise SkipTest(
            "Needs a standalone MotoServer, as cfnresponse needs to connect to something"
        )
    # Create cloudformation stack
    stack_name = f"stack{str(uuid4())[0:6]}"
    template_body = get_template(get_lambda_code())
    cf = boto3.client("cloudformation", region_name="us-east-1")
    cf.create_stack(
        StackName=stack_name,
        TemplateBody=json.dumps(template_body),
        Capabilities=["CAPABILITY_IAM"],
    )
    # Verify CloudWatch contains the correct logs
    log_group_name = get_log_group_name(cf, stack_name)
    success, logs = wait_for_log_msg(
        expected_msg="Status code: 200", log_group=log_group_name
    )
    assert success, f"Logs should indicate success: \n{logs}"

    # Verify the correct Output was returned
    outputs = get_outputs(cf, stack_name)
    outputs.should.have.length_of(1)
    outputs[0].should.have.key("OutputKey").equals("infokey")
    outputs[0].should.have.key("OutputValue").equals("special value")


@mock_cloudformation
@mock_lambda
@mock_logs
@mock_s3
@requires_docker
def test_create_custom_lambda_resource__verify_cfnresponse_failed():
    #########
    # Integration test using a Custom Resource
    # Create a Lambda
    # CF will call the Lambda
    # The Lambda should call CF --- this will fail, as we cannot make a HTTP request to the in-memory moto decorators
    # TEST: verify that the original event was send to the Lambda correctly
    # TEST: verify that a failure message appears in the CloudwatchLogs
    ##########
    if settings.TEST_SERVER_MODE:
        raise SkipTest("Verify this fails if MotoServer is not running")
    # Create cloudformation stack
    stack_name = f"stack{str(uuid4())[0:6]}"
    template_body = get_template(get_lambda_code())
    cf = boto3.client("cloudformation", region_name="us-east-1")
    cf.create_stack(
        StackName=stack_name,
        TemplateBody=json.dumps(template_body),
        Capabilities=["CAPABILITY_IAM"],
    )
    # Verify CloudWatch contains the correct logs
    log_group_name = get_log_group_name(cf, stack_name)
    execution_failed, logs = wait_for_log_msg(
        expected_msg="failed executing http.request", log_group=log_group_name
    )
    execution_failed.should.equal(True)

    printed_events = [
        line for line in logs if line.startswith("{'RequestType': 'Create'")
    ]
    printed_events.should.have.length_of(1)
    original_event = json.loads(printed_events[0].replace("'", '"'))
    original_event.should.have.key("RequestType").equals("Create")
    original_event.should.have.key("ServiceToken")  # Should equal Lambda ARN
    original_event.should.have.key("ResponseURL")
    original_event.should.have.key("StackId")
    original_event.should.have.key("RequestId")  # type UUID
    original_event.should.have.key("LogicalResourceId").equals("CustomInfo")
    original_event.should.have.key("ResourceType").equals("Custom::Info")
    original_event.should.have.key("ResourceProperties")
    original_event["ResourceProperties"].should.have.key(
        "ServiceToken"
    )  # Should equal Lambda ARN
    original_event["ResourceProperties"].should.have.key("MyProperty").equals("stuff")


@mock_cloudformation
@mock_lambda
@mock_logs
@mock_s3
def test_create_custom_lambda_resource__verify_manual_request():
    #########
    # Integration test using a Custom Resource
    # Create a Lambda
    # CF will call the Lambda
    # The Lambda should call CF --- this will fail, as we cannot make a HTTP request to the in-memory moto decorators
    # So we'll make this HTTP request manually
    # TEST: verify that the stack has a CREATE_IN_PROGRESS status before making the HTTP request
    # TEST: verify that the stack has a CREATE_COMPLETE status afterwards
    ##########
    if settings.TEST_SERVER_MODE:
        raise SkipTest(
            "Verify HTTP request can be made manually if MotoServer is not running"
        )
    # Create cloudformation stack
    stack_name = f"stack{str(uuid4())[0:6]}"
    template_body = get_template(get_lambda_code())
    region_name = "eu-north-1"
    cf = boto3.client("cloudformation", region_name=region_name)
    stack = cf.create_stack(
        StackName=stack_name,
        TemplateBody=json.dumps(template_body),
        Capabilities=["CAPABILITY_IAM"],
    )
    stack_id = stack["StackId"]
    stack = cf.describe_stacks(StackName=stack_id)["Stacks"][0]
    stack["Outputs"].should.equal([])
    stack["StackStatus"].should.equal("CREATE_IN_PROGRESS")

    callback_url = f"http://cloudformation.{region_name}.amazonaws.com/cloudformation_{region_name}/cfnresponse?stack={stack_id}"
    data = {
        "Status": "SUCCESS",
        "StackId": stack_id,
        "LogicalResourceId": "CustomInfo",
        "Data": {"info_value": "resultfromthirdpartysystem"},
    }
    requests.post(callback_url, json=data)

    stack = cf.describe_stacks(StackName=stack_id)["Stacks"][0]
    stack["StackStatus"].should.equal("CREATE_COMPLETE")
    stack["Outputs"].should.equal(
        [{"OutputKey": "infokey", "OutputValue": "resultfromthirdpartysystem"}]
    )


@mock_cloudformation
def test_create_custom_lambda_resource__unknown_arn():
    # Try to create a Lambda with an unknown ARN
    # Verify that this fails in a predictable manner
    cf = boto3.client("cloudformation", region_name="eu-north-1")
    with pytest.raises(ClientError) as exc:
        cf.create_stack(
            StackName=f"stack{str(uuid4())[0:6]}",
            TemplateBody=json.dumps(get_template_for_unknown_lambda()),
            Capabilities=["CAPABILITY_IAM"],
        )
    err = exc.value.response["Error"]
    err["Code"].should.equal("ValidationError")
    err["Message"].should.equal(
        "Template error: instance of Fn::GetAtt references undefined resource InfoFunction"
    )


def get_log_group_name(cf, stack_name):
    resources = cf.describe_stack_resources(StackName=stack_name)["StackResources"]
    start = time.time()
    while (time.time() - start) < 5:
        fns = [
            r
            for r in resources
            if r["ResourceType"] == "AWS::Lambda::Function"
            and "PhysicalResourceId" in r
        ]
        if not fns:
            time.sleep(1)
            resources = cf.describe_stack_resources(StackName=stack_name)[
                "StackResources"
            ]
            continue

        fn = fns[0]
        resource_id = fn["PhysicalResourceId"]
        return f"/aws/lambda/{resource_id}"
    raise Exception("Could not find log group name in time")


def get_outputs(cf, stack_name):
    stack = cf.describe_stacks(StackName=stack_name)["Stacks"][0]
    start = time.time()
    while (time.time() - start) < 5:
        status = stack["StackStatus"]
        if status != "CREATE_COMPLETE":
            time.sleep(1)
            stack = cf.describe_stacks(StackName=stack_name)["Stacks"][0]
            continue

        outputs = stack["Outputs"]
        return outputs