"""
Unit tests for FreeExile Account Management, Anti-Bot Captcha, OAuth, and Email Verification System.
Following Autonomous TDD Closed-Loop standards.
"""

import time
import pytest
from typing import Dict, Any

from server.auth.models import (
    Account,
    AccountStatus,
    Role,
    OAuthProviderType,
    OAuthIdentity,
    AuthTokenPair,
)
from server.auth.crypto import PasswordHasher, TokenService
from server.auth.captcha import (
    CaptchaEngine,
    CaptchaType,
    CaptchaChallenge,
    RateLimiter,
)
from server.auth.email_verification import (
    EmailVerificationService,
    MockEmailSender,
    VerificationCode,
)
from server.auth.oauth import (
    OAuthService,
    GoogleOAuthProvider,
    AppleOAuthProvider,
    FacebookOAuthProvider,
    OAuthUserProfile,
)
from server.auth.auth_service import AuthService, AccountRepository


class TestPasswordAndCrypto:
    def test_password_hashing_and_verification(self) -> None:
        hasher = PasswordHasher()
        raw_pwd = "SavageExile!2026_Secure"
        hashed = hasher.hash_password(raw_pwd)

        assert hashed != raw_pwd
        assert hashed.startswith("pbkdf2_sha256$")
        assert hasher.verify_password(raw_pwd, hashed) is True
        assert hasher.verify_password("WrongPassword123", hashed) is False

    def test_jwt_token_generation_and_validation(self) -> None:
        token_service = TokenService(secret_key="secret_test_key_freeexile_2026_super_secure_32bytes")
        token_pair = token_service.generate_token_pair(
            account_id="acc_001",
            roles=["player"],
            email="warrior@freeexile.io",
        )

        assert token_pair.access_token is not None
        assert token_pair.refresh_token is not None
        assert token_pair.expires_in_seconds == 900  # 15 mins

        claims = token_service.verify_access_token(token_pair.access_token)
        assert claims is not None
        assert claims["sub"] == "acc_001"
        assert claims["email"] == "warrior@freeexile.io"
        assert "player" in claims["roles"]

    def test_token_revocation_blacklist(self) -> None:
        token_service = TokenService(secret_key="secret_test_key_freeexile_2026_super_secure_32bytes")
        token_pair = token_service.generate_token_pair(
            account_id="acc_002",
            roles=["player"],
            email="hunter@freeexile.io",
        )

        token_service.revoke_token(token_pair.access_token)
        assert token_service.is_token_revoked(token_pair.access_token) is True
        claims = token_service.verify_access_token(token_pair.access_token)
        assert claims is None


