import base64
import json
from unittest import SkipTest
from uuid import uuid4

import boto3
import pytest
from botocore.exceptions import ClientError

from moto import mock_ec2, mock_lambda, settings

from ..markers import requires_docker
from .utilities import (
    get_lambda_using_environment_port,
    get_lambda_using_network_mode,
    get_proxy_zip_file,
    get_role_name,
    get_test_zip_file1,
    get_test_zip_file2,
    get_test_zip_file_error,
    get_test_zip_largeresponse,
    get_zip_with_multiple_files,
)

PYTHON_VERSION = "python3.11"
_lambda_region = "us-west-2"


@pytest.mark.network
@requires_docker
class TestLambdaInvocations_Error:
    mock = mock_lambda()

    @classmethod
    def setup_class(cls):
        cls.mock.start()
        cls.client = boto3.client("lambda", _lambda_region)
        cls.function_name = str(uuid4())[0:6]
        cls.client.create_function(
            FunctionName=cls.function_name,
            Runtime=PYTHON_VERSION,
            Role=get_role_name(),
            Handler="lambda_function.lambda_handler",
            Code={"ZipFile": get_test_zip_file_error()},
        )

    @classmethod
    def teardown_class(cls):
        try:
            cls.mock.stop()
        except RuntimeError:
            pass

    @pytest.mark.parametrize("invocation_type", [None, "RequestResponse"])
    def test_invoke_function_that_throws_error(self, invocation_type):
        kw = {"LogType": "Tail"}
        if invocation_type:
            kw["InvocationType"] = invocation_type

        failure_response = TestLambdaInvocations_Error.client.invoke(
            FunctionName=self.function_name, Payload=json.dumps({}), **kw
        )

        assert failure_response["FunctionError"] == "Handled"

        payload = failure_response["Payload"].read().decode("utf-8")
        payload = json.loads(payload)
        assert payload["errorType"] == "Exception"
        assert payload["errorMessage"] == "I failed!"
        assert "stackTrace" in payload

        logs = base64.b64decode(failure_response["LogResult"]).decode("utf-8")
        assert "START RequestId:" in logs
        assert "I failed!" in logs
        assert "Traceback (most recent call last):" in logs
        assert "END RequestId:" in logs


@pytest.mark.network
@requires_docker
class TestLambdaInvocations:
    mock = mock_lambda()

    @classmethod
    def setup_class(cls):
        cls.mock.start()
        cls.client = boto3.client("lambda", _lambda_region)
        cls.function_name = str(uuid4())[0:6]
        cls.fxn = cls.client.create_function(
            FunctionName=cls.function_name,
            Runtime=PYTHON_VERSION,
            Role=get_role_name(),
            Handler="lambda_function.lambda_handler",
            Code={"ZipFile": get_test_zip_file1()},
            Description="test lambda function",
            Timeout=3,
            MemorySize=128,
            Publish=True,
        )

    @classmethod
    def teardown_class(cls):
        try:
            cls.mock.stop()
        except RuntimeError:
            pass

    @pytest.mark.parametrize("invocation_type", [None, "RequestResponse"])
    @pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
    def test_invoke_requestresponse_function(self, invocation_type, key):
        name_or_arn = self.fxn[key]

        # Only add invocation-type keyword-argument when provided, otherwise the request
        # fails to be validated
        kw = {}
        if invocation_type:
            kw["InvocationType"] = invocation_type

        in_data = {"msg": "So long and thanks for all the fish"}
        success_result = self.client.invoke(
            FunctionName=name_or_arn, Payload=json.dumps(in_data), LogType="Tail", **kw
        )

        if "FunctionError" in success_result:
            assert False, success_result["Payload"].read().decode("utf-8")

        assert success_result["StatusCode"] == 200
        assert (
            success_result["ResponseMetadata"]["HTTPHeaders"]["content-type"]
            == "application/json"
        )
        logs = base64.b64decode(success_result["LogResult"]).decode("utf-8")

        assert "START RequestId:" in logs
        assert "custom log event" in logs
        assert "END RequestId:" in logs

        payload = success_result["Payload"].read().decode("utf-8")
        assert json.loads(payload) == in_data

        # Logs should not be returned by default, only when the LogType-param is supplied
        success_result = self.client.invoke(
            FunctionName=name_or_arn, Payload=json.dumps(in_data), **kw
        )

        assert success_result["StatusCode"] == 200
        assert (
            success_result["ResponseMetadata"]["HTTPHeaders"]["content-type"]
            == "application/json"
        )
        assert "LogResult" not in success_result

    def test_invoke_event_function(self):
        with pytest.raises(ClientError):
            self.client.invoke(
                FunctionName="notAFunction", InvocationType="Event", Payload="{}"
            )

        in_data = {"msg": "So long and thanks for all the fish"}
        success_result = self.client.invoke(
            FunctionName=self.function_name,
            InvocationType="Event",
            Payload=json.dumps(in_data),
        )
        assert success_result["StatusCode"] == 202
        assert json.loads(success_result["Payload"].read().decode("utf-8")) == in_data

    def test_invoke_dryrun_function(self):
        in_data = {"msg": "So long and thanks for all the fish"}
        success_result = self.client.invoke(
            FunctionName=self.function_name,
            InvocationType="DryRun",
            Payload=json.dumps(in_data),
        )
        assert success_result["StatusCode"] == 204

    @pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
    def test_invoke_async_function(self, key):
        name_or_arn = self.fxn[key]

        success_result = self.client.invoke_async(
            FunctionName=name_or_arn, InvokeArgs=json.dumps({"test": "event"})
        )

        assert success_result["Status"] == 202


