"""Tests for MountExternalAPIKeyMiddleware."""

import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError

from csp_gateway import (
    Gateway,
    GatewaySettings,
    MountRestRoutes,
)
from csp_gateway.server.demo import (
    ExampleGatewayChannels,
    ExampleModule,
)
from csp_gateway.server.middleware.api_key import MountAPIKeyMiddleware
from csp_gateway.server.middleware.api_key_external import MountExternalAPIKeyMiddleware
from csp_gateway.testing.mock_validators import (
    mock_api_key_validator_admin,
    mock_api_key_validator_invalid,
    mock_api_key_validator_valid,
)


class TestMountExternalAPIKeyMiddleware:
    """Test MountExternalAPIKeyMiddleware with external validation."""

    @pytest.fixture(scope="class")
    def external_key_gateway(self, free_port):
        """Create a gateway with external API key validation."""
        gateway = Gateway(
            modules=[
                ExampleModule(),
                MountRestRoutes(force_mount_all=True),
                MountExternalAPIKeyMiddleware(external_validator=mock_api_key_validator_valid),
            ],
            channels=ExampleGatewayChannels(),
            settings=GatewaySettings(PORT=free_port),
        )
        return gateway

    @pytest.fixture(scope="class")
    def external_key_webserver(self, external_key_gateway):
        external_key_gateway.start(rest=True, _in_test=True)
        yield external_key_gateway
        external_key_gateway.stop()

    @pytest.fixture(scope="class")
    def external_key_rest_client(self, external_key_webserver) -> TestClient:
        return TestClient(external_key_webserver.web_app.get_fastapi())

    def test_external_valid_key_accepted(self, external_key_rest_client: TestClient):
        """Test that a valid external API key is accepted."""
        response = external_key_rest_client.get("/api/v1/last?token=valid_key_1")
        assert response.status_code == 200, "Valid external API key should be accepted"

    def test_external_second_valid_key_accepted(self, external_key_rest_client: TestClient):
        """Test that a second valid external API key is also accepted."""
        response = external_key_rest_client.get("/api/v1/last?token=valid_key_2")
        assert response.status_code == 200, "Second valid external API key should be accepted"

    def test_external_invalid_key_rejected(self, external_key_rest_client: TestClient):
        """Test that an invalid external API key is rejected."""
        response = external_key_rest_client.get("/api/v1/last?token=invalid_key")
        assert response.status_code == 403, "Invalid external API key should be rejected"

    def test_external_no_key_rejected(self, external_key_rest_client: TestClient):
        """Test that requests without an API key are rejected."""
        response = external_key_rest_client.get("/api/v1/last")
        assert response.status_code == 403, "Request without API key should be rejected"


class TestExternalAPIKeyLogin:
    """Test login/logout flow with external API key validation."""

    @pytest.fixture(scope="class")
    def login_gateway(self, free_port):
        """Create a gateway for login testing."""
        gateway = Gateway(
            modules=[
                ExampleModule(),
                MountRestRoutes(force_mount_all=True),
                MountExternalAPIKeyMiddleware(external_validator=mock_api_key_validator_valid),
            ],
            channels=ExampleGatewayChannels(),
            settings=GatewaySettings(PORT=free_port),
        )
        return gateway

    @pytest.fixture(scope="class")
    def login_webserver(self, login_gateway):
        login_gateway.start(rest=True, _in_test=True)
        yield login_gateway
        login_gateway.stop()

    @pytest.fixture(scope="class")
    def login_rest_client(self, login_webserver) -> TestClient:
        return TestClient(login_webserver.web_app.get_fastapi())

    def test_login_redirects_with_valid_key(self, login_rest_client: TestClient):
        """Test that login with a valid key redirects."""
        response = login_rest_client.get("/api/v1/auth/login?token=valid_key_1", follow_redirects=False)
        assert response.status_code == 307, "Login should redirect"
        # Note: httponly cookies may not be visible in TestClient.cookies
        # The actual cookie setting is tested implicitly by successful authentication

    def test_logout_redirects(self, login_rest_client: TestClient):
        """Test that public logout redirects to login page."""
        # Public logout should always redirect to login page
        logout_response = login_rest_client.get("/logout", follow_redirects=False)
        assert logout_response.status_code == 200, "Public logout page should return 200"