class TestAntiBotCaptchaEngine:
    def test_math_captcha_generation_and_verification(self) -> None:
        engine = CaptchaEngine(hmac_secret="captcha_test_secret_123")
        challenge = engine.generate_math_challenge()

        assert challenge.challenge_id is not None
        assert challenge.captcha_type == CaptchaType.MATH
        assert "+" in challenge.question or "-" in challenge.question or "*" in challenge.question
        assert challenge.signature is not None

        # Calculate correct answer
        parts = challenge.question.split()
        a, op, b = int(parts[0]), parts[1], int(parts[2])
        ans = a + b if op == "+" else (a - b if op == "-" else a * b)

        # Successful verification
        is_valid, msg = engine.verify_math_challenge(
            challenge_id=challenge.challenge_id,
            user_answer=str(ans),
            signature=challenge.signature,
            expires_at=challenge.expires_at_ms,
        )
        assert is_valid is True
        assert msg == "OK"

        # Replay attack prevention (single-use challenge)
        is_valid_replay, msg_replay = engine.verify_math_challenge(
            challenge_id=challenge.challenge_id,
            user_answer=str(ans),
            signature=challenge.signature,
            expires_at=challenge.expires_at_ms,
        )
        assert is_valid_replay is False
        assert "already used" in msg_replay.lower()

    def test_math_captcha_wrong_answer_or_tampering(self) -> None:
        engine = CaptchaEngine(hmac_secret="captcha_test_secret_123")
        challenge = engine.generate_math_challenge()

        # Wrong answer
        is_valid, msg = engine.verify_math_challenge(
            challenge_id=challenge.challenge_id,
            user_answer="999999",
            signature=challenge.signature,
            expires_at=challenge.expires_at_ms,
        )
        assert is_valid is False
        assert "incorrect" in msg.lower()

        # Tampered signature
        is_valid_tamper, msg_tamper = engine.verify_math_challenge(
            challenge_id=challenge.challenge_id,
            user_answer="10",
            signature="tampered_fake_signature",
            expires_at=challenge.expires_at_ms,
        )
        assert is_valid_tamper is False
        assert "signature" in msg_tamper.lower()

    def test_proof_of_work_challenge(self) -> None:
        engine = CaptchaEngine(hmac_secret="captcha_test_secret_123")
        # Difficulty 2 = 2 leading zeros (fast in test)
        pow_challenge = engine.generate_pow_challenge(difficulty=2)
        assert pow_challenge.captcha_type == CaptchaType.POW

        # Solve PoW
        nonce = engine.solve_pow(pow_challenge.seed, difficulty=2)
        assert nonce is not None

        # Verify PoW
        is_valid, msg = engine.verify_pow_challenge(
            seed=pow_challenge.seed,
            nonce=nonce,
            difficulty=2,
            signature=pow_challenge.signature,
            expires_at=pow_challenge.expires_at_ms,
        )
        assert is_valid is True
        assert msg == "OK"

    def test_rate_limiter_sliding_window(self) -> None:
        limiter = RateLimiter(max_requests=3, window_seconds=10.0)
        client_ip = "192.168.1.50"

        assert limiter.is_allowed(client_ip) is True
        assert limiter.is_allowed(client_ip) is True
        assert limiter.is_allowed(client_ip) is True
        # 4th request within window must be blocked
        assert limiter.is_allowed(client_ip) is False


class TestEmailVerificationService:
    def test_code_generation_dispatch_and_activation(self) -> None:
        sender = MockEmailSender()
        service = EmailVerificationService(
            email_sender=sender,
            code_ttl_seconds=300,
            cooldown_seconds=60,
        )

        email = "exile_survivor@freeexile.io"
        code_info = service.send_activation_email(email=email, account_id="acc_999")
        assert len(code_info.code) == 6
        assert code_info.code.isdigit()
        assert len(sender.sent_messages) == 1
        assert code_info.code in sender.sent_messages[0]["body"]

        # Verify correct OTP
        is_valid, msg = service.verify_code(email, code_info.code)
        assert is_valid is True
        assert msg == "OK"

        # Code is consumed and cannot be reused
        is_valid_reused, _ = service.verify_code(email, code_info.code)
        assert is_valid_reused is False

    def test_resend_cooldown_enforcement(self) -> None:
        sender = MockEmailSender()
        service = EmailVerificationService(
            email_sender=sender,
            code_ttl_seconds=300,
            cooldown_seconds=60,
        )

        email = "cooldown_test@freeexile.io"
        service.send_activation_email(email=email, account_id="acc_100")

        # Immediate resend fails due to cooldown
        can_resend, remaining_cd = service.can_resend(email)
        assert can_resend is False
        assert remaining_cd > 0

    def test_max_failed_attempts_locks_code(self) -> None:
        sender = MockEmailSender()
        service = EmailVerificationService(
            email_sender=sender,
            code_ttl_seconds=300,
            cooldown_seconds=60,
            max_attempts=3,
        )

        email = "lock_test@freeexile.io"
        service.send_activation_email(email=email, account_id="acc_101")

        for _ in range(3):
            service.verify_code(email, "000000")

        # 4th attempt even with correct code is rejected due to max attempts exceeded
        record = service.get_active_record(email)
        assert record is not None
        assert record.failed_attempts >= 3