@pytest.mark.network
@mock_lambda
def test_invoke_lambda_using_environment_port():
    if not settings.TEST_SERVER_MODE:
        raise SkipTest("Can only test environment variables in server mode")
    conn = boto3.client("lambda", _lambda_region)
    function_name = str(uuid4())[0:6]
    conn.create_function(
        FunctionName=function_name,
        Runtime=PYTHON_VERSION,
        Role=get_role_name(),
        Handler="lambda_function.lambda_handler",
        Code={"ZipFile": get_lambda_using_environment_port()},
    )

    success_result = conn.invoke(
        FunctionName=function_name, InvocationType="Event", Payload="{}"
    )

    assert success_result["StatusCode"] == 202
    response = success_result["Payload"].read()
    response = json.loads(response.decode("utf-8"))

    functions = response["functions"]
    function_names = [f["FunctionName"] for f in functions]
    assert function_name in function_names

    # Host matches the full URL, so one of:
    # http://host.docker.internal:5000
    # http://172.0.2.1:5000
    # http://172.0.1.1:4555
    assert "http://" in response["host"]


@pytest.mark.network
@mock_lambda
def test_invoke_lambda_using_networkmode():
    """
    Special use case - verify that Lambda can send a request to 'http://localhost'
    This is only possible when the `network_mode` is set to host in the Docker args
    Test is only run in our CI (for now)
    """
    if not settings.moto_network_mode():
        raise SkipTest("Can only test this when NETWORK_MODE is specified")
    conn = boto3.client("lambda", _lambda_region)
    function_name = str(uuid4())[0:6]
    conn.create_function(
        FunctionName=function_name,
        Runtime=PYTHON_VERSION,
        Role=get_role_name(),
        Handler="lambda_function.lambda_handler",
        Code={"ZipFile": get_lambda_using_network_mode()},
    )

    success_result = conn.invoke(
        FunctionName=function_name, InvocationType="Event", Payload="{}"
    )

    response = success_result["Payload"].read()
    functions = json.loads(response.decode("utf-8"))["response"]
    function_names = [f["FunctionName"] for f in functions]
    assert function_name in function_names