class TestExternalAPIKeyIdentityStore:
    """Test that identity is stored and retrieved correctly."""

    def test_identity_stored_on_login(self):
        """Test that identity is stored in _identity_store after validation."""
        middleware = MountExternalAPIKeyMiddleware(external_validator=mock_api_key_validator_valid)

        # Clear any existing identity store
        middleware._identity_store = {}

        # Directly invoke the external validator to check identity
        identity = middleware._invoke_external("valid_key_1", None, None)
        assert identity == {"user": "alice", "role": "admin"}

    def test_invalid_key_returns_none(self):
        """Test that invalid key returns None from validator."""
        middleware = MountExternalAPIKeyMiddleware(external_validator=mock_api_key_validator_invalid)

        identity = middleware._invoke_external("any_key", None, None)
        assert identity is None


class TestExternalValidatorConfiguration:
    """Test validator configuration and validation."""

    def test_invalid_python_path_raises(self):
        """Test that invalid python path raises ValidationError."""
        with pytest.raises(ValidationError, match="ensure this value contains valid import path or importable object|Invalid python path"):
            MountExternalAPIKeyMiddleware(external_validator="not_a_valid_path")

    def test_validator_must_be_callable(self):
        """Test that external_validator must point to a callable object."""
        # This path points to a non-callable (a string constant)
        with pytest.raises(ValueError, match="external_validator must point to a callable object"):
            MountExternalAPIKeyMiddleware(external_validator="csp_gateway.testing.mock_validators.NON_CALLABLE")

    def test_none_validator_allowed(self):
        """Test that None is allowed for external_validator."""
        middleware = MountExternalAPIKeyMiddleware(external_validator=None)
        assert middleware.external_validator is None


class TestScopeMatching:
    """Test scope-based authentication filtering."""

    @pytest.fixture(scope="class")
    def scoped_gateway(self, free_port):
        """Create a gateway with scoped API key validation (only /api/*)."""
        gateway = Gateway(
            modules=[
                ExampleModule(),
                MountRestRoutes(force_mount_all=True),
                MountExternalAPIKeyMiddleware(
                    external_validator=mock_api_key_validator_valid,
                    scope="/api/*",
                ),
            ],
            channels=ExampleGatewayChannels(),
            settings=GatewaySettings(PORT=free_port),
        )
        return gateway

    @pytest.fixture(scope="class")
    def scoped_webserver(self, scoped_gateway):
        scoped_gateway.start(rest=True, _in_test=True)
        yield scoped_gateway
        scoped_gateway.stop()

    @pytest.fixture(scope="class")
    def scoped_rest_client(self, scoped_webserver) -> TestClient:
        return TestClient(scoped_webserver.web_app.get_fastapi())

    def test_api_route_requires_auth(self, scoped_rest_client: TestClient):
        """Test that /api/* routes require authentication."""
        response = scoped_rest_client.get("/api/v1/last")
        assert response.status_code == 403, "API route without key should be rejected"

    def test_api_route_accepts_valid_key(self, scoped_rest_client: TestClient):
        """Test that /api/* routes accept valid keys."""
        response = scoped_rest_client.get("/api/v1/last?token=valid_key_1")
        assert response.status_code == 200, "API route with valid key should be accepted"


