"""Tests for JWT token parsing and authentication handling."""

import base64
import json
from datetime import UTC, datetime, timedelta

import pytest

from yukka.auth import Token, YukkaPayload, _parse_jwt
from yukka.exceptions import ExpiredToken, TokenTooOld


def create_jwt_token(
    email: str = "test@yukkalab.com",
    groups: list[str] | None = None,
    exp_offset_seconds: int = 3600,
    iat_offset_seconds: int = 0,
) -> str:
    """Create a valid JWT token for testing."""
    if groups is None:
        groups = ["users", "quant"]

    now = datetime.now(tz=UTC)
    payload = {
        "exp": (now + timedelta(seconds=exp_offset_seconds)).timestamp(),
        "iat": (now + timedelta(seconds=iat_offset_seconds)).timestamp(),
        "email": email,
        "groups": groups,
    }

    header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()).decode().rstrip("=")
    payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=")
    signature = "fake_signature_for_testing"

    return f"{header}.{payload_b64}.{signature}"


class TestJWTParsing:
    """Tests for JWT token parsing functionality."""

    def test_parse_valid_jwt(self, valid_token_str: str):
        """Valid JWT should be parsed correctly."""
        payload = _parse_jwt(valid_token_str)

        assert isinstance(payload, YukkaPayload)
        assert payload.email == "test@yukkalab.com"
        assert payload.groups == ["users", "quant"]
        assert isinstance(payload.exp, datetime)
        assert isinstance(payload.iat, datetime)

    def test_parse_jwt_extracts_expiration_time(self):
        """JWT expiration time should be extracted correctly."""
        token = create_jwt_token(exp_offset_seconds=7200)
        payload = _parse_jwt(token)

        # Should be roughly 2 hours from now
        expected_exp = datetime.now(tz=UTC) + timedelta(hours=2)
        assert abs((payload.exp - expected_exp).total_seconds()) < 5

    def test_parse_jwt_with_different_groups(self):
        """JWT with custom groups should parse correctly."""
        groups = ["admin", "premium", "api-access"]
        token = create_jwt_token(groups=groups)
        payload = _parse_jwt(token)

        assert payload.groups == groups

    def test_parse_jwt_handles_base64_padding(self):
        """JWT base64 padding should be handled correctly."""
        # Create tokens with payloads of different lengths to test padding
        for email_len in range(1, 10):
            email = "a" * email_len + "@test.com"
            token = create_jwt_token(email=email)
            payload = _parse_jwt(token)
            assert payload.email == email

    def test_parse_malformed_jwt_missing_parts(self):
        """Malformed JWT with missing parts should raise an error."""
        with pytest.raises((IndexError, ValueError)):
            _parse_jwt("not.a.valid.jwt.token")

    def test_parse_malformed_jwt_invalid_base64(self):
        """JWT with invalid base64 encoding should raise an error."""
        # Create a token with invalid base64 in payload
        invalid_token = "eyJhbGciOiJIUzI1NiJ9.!!!invalid-base64!!!.signature"  # noqa: S105
        with pytest.raises(ValueError, match="base64"):
            _parse_jwt(invalid_token)

    def test_parse_malformed_jwt_invalid_json(self):
        """JWT with invalid JSON in payload should raise an error."""
        # Create base64 of invalid JSON
        invalid_json = base64.urlsafe_b64encode(b"not valid json").decode().rstrip("=")
        invalid_token = f"eyJhbGciOiJIUzI1NiJ9.{invalid_json}.signature"
        with pytest.raises(json.JSONDecodeError):
            _parse_jwt(invalid_token)

    def test_parse_jwt_missing_required_fields(self):
        """JWT payload missing required fields should raise KeyError."""
        # Create a payload missing 'exp' field
        payload = {"iat": 1234567890, "email": "test@test.com", "groups": []}
        payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=")
        token = f"eyJhbGciOiJIUzI1NiJ9.{payload_b64}.signature"

        with pytest.raises(KeyError):
            _parse_jwt(token)

    def test_parse_jwt_empty_string(self):
        """Empty JWT string should raise an error."""
        with pytest.raises((IndexError, ValueError)):
            _parse_jwt("")

    def test_parse_jwt_whitespace_only(self):
        """Whitespace-only JWT string should raise an error."""
        with pytest.raises((IndexError, ValueError)):
            _parse_jwt("   ")


