"""
SEC-03: Constant-time comparison for token/code verification.

Two layers of verification:
  1. Source-code inspection: confirm hmac.compare_digest is used in auth service.
  2. Behavioral tests: confirm that wrong tokens/codes are rejected correctly
     (proving the comparison pathway functions at minimum).

The behavioral tests do not prove timing properties (impossible without
statistical measurement), but they prove the reject-path works — a necessary
condition for constant-time implementations to be meaningful.
"""
from __future__ import annotations

import inspect
import os
import re

import pytest


# ── Source inspection ─────────────────────────────────────────────────────────

def test_hmac_compare_digest_used_in_hibp_check():
    """SEC-03: auth service uses hmac.compare_digest for HIBP suffix comparison.

    The HIBP check compares SHA-1 suffixes; using == would leak timing information
    about whether the prefix matches. hmac.compare_digest is the required approach.
    """
    auth_path = os.path.join(
        os.path.dirname(__file__), "..", "services", "auth.py"
    )
    with open(auth_path) as f:
        source = f.read()

    assert "hmac.compare_digest" in source, (
        "SEC-03 VIOLATED: 'hmac.compare_digest' not found in services/auth.py. "
        "All security-sensitive comparisons must use constant-time comparison."
    )


def test_no_plain_equality_for_suffix_comparison():
    """SEC-03: The HIBP check must not use plain == to compare hash suffixes.

    A plain == comparison leaks timing information — rejected by SEC-03.
    The implementation must use hmac.compare_digest.
    """
    auth_path = os.path.join(
        os.path.dirname(__file__), "..", "services", "auth.py"
    )
    with open(auth_path) as f:
        source = f.read()

    # Detect patterns like: candidate_suffix == suffix or suffix == candidate_suffix
    # These would be in the HIBP check or token comparison area
    # We allow == for simple type/key checks but not for suffix/token/code comparisons
    # Look specifically for suffix comparison using ==
    dangerous_patterns = re.findall(
        r'candidate_suffix\s*==\s*\w+|suffix\s*==\s*candidate_suffix',
        source
    )
    assert len(dangerous_patterns) == 0, (
        f"SEC-03 VIOLATED: Plain equality (==) used for suffix comparison: "
        f"{dangerous_patterns}. Must use hmac.compare_digest."
    )


# ── Behavioral: verify_password ───────────────────────────────────────────────

def test_verify_password_correct_password_returns_true_constant_time():
    """SEC-03: verify_password returns True for correct password."""
    from services.auth import hash_password, verify_password

    h = hash_password("Correct1Password!")
    result = verify_password("Correct1Password!", h)
    assert result is True, (
        "verify_password should return True for the correct password"
    )


def test_verify_password_wrong_password_returns_false_constant_time():
    """SEC-03: verify_password returns False for incorrect password.

    This behavioral test confirms the rejection path works. The underlying
    pwdlib uses constant-time Argon2 verification.
    """
    from services.auth import hash_password, verify_password

    h = hash_password("Correct1Password!")
    result = verify_password("WrongPassword1!", h)
    assert result is False, (
        "SEC-03: verify_password must return False for wrong password — "
        "the comparison pathway is broken if this fails."
    )


def test_verify_password_empty_wrong_returns_false():
    """SEC-03: verify_password returns False for empty string against real hash."""
    from services.auth import hash_password, verify_password

    h = hash_password("ActualPassword1!")
    result = verify_password("", h)
    assert result is False


def test_verify_password_against_invalid_hash_returns_false():
    """SEC-03: verify_password returns False (not exception) for a malformed hash."""
    from services.auth import verify_password

    # verify_password must never raise — it catches exceptions and returns False
    result = verify_password("anything", "not-a-valid-argon2-hash")
    assert result is False, (
        "verify_password must return False on exception, not propagate it"
    )


# ── Behavioral: verify_backup_code ───────────────────────────────────────────

@pytest.mark.asyncio
async def test_backup_code_verification_is_constant_time(db_session):
    """SEC-03: verify_backup_code checks ALL rows (no early exit on match).

    The implementation must iterate every unused backup code even after finding
    a match, to prevent timing-based enumeration of which position matched.
    This test checks the behavioral correctness of the implementation.
    """
    import uuid as _uuid
    from db.models import User, Quota
    from services.auth import (
        generate_backup_codes,
        hash_password,
        store_backup_codes,
        verify_backup_code,
    )
    from sqlalchemy import select
    from db.models import BackupCode

    user_id = _uuid.uuid4()
    user = User(
        id=user_id,
        handle=f"ct_user_{user_id.hex[:6]}",
        email=f"ct_{user_id.hex[:6]}@example.com",
        password_hash=hash_password("ValidP@ss1!"),
        role="user",
        is_active=True,
        password_must_change=False,
    )
    quota = Quota(user_id=user_id, limit_bytes=104857600, used_bytes=0)
    db_session.add(user)
    db_session.add(quota)
    await db_session.commit()

    codes = generate_backup_codes(10)
    await store_backup_codes(db_session, user_id, codes)

    # Verify: correct code returns True
    result = await verify_backup_code(db_session, user_id, codes[0])
    assert result is True, "verify_backup_code must return True for a valid backup code"

    # Verify: SAME code second time returns False (marked used)
    result2 = await verify_backup_code(db_session, user_id, codes[0])
    assert result2 is False, (
        "verify_backup_code must return False when the code has already been used"
    )

    # Verify: wrong code returns False
    result3 = await verify_backup_code(db_session, user_id, "XXXXXXXX")
    assert result3 is False, (
        "verify_backup_code must return False for a code that was never issued"
    )

    # Verify: after first code is consumed, remaining codes still work (independent rows)
    result4 = await verify_backup_code(db_session, user_id, codes[1])
    assert result4 is True, (
        "verify_backup_code must still validate unused backup codes after another was consumed"
    )

    # Confirm the implementation inspects source — iterates all rows
    auth_path = os.path.join(
        os.path.dirname(__file__), "..", "services", "auth.py"
    )
    with open(auth_path) as f:
        source = f.read()

    assert "matched_row" in source and "keep iterating" in source, (
        "SEC-03: verify_backup_code should iterate all rows even after matching "
        "(constant-time invariant). Implementation comment 'keep iterating' not found."
    )
