from typing import cast

import pytest
from aws_lambda_typing.events import APIGatewayProxyEventV1, EventBridgeEvent

from iisi_app_core import (
    AmbiguousPolicyError,
    ApiGatewayEventHandler,
    ApiRoute,
    DefaultApiExceptionMapper,
    DefaultApiRouteResolver,
    DefaultCorsPolicy,
    DefaultPolicyResolver,
    EventBridgeEventHandler,
    EventPolicy,
    PolicyNotFoundError,
    Principal,
    Role,
    SystemPrincipalProvider,
    correlation_id,
    principal,
)


class FixedAuthenticator:
    def __init__(self, resolved_principal: Principal) -> None:
        self.resolved_principal = resolved_principal
        self.calls = 0

    def authenticate(self, request):  # noqa: ANN001, ANN202
        self.calls += 1
        return self.resolved_principal


class RecordingRouteHandler:
    route = ApiRoute(method="GET", resource="/items/{id}")

    def __init__(self) -> None:
        self.calls = 0
        self.last_resource = ""
        self.last_principal = ""
        self.last_correlation_id = ""

    def handle(self, request):  # noqa: ANN001, ANN202
        self.calls += 1
        self.last_resource = request.resource
        self.last_principal = principal().name
        self.last_correlation_id = correlation_id()
        return {
            "statusCode": 200,
            "headers": {},
            "multiValueHeaders": {},
            "body": "",
            "isBase64Encoded": False,
        }


class AmbiguousRouteHandler(RecordingRouteHandler):
    pass


class RecordingPolicyHandler:
    policy = EventPolicy(detail_type="sample", source="tests")

    def __init__(self) -> None:
        self.calls = 0
        self.last_principal = ""
        self.last_correlation_id = ""

    def handle(self, event):  # noqa: ANN001, ANN202
        self.calls += 1
        self.last_principal = principal().name
        self.last_correlation_id = correlation_id()
        return "ok"


class AmbiguousPolicyHandler(RecordingPolicyHandler):
    pass


def test_api_gateway_event_handler_routes_and_normalizes_resource() -> None:
    auth = FixedAuthenticator(Principal.of(name="alice", role=Role.READER, tenant_id="tenant"))
    route_handler = RecordingRouteHandler()
    handler = ApiGatewayEventHandler(
        route_resolver=DefaultApiRouteResolver([route_handler]),
        authenticator=auth,  # type: ignore[arg-type]
        exception_mapper=DefaultApiExceptionMapper(),
        cors_policy=DefaultCorsPolicy(),
    )

    response = handler.handle(
        cast(
            APIGatewayProxyEventV1,
            {
                "httpMethod": "GET",
                "path": "/items/123",
                "pathParameters": {"id": "123"},
                "headers": {"Origin": "http://localhost:3000", "X-Correlation-Id": "cid-123"},
            },
        )
    )

    assert response["statusCode"] == 200
    assert response["headers"]["Access-Control-Allow-Origin"] == "http://localhost:3000"
    assert auth.calls == 1
    assert route_handler.calls == 1
    assert route_handler.last_resource == "/items/{id}"
    assert route_handler.last_principal == "alice"
    assert route_handler.last_correlation_id == "cid-123"
    assert principal().name == "guest"


def test_api_gateway_event_handler_short_circuits_options_requests() -> None:
    class ExplodingAuthenticator:
        def authenticate(self, request):  # noqa: ANN001, ANN202
            raise AssertionError("authenticate should not be called")

    handler = ApiGatewayEventHandler(
        route_resolver=DefaultApiRouteResolver([]),
        authenticator=ExplodingAuthenticator(),  # type: ignore[arg-type]
        exception_mapper=DefaultApiExceptionMapper(),
        cors_policy=DefaultCorsPolicy(),
    )

    response = handler.handle(
        cast(
            APIGatewayProxyEventV1,
            {"httpMethod": "OPTIONS", "resource": "/items", "headers": {"Origin": "http://localhost:3000"}},
        )
    )

    assert response["statusCode"] == 200
    assert response["headers"]["Access-Control-Allow-Origin"] == "http://localhost:3000"


def test_api_gateway_event_handler_returns_not_found_when_no_route_matches() -> None:
    handler = ApiGatewayEventHandler(
        route_resolver=DefaultApiRouteResolver([]),
        authenticator=FixedAuthenticator(Principal.guest()),  # type: ignore[arg-type]
        exception_mapper=DefaultApiExceptionMapper(),
        cors_policy=DefaultCorsPolicy(),
    )

    response = handler.handle(
        cast(APIGatewayProxyEventV1, {"httpMethod": "GET", "resource": "/missing", "headers": {}})
    )

    assert response["statusCode"] == 404


def test_api_gateway_event_handler_returns_internal_server_error_for_ambiguous_routes() -> None:
    handler = ApiGatewayEventHandler(
        route_resolver=DefaultApiRouteResolver([RecordingRouteHandler(), AmbiguousRouteHandler()]),
        authenticator=FixedAuthenticator(Principal.guest()),  # type: ignore[arg-type]
        exception_mapper=DefaultApiExceptionMapper(),
        cors_policy=DefaultCorsPolicy(),
    )

    response = handler.handle(
        cast(APIGatewayProxyEventV1, {"httpMethod": "GET", "resource": "/items/{id}", "headers": {}})
    )

    assert response["statusCode"] == 500


def test_eventbridge_event_handler_routes_to_matching_policy_handler_and_sets_system_principal() -> None:
    policy_handler = RecordingPolicyHandler()
    handler = EventBridgeEventHandler(
        resolver=DefaultPolicyResolver([policy_handler]),
        principal_provider=SystemPrincipalProvider(),
    )

    result = handler.handle(
        cast(EventBridgeEvent, {"detail-type": "sample", "source": "tests", "detail": {"correlation_id": "cid-9"}})
    )

    assert result == "ok"
    assert policy_handler.calls == 1
    assert policy_handler.last_principal == "system"
    assert policy_handler.last_correlation_id == "cid-9"
    assert principal().name == "guest"


def test_eventbridge_event_handler_raises_when_no_policy_matches() -> None:
    handler = EventBridgeEventHandler(
        resolver=DefaultPolicyResolver([]),
        principal_provider=SystemPrincipalProvider(),
    )

    with pytest.raises(PolicyNotFoundError):
        handler.handle(cast(EventBridgeEvent, {"detail-type": "sample", "source": "tests", "detail": {}}))


def test_eventbridge_event_handler_raises_when_multiple_policies_match() -> None:
    handler = EventBridgeEventHandler(
        resolver=DefaultPolicyResolver([RecordingPolicyHandler(), AmbiguousPolicyHandler()]),
        principal_provider=SystemPrincipalProvider(),
    )

    with pytest.raises(AmbiguousPolicyError):
        handler.handle(cast(EventBridgeEvent, {"detail-type": "sample", "source": "tests", "detail": {}}))