class TestToken:
    """Tests for Token class functionality."""

    def test_token_from_valid_string(self, valid_token_str: str):
        """Token should be created from valid JWT string."""
        token = Token.from_token(valid_token_str)

        assert token._token == valid_token_str.strip()
        assert isinstance(token._payload, YukkaPayload)

    def test_token_from_string_with_whitespace(self, valid_token_str: str):
        """Token should strip whitespace from input."""
        token_with_whitespace = f"  {valid_token_str}  \n"
        token = Token.from_token(token_with_whitespace)

        assert token._token == valid_token_str

    def test_token_expired_property_false_for_valid(self, valid_token: Token):
        """Non-expired token should have expired=False."""
        assert valid_token.expired is False

    def test_token_expired_property_true_for_expired(self, expired_token: Token):
        """Expired token should have expired=True."""
        assert expired_token.expired is True

    def test_token_get_returns_string_for_valid(self, valid_token: Token, valid_token_str: str):
        """Token.get() should return token string for valid token."""
        result = valid_token.get()
        assert result == valid_token_str

    def test_token_get_raises_for_expired(self, expired_token: Token):
        """Token.get() should raise ExpiredToken for expired token."""
        with pytest.raises(ExpiredToken) as exc_info:
            expired_token.get()

        assert "expired" in str(exc_info.value).lower()

    def test_token_about_to_expire(self):
        """Token that expires very soon should still be valid until expiration."""
        # Token that expires in 1 second
        token_str = create_jwt_token(exp_offset_seconds=1)
        token = Token.from_token(token_str)

        # Should not be expired yet
        assert token.expired is False
        assert token.get() == token_str

    def test_token_just_expired(self):
        """Token that just expired should be detected as expired."""
        # Token that expired 1 second ago
        token_str = create_jwt_token(exp_offset_seconds=-1)
        token = Token.from_token(token_str)

        assert token.expired is True
        with pytest.raises(ExpiredToken):
            token.get()


class TestTokenTooOld:
    """Tests for the 3-month token age check."""

    def test_token_issued_today_is_not_too_old(self):
        """Token issued today should not be considered too old."""
        token_str = create_jwt_token(iat_offset_seconds=0)
        token = Token.from_token(token_str)
        assert token.too_old is False

    def test_token_issued_119_days_ago_is_not_too_old(self):
        """Token issued 119 days ago should not be considered too old."""
        token_str = create_jwt_token(iat_offset_seconds=-(119 * 24 * 3600))
        token = Token.from_token(token_str)
        assert token.too_old is False

    def test_token_issued_121_days_ago_is_too_old(self):
        """Token issued 121 days ago should be considered too old."""
        token_str = create_jwt_token(iat_offset_seconds=-(121 * 24 * 3600), exp_offset_seconds=121 * 24 * 3600)
        token = Token.from_token(token_str)
        assert token.too_old is True

    def test_get_raises_token_too_old(self):
        """Token.get() should raise TokenTooOld for a token older than 120 days."""
        token_str = create_jwt_token(iat_offset_seconds=-(121 * 24 * 3600), exp_offset_seconds=121 * 24 * 3600)
        token = Token.from_token(token_str)
        with pytest.raises(TokenTooOld):
            token.get()

    def test_too_old_error_message_contains_issued_date(self):
        """TokenTooOld error message should include the issuance date."""
        token_str = create_jwt_token(iat_offset_seconds=-(121 * 24 * 3600), exp_offset_seconds=121 * 24 * 3600)
        token = Token.from_token(token_str)
        with pytest.raises(TokenTooOld, match=r"\d{4}-\d{2}-\d{2}"):
            token.get()

    def test_too_old_checked_before_expired(self):
        """TokenTooOld should be raised even if the token is also expired."""
        token_str = create_jwt_token(iat_offset_seconds=-(121 * 24 * 3600), exp_offset_seconds=-3600)
        token = Token.from_token(token_str)
        with pytest.raises(TokenTooOld):
            token.get()


class TestExpiredTokenException:
    """Tests for ExpiredToken exception."""

    def test_expired_token_is_value_error(self):
        """ExpiredToken should be a subclass of ValueError."""
        assert issubclass(ExpiredToken, ValueError)

    def test_expired_token_message(self, expired_token: Token):
        """ExpiredToken should contain helpful message."""
        with pytest.raises(ExpiredToken, match=r"(?i)expired"):
            expired_token.get()


class TestTokenEdgeCases:
    """Edge case tests for token handling."""

    def test_token_with_very_long_expiration(self):
        """Token with expiration far in the future should work."""
        # Token that expires in 1 year
        token_str = create_jwt_token(exp_offset_seconds=365 * 24 * 3600)
        token = Token.from_token(token_str)

        assert token.expired is False
        assert token.get() == token_str

    def test_token_with_empty_groups(self):
        """Token with empty groups list should parse correctly."""
        token_str = create_jwt_token(groups=[])
        token = Token.from_token(token_str)

        assert token._payload.groups == []

    def test_token_with_special_characters_in_email(self):
        """Token with special characters in email should parse correctly."""
        special_email = "test+special@sub.domain.yukkalab.com"
        token_str = create_jwt_token(email=special_email)
        token = Token.from_token(token_str)

        assert token._payload.email == special_email

    def test_token_payload_timestamps_are_utc(self, valid_token: Token):
        """Token timestamps should have UTC timezone."""
        assert valid_token._payload.exp.tzinfo == UTC
        assert valid_token._payload.iat.tzinfo == UTC

    def test_multiple_get_calls_on_valid_token(self, valid_token: Token):
        """Multiple calls to get() on valid token should all succeed."""
        for _ in range(10):
            assert valid_token.get() is not None
