"""Tests for JWT validator."""

import time

import pytest
from pytest_httpx import HTTPXMock

from vikdata_auth.jwt import JWTValidator, TokenClaims
from vikdata_auth.exceptions import AuthenticationError


class TestJWTValidator:
    """Tests for JWTValidator."""

    @pytest.fixture
    def jwks_url(self):
        return "https://api.vikdata.com/.well-known/jwks.json"

    @pytest.fixture
    def validator(self, jwks_url):
        return JWTValidator(
            jwks_url=jwks_url,
            audience="vikdata",
            issuer="https://api.vikdata.com",
        )

    @pytest.mark.asyncio
    async def test_validate_valid_token(
        self, validator, jwks_url, jwks, valid_token, httpx_mock: HTTPXMock
    ):
        """Successfully validate a valid token."""
        httpx_mock.add_response(url=jwks_url, json=jwks)

        claims = await validator.validate(valid_token)

        assert isinstance(claims, TokenClaims)
        assert claims.tenant_id is not None
        assert claims.user_id is not None
        assert claims.permissions == ["vessels:read"]

    @pytest.mark.asyncio
    async def test_validate_expired_token(
        self, validator, jwks_url, jwks, expired_token, httpx_mock: HTTPXMock
    ):
        """Expired token raises AuthenticationError."""
        httpx_mock.add_response(url=jwks_url, json=jwks)

        with pytest.raises(AuthenticationError, match="expired"):
            await validator.validate(expired_token)

    @pytest.mark.asyncio
    async def test_validate_invalid_audience(
        self, jwks_url, jwks, create_token, httpx_mock: HTTPXMock
    ):
        """Token with wrong audience raises AuthenticationError."""
        validator = JWTValidator(
            jwks_url=jwks_url,
            audience="other-service",
            issuer="https://api.vikdata.com",
        )
        httpx_mock.add_response(url=jwks_url, json=jwks)

        token = create_token(aud="vikdata")  # Wrong audience

        with pytest.raises(AuthenticationError, match="audience"):
            await validator.validate(token)

    @pytest.mark.asyncio
    async def test_validate_invalid_issuer(
        self, jwks_url, jwks, create_token, httpx_mock: HTTPXMock
    ):
        """Token with wrong issuer raises AuthenticationError."""
        validator = JWTValidator(
            jwks_url=jwks_url,
            audience="vikdata",
            issuer="https://other.com",
        )
        httpx_mock.add_response(url=jwks_url, json=jwks)

        token = create_token(iss="https://api.vikdata.com")  # Wrong issuer

        with pytest.raises(AuthenticationError, match="issuer"):
            await validator.validate(token)

    @pytest.mark.asyncio
    async def test_validate_missing_kid(
        self, validator, jwks_url, jwks, rsa_key_pair, httpx_mock: HTTPXMock
    ):
        """Token without kid in header raises AuthenticationError."""
        import jwt

        private_key, _ = rsa_key_pair
        httpx_mock.add_response(url=jwks_url, json=jwks)

        # Create token without kid
        token = jwt.encode(
            {
                "sub": "user-123",
                "tenant_id": "tenant-456",
                "exp": int(time.time()) + 3600,
                "iat": int(time.time()),
                "iss": "https://api.vikdata.com",
                "aud": "vikdata",
            },
            private_key,
            algorithm="RS256",
            headers={},  # No kid
        )

        with pytest.raises(AuthenticationError, match="kid"):
            await validator.validate(token)

    @pytest.mark.asyncio
    async def test_validate_unknown_kid(
        self, validator, jwks_url, jwks, create_token, httpx_mock: HTTPXMock
    ):
        """Token with unknown kid raises AuthenticationError after JWKS refresh."""
        # First call returns JWKS without the key
        # Second call (force refresh) also returns JWKS without the key
        httpx_mock.add_response(url=jwks_url, json=jwks)
        httpx_mock.add_response(url=jwks_url, json=jwks)

        token = create_token(custom_kid="unknown-key-id")

        with pytest.raises(AuthenticationError, match="Unknown key ID"):
            await validator.validate(token)

    @pytest.mark.asyncio
    async def test_validate_invalid_token_format(
        self, validator, jwks_url, httpx_mock: HTTPXMock
    ):
        """Malformed token raises AuthenticationError."""
        with pytest.raises(AuthenticationError, match="Invalid token"):
            await validator.validate("not-a-jwt")

    @pytest.mark.asyncio
    async def test_validate_without_expiry_check(
        self, validator, jwks_url, jwks, expired_token, httpx_mock: HTTPXMock
    ):
        """validate_without_expiry_check allows expired tokens."""
        httpx_mock.add_response(url=jwks_url, json=jwks)

        # This should NOT raise even though token is expired
        claims = await validator.validate_without_expiry_check(expired_token)

        assert isinstance(claims, TokenClaims)
        assert claims.exp < int(time.time())  # Confirm token is actually expired

    @pytest.mark.asyncio
    async def test_key_rotation_refresh(
        self, validator, jwks_url, jwks, create_token, rsa_key_pair, kid, httpx_mock: HTTPXMock
    ):
        """JWKS is refreshed when key not found (key rotation scenario)."""
        new_kid = "new-key-id"

        # First JWKS doesn't have the new key
        old_jwks = {"keys": []}
        httpx_mock.add_response(url=jwks_url, json=old_jwks)

        # Second JWKS (after refresh) has the key
        httpx_mock.add_response(url=jwks_url, json=jwks)

        # Token signed with key that's only in refreshed JWKS
        token = create_token()

        claims = await validator.validate(token)

        assert isinstance(claims, TokenClaims)
        assert len(httpx_mock.get_requests()) == 2  # Initial fetch + refresh