class TestPluggableOAuthProviders:
    def test_google_provider_staged_verification(self) -> None:
        google_prov = GoogleOAuthProvider(
            client_id="google_client_id_freeexile",
            client_secret="google_secret_freeexile",
        )
        assert google_prov.provider_type == OAuthProviderType.GOOGLE

        auth_url = google_prov.get_authorization_url(state="random_state_123", redirect_uri="https://freeexile.io/oauth/callback")
        assert "accounts.google.com" in auth_url
        assert "client_id=google_client_id_freeexile" in auth_url

        # Staging mock token verification
        user_profile = google_prov.verify_token_or_code(
            token_or_code="mock_google_token_user456",
            redirect_uri="https://freeexile.io/oauth/callback",
        )
        assert user_profile.provider == OAuthProviderType.GOOGLE
        assert user_profile.email_verified is True
        assert user_profile.provider_user_id.startswith("google_")

    def test_apple_provider_staged_verification(self) -> None:
        apple_prov = AppleOAuthProvider(
            team_id="APPLE_TEAM_ID",
            client_id="com.freeexile.ios",
            key_id="KEY_12345",
        )
        assert apple_prov.provider_type == OAuthProviderType.APPLE

        auth_url = apple_prov.get_authorization_url(state="apple_state", redirect_uri="https://freeexile.io/apple/callback")
        assert "appleid.apple.com" in auth_url

        user_profile = apple_prov.verify_token_or_code(
            token_or_code="mock_apple_token_user789",
            redirect_uri="https://freeexile.io/apple/callback",
        )
        assert user_profile.provider == OAuthProviderType.APPLE
        assert user_profile.email_verified is True

    def test_facebook_provider_staged_verification(self) -> None:
        fb_prov = FacebookOAuthProvider(
            app_id="fb_app_id_123",
            app_secret="fb_secret_456",
        )
        assert fb_prov.provider_type == OAuthProviderType.FACEBOOK

        auth_url = fb_prov.get_authorization_url(state="fb_state", redirect_uri="https://freeexile.io/fb/callback")
        assert "facebook.com" in auth_url

        user_profile = fb_prov.verify_token_or_code(
            token_or_code="mock_fb_token_user321",
            redirect_uri="https://freeexile.io/fb/callback",
        )
        assert user_profile.provider == OAuthProviderType.FACEBOOK
        assert user_profile.email_verified is True


