"""Tests for password hashing and JWT helpers."""

from __future__ import annotations

import asyncio
from datetime import UTC, datetime, timedelta

import jwt
import pytest

from aura_auth._core.exceptions import InvalidPasswordError, InvalidTokenError, TokenExpiredError
from aura_auth._core.security import PasswordHelper, TokenHelper, generate_random_token

# PyJWT warns (and pytest treats warnings as errors) if HMAC secrets are shorter than 32 bytes.
_JWT_KEY = "0" * 32
_JWT_KEY_A = "1" * 32
_JWT_KEY_B = "2" * 32


def test_password_hash_format() -> None:
    helper = PasswordHelper()
    hashed = helper.hash("correct horse battery staple")
    assert hashed.startswith("$2b$")


def test_password_verify_success() -> None:
    helper = PasswordHelper()
    hashed = helper.hash("secret-password")
    assert helper.verify("secret-password", hashed) is True


def test_password_verify_failure() -> None:
    helper = PasswordHelper()
    hashed = helper.hash("secret-password")
    assert helper.verify("wrong-password", hashed) is False


def test_password_hashes_use_unique_salts() -> None:
    helper = PasswordHelper()
    first = helper.hash("same-password")
    second = helper.hash("same-password")
    assert first != second


def test_password_validate_strength_short_raises() -> None:
    helper = PasswordHelper(min_length=8)
    with pytest.raises(InvalidPasswordError):
        helper.validate_strength("short")


def test_password_validate_strength_ok() -> None:
    PasswordHelper().validate_strength("longenough")


def test_password_empty_rejected() -> None:
    with pytest.raises(InvalidPasswordError):
        PasswordHelper().validate_strength("")


def test_token_roundtrip_sub() -> None:
    helper = TokenHelper(_JWT_KEY, lifetime_seconds=3600)
    token = helper.create_token("user-123")
    payload = helper.decode_token(token)
    assert payload["sub"] == "user-123"


def test_token_expired_raises() -> None:
    helper = TokenHelper(_JWT_KEY, lifetime_seconds=-3600)
    token = helper.create_token("user-123")
    with pytest.raises(TokenExpiredError):
        helper.decode_token(token)


def test_token_tampered_raises() -> None:
    helper = TokenHelper(_JWT_KEY, lifetime_seconds=3600)
    token = helper.create_token("user-123")
    tampered = token[:-3] + "xxx"
    with pytest.raises(InvalidTokenError):
        helper.decode_token(tampered)


def test_token_wrong_secret_raises() -> None:
    helper = TokenHelper(_JWT_KEY_A, lifetime_seconds=3600)
    token = helper.create_token("user-123")
    other = TokenHelper(_JWT_KEY_B, lifetime_seconds=3600)
    with pytest.raises(InvalidTokenError):
        other.decode_token(token)


def test_token_extra_claims_present() -> None:
    helper = TokenHelper(_JWT_KEY, lifetime_seconds=3600)
    token = helper.create_token("user-123", extra_claims={"role": "admin"})
    payload = helper.decode_token(token)
    assert payload["role"] == "admin"


def test_token_jti_unique() -> None:
    helper = TokenHelper(_JWT_KEY, lifetime_seconds=3600)
    first = helper.decode_token(helper.create_token("user-1"))
    second = helper.decode_token(helper.create_token("user-1"))
    assert first["jti"] != second["jti"]


def test_token_missing_sub_raises() -> None:
    token = jwt.encode(
        {
            "iat": int(datetime.now(tz=UTC).timestamp()),
            "exp": int((datetime.now(tz=UTC) + timedelta(hours=1)).timestamp()),
        },
        _JWT_KEY,
        algorithm="HS256",
    )
    helper = TokenHelper(_JWT_KEY, lifetime_seconds=3600)
    with pytest.raises(InvalidTokenError):
        helper.decode_token(token)


def test_create_token_empty_user_id() -> None:
    with pytest.raises(ValueError):
        TokenHelper(_JWT_KEY).create_token("")


def test_generate_random_token_url_safe() -> None:
    token = generate_random_token(16)
    assert "+" not in token and "/" not in token and "=" not in token


def test_generate_random_token_respects_length() -> None:
    token = generate_random_token(8)
    assert len(token) > 0


def test_generate_random_token_unique() -> None:
    first = generate_random_token()
    second = generate_random_token()
    assert first != second


async def test_token_expires_after_delay() -> None:
    helper = TokenHelper(_JWT_KEY, lifetime_seconds=1)
    token = helper.create_token("user-123")
    await asyncio.sleep(1.1)
    with pytest.raises(TokenExpiredError):
        helper.decode_token(token)