@pytest.mark.skip(
    reason="Multiple middlewares with different scopes requires scope checking at validate() level, which conflicts with WebSocket support"
)
class TestMultipleScopedMiddlewares:
    """Test multiple authentication middlewares with different scopes."""

    @pytest.fixture(scope="class")
    def multi_scope_gateway(self, free_port):
        """Create a gateway with two middlewares with different scopes and keys."""
        validator_path_1 = mock_api_key_validator_valid
        validator_path_2 = mock_api_key_validator_admin
        gateway = Gateway(
            modules=[
                ExampleModule(),
                MountRestRoutes(force_mount_all=True),
                # First middleware: validates /api/v1/* with valid_key_1, valid_key_2
                MountExternalAPIKeyMiddleware(
                    external_validator=validator_path_1,
                    scope="/api/v1/*",
                ),
                # Second middleware: validates /api/admin/* with admin_key only
                MountExternalAPIKeyMiddleware(
                    external_validator=validator_path_2,
                    scope="/api/admin/*",
                ),
            ],
            channels=ExampleGatewayChannels(),
            settings=GatewaySettings(PORT=free_port),
        )
        return gateway

    @pytest.fixture(scope="class")
    def multi_scope_webserver(self, multi_scope_gateway):
        multi_scope_gateway.start(rest=True, _in_test=True)
        yield multi_scope_gateway
        multi_scope_gateway.stop()

    @pytest.fixture(scope="class")
    def multi_scope_rest_client(self, multi_scope_webserver) -> TestClient:
        return TestClient(multi_scope_webserver.web_app.get_fastapi())

    def test_v1_route_accepts_v1_key(self, multi_scope_rest_client: TestClient):
        """Test that /api/v1/* accepts valid_key_1."""
        response = multi_scope_rest_client.get("/api/v1/last?token=valid_key_1")
        assert response.status_code == 200, "v1 route should accept valid_key_1"

    def test_v1_route_rejects_admin_key(self, multi_scope_rest_client: TestClient):
        """Test that /api/v1/* rejects admin_key (wrong scope)."""
        response = multi_scope_rest_client.get("/api/v1/last?token=admin_key")
        assert response.status_code == 403, "v1 route should reject admin_key"


class TestListScope:
    """Test scope as a list of patterns."""

    @pytest.fixture(scope="class")
    def list_scope_gateway(self, free_port):
        """Create a gateway with a list of scope patterns."""
        gateway = Gateway(
            modules=[
                ExampleModule(),
                MountRestRoutes(force_mount_all=True),
                MountExternalAPIKeyMiddleware(
                    external_validator=mock_api_key_validator_valid,
                    scope=["/api/v1/*", "/api/v2/*"],
                ),
            ],
            channels=ExampleGatewayChannels(),
            settings=GatewaySettings(PORT=free_port),
        )
        return gateway

    @pytest.fixture(scope="class")
    def list_scope_webserver(self, list_scope_gateway):
        list_scope_gateway.start(rest=True, _in_test=True)
        yield list_scope_gateway
        list_scope_gateway.stop()

    @pytest.fixture(scope="class")
    def list_scope_rest_client(self, list_scope_webserver) -> TestClient:
        return TestClient(list_scope_webserver.web_app.get_fastapi())

    def test_v1_route_requires_auth(self, list_scope_rest_client: TestClient):
        """Test that /api/v1/* requires authentication."""
        response = list_scope_rest_client.get("/api/v1/last")
        assert response.status_code == 403, "v1 route without key should be rejected"

    def test_v1_route_accepts_valid_key(self, list_scope_rest_client: TestClient):
        """Test that /api/v1/* accepts valid key."""
        response = list_scope_rest_client.get("/api/v1/last?token=valid_key_1")
        assert response.status_code == 200, "v1 route with valid key should be accepted"


