"""Token expiry parsing and the on-disk cache.

The rule these pin: expiry comes from the JWT's own `exp` when readable, and
`ttlSeconds` is a fallback only — the authentication API documents that field as
ignored on initial authentication, where it arrives as "0".
"""

import time

import pytest

from a360_mcp.auth._paths import account_key
from a360_mcp.auth._token_store import Token, TokenStore
from a360_mcp.utils.config import DEFAULT_TOKEN_TTL_SECONDS, TOKEN_REFRESH_SKEW_SECONDS

KEY = account_key("https://cr.example.com", "svc_mcp")


class TestTokenExpiry:
    def test_prefers_jwt_exp_over_ttl(self, make_jwt):
        token = Token.from_auth_response(
            {"token": make_jwt(5_000.0), "ttlSeconds": "1200"}, username="svc_mcp", now=0.0
        )
        assert token.expires_at == 5_000.0

    def test_falls_back_to_ttl_seconds_when_jwt_unreadable(self):
        token = Token.from_auth_response(
            {"token": "not-a-jwt", "ttlSeconds": "600"}, username="svc_mcp", now=1_000.0
        )
        assert token.expires_at == 1_600.0

    def test_ignores_zero_ttl_from_initial_authentication(self):
        # Initial auth returns ttlSeconds "0"; it is documented as meaningless there.
        token = Token.from_auth_response(
            {"token": "not-a-jwt", "ttlSeconds": "0"}, username="svc_mcp", now=0.0
        )
        assert token.expires_at == float(DEFAULT_TOKEN_TTL_SECONDS)

    def test_falls_back_when_ttl_missing(self):
        token = Token.from_auth_response({"token": "not-a-jwt"}, username="svc_mcp", now=0.0)
        assert token.expires_at == float(DEFAULT_TOKEN_TTL_SECONDS)

    def test_jwt_without_exp_claim_falls_back(self, make_jwt):
        token = Token.from_auth_response(
            {"token": make_jwt(None), "ttlSeconds": "300"}, username="svc_mcp", now=0.0
        )
        assert token.expires_at == 300.0

    def test_missing_token_field_raises(self):
        with pytest.raises(ValueError, match="did not contain a 'token' field"):
            Token.from_auth_response({"ttlSeconds": "1200"}, username="svc_mcp")


class TestIsExpired:
    def make(self, expires_at: float) -> Token:
        return Token(value="v", expires_at=expires_at, username="svc_mcp")

    def test_fresh_token_is_not_expired(self):
        assert self.make(10_000.0).is_expired(now=0.0) is False

    def test_token_past_expiry_is_expired(self):
        assert self.make(100.0).is_expired(now=200.0) is True

    def test_renews_early_by_the_skew_window(self):
        # Waiting for a 401 costs a round trip and, because refresh kills the old
        # token, turns every in-flight request into a retry.
        expires_at = 1_000.0
        just_inside = expires_at - TOKEN_REFRESH_SKEW_SECONDS + 1
        assert self.make(expires_at).is_expired(now=just_inside) is True

    def test_outside_skew_window_is_still_valid(self):
        expires_at = 1_000.0
        outside = expires_at - TOKEN_REFRESH_SKEW_SECONDS - 1
        assert self.make(expires_at).is_expired(now=outside) is False


class TestTokenStore:
    def test_round_trips_a_token(self):
        store = TokenStore(KEY)
        token = Token(value="abc", expires_at=time.time() + 600, username="svc_mcp")
        store.save(token)
        assert store.load() == token

    def test_load_returns_none_when_absent(self):
        assert TokenStore(KEY).load() is None

    def test_load_returns_none_on_corrupt_file(self):
        store = TokenStore(KEY)
        store.path.parent.mkdir(parents=True, exist_ok=True)
        store.path.write_text("{broken", encoding="utf-8")
        assert store.load() is None

    def test_saved_token_file_is_owner_only(self):
        store = TokenStore(KEY)
        store.save(Token(value="abc", expires_at=1.0, username="svc_mcp"))
        assert (store.path.stat().st_mode & 0o777) == 0o600

    def test_clear_removes_the_cache(self):
        store = TokenStore(KEY)
        store.save(Token(value="abc", expires_at=1.0, username="svc_mcp"))
        store.clear()
        assert store.load() is None

    def test_clear_is_safe_when_absent(self):
        TokenStore(KEY).clear()  # no raise


class TestAccountKey:
    def test_same_inputs_give_same_key(self):
        assert account_key("https://cr.example.com", "svc_mcp") == account_key(
            "https://cr.example.com/", "SVC_MCP"
        )

    def test_different_users_give_different_keys(self):
        assert account_key("https://cr.example.com", "a") != account_key(
            "https://cr.example.com", "b"
        )

    def test_different_control_rooms_give_different_keys(self):
        assert account_key("https://a.example.com", "u") != account_key(
            "https://b.example.com", "u"
        )