@pytest.mark.network
@mock_lambda
@requires_docker
def test_invoke_function_with_multiple_files_in_zip():
    conn = boto3.client("lambda", _lambda_region)
    function_name = str(uuid4())[0:6]
    conn.create_function(
        FunctionName=function_name,
        Runtime=PYTHON_VERSION,
        Role=get_role_name(),
        Handler="lambda_function.lambda_handler",
        Code={"ZipFile": get_zip_with_multiple_files()},
        Description="test lambda function",
        Timeout=3,
        MemorySize=128,
        Publish=True,
    )

    in_data = {"msg": "So long and thanks for: "}
    success_result = conn.invoke(
        FunctionName=function_name, InvocationType="Event", Payload=json.dumps(in_data)
    )
    assert json.loads(success_result["Payload"].read().decode("utf-8")) == {
        "msg": "So long and thanks for: stuff"
    }


if settings.TEST_SERVER_MODE:

    @mock_ec2
    @mock_lambda
    def test_invoke_function_get_ec2_volume():
        conn = boto3.resource("ec2", _lambda_region)
        vol = conn.create_volume(Size=99, AvailabilityZone=_lambda_region)
        vol = conn.Volume(vol.id)

        conn = boto3.client("lambda", _lambda_region)
        function_name = str(uuid4())[0:6]
        conn.create_function(
            FunctionName=function_name,
            Runtime=PYTHON_VERSION,
            Role=get_role_name(),
            Handler="lambda_function.lambda_handler",
            Code={"ZipFile": get_test_zip_file2()},
            Description="test lambda function",
            Timeout=3,
            MemorySize=128,
            Publish=True,
        )

        in_data = {"volume_id": vol.id}
        result = conn.invoke(
            FunctionName=function_name,
            InvocationType="RequestResponse",
            Payload=json.dumps(in_data),
        )
        assert result["StatusCode"] == 200
        actual_payload = json.loads(result["Payload"].read().decode("utf-8"))
        expected_payload = {"id": vol.id, "state": vol.state, "size": vol.size}
        assert actual_payload == expected_payload


@pytest.mark.network
@mock_lambda
@requires_docker
@pytest.mark.xfail(message="Fails intermittently - functionality exists though")
def test_invoke_function_large_response():
    # AWS Lambda should only return bodies smaller than 6 MB
    conn = boto3.client("lambda", _lambda_region)
    fxn = conn.create_function(
        FunctionName=str(uuid4())[0:6],
        Runtime=PYTHON_VERSION,
        Role=get_role_name(),
        Handler="lambda_function.lambda_handler",
        Code={"ZipFile": get_test_zip_largeresponse()},
        Description="test lambda function",
        Timeout=3,
        MemorySize=128,
        Publish=True,
    )

    resp = conn.invoke(FunctionName=fxn["FunctionArn"])
    assert resp["FunctionError"] == "Unhandled"
    payload = resp["Payload"].read().decode("utf-8")
    payload = json.loads(payload)
    assert payload == {
        "errorMessage": "Response payload size exceeded maximum allowed payload size (6291556 bytes).",
        "errorType": "Function.ResponseSizeTooLarge",
    }

    # Absolutely fine when invoking async
    resp = conn.invoke(FunctionName=fxn["FunctionArn"], InvocationType="Event")
    assert "FunctionError" not in resp


@mock_lambda
def test_invoke_lambda_with_proxy():
    if not settings.test_proxy_mode():
        raise SkipTest("We only want to test this in ProxyMode")

    conn = boto3.resource("ec2", _lambda_region)
    vol = conn.create_volume(Size=99, AvailabilityZone=_lambda_region)
    vol = conn.Volume(vol.id)

    conn = boto3.client("lambda", _lambda_region)
    function_name = str(uuid4())[0:6]
    conn.create_function(
        FunctionName=function_name,
        Runtime=PYTHON_VERSION,
        Role=get_role_name(),
        Handler="lambda_function.lambda_handler",
        Code={"ZipFile": get_proxy_zip_file()},
        Description="test lambda function",
        Timeout=3,
        MemorySize=128,
        Publish=True,
    )

    in_data = {"volume_id": vol.id}
    result = conn.invoke(
        FunctionName=function_name,
        InvocationType="RequestResponse",
        Payload=json.dumps(in_data),
    )
    assert result["StatusCode"] == 200
    payload = result["Payload"].read().decode("utf-8")

    expected_payload = {"id": vol.id, "state": vol.state, "size": vol.size}
    assert json.loads(payload) == expected_payload
