from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from app.core import jwt_denylist as denylist_module
from app.core.jwt_denylist import (
    bump_auth_epoch,
    deny_jti,
    ensure_jwt_revocation_backend,
    get_auth_epoch,
    is_jti_denied,
    revoke_access_token,
    validate_access_claims,
)
from app.core.security import create_access_token, hash_password
from app.modules.users.repository import create_user


def test_auth_epoch_bump_invalidates_token():
    user = create_user("epoch-user@example.com", hash_password("Valid123"), status="active")
    token = create_access_token(user.id, user.role, user.is_superuser)
    from app.core.security import decode_access_token

    payload = decode_access_token(token)
    validate_access_claims(payload)

    bump_auth_epoch(user.id)
    with pytest.raises(ValueError, match="TOKEN_REVOKED"):
        validate_access_claims(payload)


def test_deny_jti_blocks_specific_token():
    user = create_user("jti-user@example.com", hash_password("Valid123"), status="active")
    token = create_access_token(user.id, user.role, user.is_superuser)
    from app.core.security import decode_access_token

    payload = decode_access_token(token)
    deny_jti(payload["jti"], payload["exp"])
    assert is_jti_denied(payload["jti"])
    with pytest.raises(ValueError, match="TOKEN_REVOKED"):
        validate_access_claims(payload)


def test_revoke_access_token_helper():
    user = create_user("revoke-user@example.com", hash_password("Valid123"), status="active")
    token = create_access_token(user.id, user.role, user.is_superuser)
    revoke_access_token(token)
    from app.core.security import decode_access_token

    with pytest.raises(ValueError, match="TOKEN_REVOKED"):
        validate_access_claims(decode_access_token(token))


def test_get_auth_epoch_defaults_to_zero():
    assert get_auth_epoch("missing-user-id") == 0


def test_validate_access_claims_rejects_missing_sub():
    with pytest.raises(ValueError, match="INVALID_TOKEN"):
        validate_access_claims({})


def test_production_is_jti_denied_fail_closed(monkeypatch):
    monkeypatch.setattr(denylist_module.settings, "app_env", "production")
    mock_client = MagicMock()
    mock_client.exists.side_effect = RuntimeError("redis down")
    with patch("app.core.jwt_denylist.get_redis_client", return_value=mock_client):
        assert is_jti_denied("any-jti") is True


def test_revoke_access_token_ignores_invalid_token():
    revoke_access_token("not-a-jwt")


def test_bump_auth_epoch_production_raises_when_redis_fails(monkeypatch):
    monkeypatch.setattr(denylist_module.settings, "app_env", "production")
    mock_client = MagicMock()
    mock_client.incr.side_effect = RuntimeError("redis down")
    with patch("app.core.jwt_denylist.get_redis_client", return_value=mock_client):
        with pytest.raises(RuntimeError):
            bump_auth_epoch("user-x")


def test_empty_jti_is_not_denied():
    assert is_jti_denied("") is False
    deny_jti("", 9999999999)


def test_ensure_jwt_revocation_backend_requires_redis_in_production(monkeypatch):
    monkeypatch.setattr(denylist_module.settings, "app_env", "production")
    with patch("app.core.jwt_denylist.get_redis_client", return_value=None):
        with pytest.raises(RuntimeError, match="Redis is required"):
            ensure_jwt_revocation_backend()


def test_get_auth_epoch_production_raises_when_redis_fails(monkeypatch):
    monkeypatch.setattr(denylist_module.settings, "app_env", "production")
    mock_client = MagicMock()
    mock_client.get.side_effect = RuntimeError("redis down")
    with patch("app.core.jwt_denylist.get_redis_client", return_value=mock_client):
        with pytest.raises(RuntimeError):
            get_auth_epoch("user-y")


def test_deny_jti_production_raises_when_redis_fails(monkeypatch):
    monkeypatch.setattr(denylist_module.settings, "app_env", "production")
    mock_client = MagicMock()
    mock_client.setex.side_effect = RuntimeError("redis down")
    with patch("app.core.jwt_denylist.get_redis_client", return_value=mock_client):
        with pytest.raises(RuntimeError):
            deny_jti("jti-123", int(__import__("time").time()) + 3600)


def test_memory_denylist_purges_expired_jti(monkeypatch):
    monkeypatch.setattr(denylist_module, "_memory_denied_jti", {"expired-jti": 1.0})
    assert is_jti_denied("expired-jti") is False