class TestAuthServiceIntegration:
    @pytest.fixture
    def auth_system(self) -> AuthService:
        repo = AccountRepository()
        hasher = PasswordHasher()
        token_service = TokenService(secret_key="unit_test_jwt_secret_key_2026_super_secure_32bytes")
        captcha_engine = CaptchaEngine(hmac_secret="captcha_test_secret_123")
        email_sender = MockEmailSender()
        email_service = EmailVerificationService(
            email_sender=email_sender,
            code_ttl_seconds=300,
            cooldown_seconds=60,
        )

        oauth_service = OAuthService(repository=repo, token_service=token_service)
        oauth_service.register_provider(GoogleOAuthProvider("client_g", "secret_g"))
        oauth_service.register_provider(AppleOAuthProvider("team_a", "com.freeexile", "key_a"))
        oauth_service.register_provider(FacebookOAuthProvider("app_fb", "secret_fb"))

        return AuthService(
            repository=repo,
            hasher=hasher,
            token_service=token_service,
            captcha_engine=captcha_engine,
            email_service=email_service,
            oauth_service=oauth_service,
        )

    def test_quick_registration_flow_and_activation(self, auth_system: AuthService) -> None:
        # 1. Request Captcha
        captcha = auth_system.request_captcha(client_ip="127.0.0.1")
        parts = captcha.question.split()
        a, op, b = int(parts[0]), parts[1], int(parts[2])
        ans = a + b if op == "+" else (a - b if op == "-" else a * b)

        # 2. Quick Register
        reg_res = auth_system.quick_register(
            email="wanderer@freeexile.io",
            password="GrimExile#Password2026",
            username="WandererBlade",
            captcha_id=captcha.challenge_id,
            captcha_solution=str(ans),
            client_ip="127.0.0.1",
        )
        assert reg_res.success is True
        assert reg_res.requires_verification is True
        assert reg_res.account_id is not None

        # Verify Account status is PENDING_ACTIVATION
        account = auth_system.repository.get_by_email("wanderer@freeexile.io")
        assert account is not None
        assert account.status == AccountStatus.PENDING_ACTIVATION

        # Login must be rejected before activation
        login_before_act = auth_system.login(
            email="wanderer@freeexile.io",
            password="GrimExile#Password2026",
            client_ip="127.0.0.1",
        )
        assert login_before_act.success is False
        assert "pending activation" in login_before_act.message.lower()

        # 3. Email Verification
        active_code = auth_system.email_service.get_active_record("wanderer@freeexile.io")
        assert active_code is not None

        act_res = auth_system.verify_email(
            email="wanderer@freeexile.io",
            verification_code=active_code.code,
        )
        assert act_res.success is True
        assert act_res.access_token is not None
        assert act_res.refresh_token is not None

        # Verify Account status is now ACTIVE
        account_active = auth_system.repository.get_by_email("wanderer@freeexile.io")
        assert account_active is not None
        assert account_active.status == AccountStatus.ACTIVE

        # 4. Standard Login now succeeds
        login_after_act = auth_system.login(
            email="wanderer@freeexile.io",
            password="GrimExile#Password2026",
            client_ip="127.0.0.1",
        )
        assert login_after_act.success is True
        assert login_after_act.access_token is not None
        assert login_after_act.username == "WandererBlade"

    def test_quick_registration_spam_bot_blocked_by_captcha(self, auth_system: AuthService) -> None:
        # Bot attempts registration with fake captcha solution
        reg_res = auth_system.quick_register(
            email="bot_attacker@freeexile.io",
            password="BotPassword123!",
            username="BotSpammer",
            captcha_id="fake_challenge_id",
            captcha_solution="invalid_answer",
            client_ip="192.168.100.99",
        )
        assert reg_res.success is False
        assert "captcha" in reg_res.message.lower()

        # Ensure no account was created
        account = auth_system.repository.get_by_email("bot_attacker@freeexile.io")
        assert account is None

    def test_oauth_login_and_binding(self, auth_system: AuthService) -> None:
        # OAuth Login for new user automatically creates & activates account
        oauth_res = auth_system.oauth_login(
            provider=OAuthProviderType.GOOGLE,
            auth_code_or_token="mock_google_token_exile_hero",
            redirect_uri="https://freeexile.io/oauth/callback",
            client_ip="127.0.0.1",
        )
        assert oauth_res.success is True
        assert oauth_res.is_new_account is True
        assert oauth_res.access_token is not None

        account = auth_system.repository.get_by_id(oauth_res.account_id)
        assert account is not None
        assert account.status == AccountStatus.ACTIVE
        assert OAuthProviderType.GOOGLE in account.oauth_identities

    def test_refresh_token_rotation(self, auth_system: AuthService) -> None:
        # Create an active account directly
        account = auth_system.repository.create_account(
            email="refreshtest@freeexile.io",
            password_hash=auth_system.hasher.hash_password("Pass123!"),
            username="TokenTester",
            status=AccountStatus.ACTIVE,
        )

        login_res = auth_system.login(
            email="refreshtest@freeexile.io",
            password="Pass123!",
            client_ip="127.0.0.1",
        )
        assert login_res.refresh_token is not None

        # Refresh tokens
        refresh_res = auth_system.refresh_token(login_res.refresh_token)
        assert refresh_res.access_token is not None
        assert refresh_res.refresh_token is not None
        assert refresh_res.refresh_token != login_res.refresh_token  # Rotated!