class TestScopeMatchingUnit:
    """Unit tests for _matches_scope method."""

    def test_matches_wildcard(self):
        """Test that '*' matches all paths."""
        middleware = MountExternalAPIKeyMiddleware(external_validator=None, scope="*")
        assert middleware._matches_scope("/api/v1/last") is True
        assert middleware._matches_scope("/anything") is True

    def test_matches_specific_pattern(self):
        """Test that specific patterns match correctly."""
        middleware = MountExternalAPIKeyMiddleware(external_validator=None, scope="/api/*")
        assert middleware._matches_scope("/api/v1/last") is True
        assert middleware._matches_scope("/api/") is True
        assert middleware._matches_scope("/other/path") is False

    def test_matches_list_patterns(self):
        """Test that list of patterns works correctly."""
        middleware = MountExternalAPIKeyMiddleware(external_validator=None, scope=["/api/*", "/admin/*"])
        assert middleware._matches_scope("/api/v1/last") is True
        assert middleware._matches_scope("/admin/users") is True
        assert middleware._matches_scope("/public/page") is False

    def test_none_scope_matches_all(self):
        """Test that None scope matches all paths."""
        middleware = MountExternalAPIKeyMiddleware(external_validator=None, scope=None)
        assert middleware._matches_scope("/any/path") is True

    def test_skip_if_out_of_scope(self):
        """Test _skip_if_out_of_scope helper."""
        from unittest.mock import Mock

        middleware = MountExternalAPIKeyMiddleware(external_validator=None, scope="/api/*")

        request_in_scope = Mock()
        request_in_scope.url.path = "/api/v1/last"
        assert middleware._skip_if_out_of_scope(request_in_scope) is False

        request_out_of_scope = Mock()
        request_out_of_scope.url.path = "/public/page"
        assert middleware._skip_if_out_of_scope(request_out_of_scope) is True


class TestMountAPIKeyMiddlewareScope:
    """Test MountAPIKeyMiddleware with scope configuration."""

    @pytest.fixture(scope="class")
    def scoped_static_key_gateway(self, free_port):
        """Create a gateway with scoped static API key validation."""
        gateway = Gateway(
            modules=[
                ExampleModule(),
                MountRestRoutes(force_mount_all=True),
                MountAPIKeyMiddleware(
                    api_key="test-api-key",
                    scope="/api/*",
                ),
            ],
            channels=ExampleGatewayChannels(),
            settings=GatewaySettings(PORT=free_port),
        )
        return gateway

    @pytest.fixture(scope="class")
    def scoped_static_key_webserver(self, scoped_static_key_gateway):
        scoped_static_key_gateway.start(rest=True, _in_test=True)
        yield scoped_static_key_gateway
        scoped_static_key_gateway.stop()

    @pytest.fixture(scope="class")
    def scoped_static_key_rest_client(self, scoped_static_key_webserver) -> TestClient:
        return TestClient(scoped_static_key_webserver.web_app.get_fastapi())

    def test_api_route_requires_auth(self, scoped_static_key_rest_client: TestClient):
        """Test that /api/* routes require authentication with MountAPIKeyMiddleware."""
        response = scoped_static_key_rest_client.get("/api/v1/last")
        assert response.status_code == 403, "API route without key should be rejected"

    def test_api_route_accepts_valid_key(self, scoped_static_key_rest_client: TestClient):
        """Test that /api/* routes accept valid keys with MountAPIKeyMiddleware."""
        response = scoped_static_key_rest_client.get("/api/v1/last?token=test-api-key")
        assert response.status_code == 200, "API route with valid key should be accepted"

    def test_api_route_rejects_invalid_key(self, scoped_static_key_rest_client: TestClient):
        """Test that /api/* routes reject invalid keys with MountAPIKeyMiddleware."""
        response = scoped_static_key_rest_client.get("/api/v1/last?token=wrong-key")
        assert response.status_code == 403, "API route with invalid key should be rejected"


class TestMountAPIKeyMiddlewareScopeUnit:
    """Unit tests for MountAPIKeyMiddleware scope methods."""

    def test_matches_scope_wildcard(self):
        """Test that '*' matches all paths."""
        middleware = MountAPIKeyMiddleware(api_key="test", scope="*")
        assert middleware._matches_scope("/api/v1/last") is True
        assert middleware._matches_scope("/anything") is True

    def test_matches_scope_specific_pattern(self):
        """Test that specific patterns match correctly."""
        middleware = MountAPIKeyMiddleware(api_key="test", scope="/api/*")
        assert middleware._matches_scope("/api/v1/last") is True
        assert middleware._matches_scope("/other/path") is False

    def test_matches_scope_list_patterns(self):
        """Test that list of patterns works correctly."""
        middleware = MountAPIKeyMiddleware(api_key="test", scope=["/api/*", "/admin/*"])
        assert middleware._matches_scope("/api/v1/last") is True
        assert middleware._matches_scope("/admin/users") is True
        assert middleware._matches_scope("/public/page") is False
