"""Tests for verify module."""

import time

import pytest

from ect import (
    Payload,
    create,
    generate_key,
    CreateOptions,
    parse,
    verify,
    VerifyOptions,
    default_verify_options,
)


def test_parse():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="iss", aud=["a"], iat=now, exp=now + 3600,
        jti="jti-parse", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    parsed = parse(compact)
    assert parsed.payload.jti == "jti-parse"
    assert parsed.raw == compact


def test_default_verify_options():
    opts = default_verify_options()
    assert opts.dag is not None
    assert opts.iat_max_age_sec == 900


def test_verify_expired():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="iss", aud=["v"], iat=now - 3600, exp=now - 60,
        jti="jti-exp", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda kid: key.public_key() if kid == "kid" else None
    with pytest.raises(ValueError, match="expired"):
        verify(compact, VerifyOptions(verifier_id="v", resolve_key=resolver, now=now))


def test_verify_replay():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="iss", aud=["v"], iat=now, exp=now + 3600,
        jti="jti-replay", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda kid: key.public_key() if kid == "kid" else None
    with pytest.raises(ValueError, match="replay"):
        verify(compact, VerifyOptions(
            verifier_id="v", resolve_key=resolver, now=now,
            jti_seen=lambda j: j == "jti-replay",
        ))


def test_verify_invalid_typ():
    import jwt as jwt_lib
    with pytest.raises((ValueError, jwt_lib.exceptions.DecodeError)):
        verify("not-a-jws", VerifyOptions())


def test_verify_audience_mismatch():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="iss", aud=["other"], iat=now, exp=now + 3600,
        jti="jti-a", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda kid: key.public_key() if kid == "kid" else None
    with pytest.raises(ValueError, match="audience"):
        verify(compact, VerifyOptions(verifier_id="verifier", resolve_key=resolver, now=now))


def test_verify_wit_subject_mismatch():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="wrong-iss", aud=["v"], iat=now, exp=now + 3600,
        jti="jti-w", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda kid: key.public_key() if kid == "kid" else None
    with pytest.raises(ValueError, match="WIT subject"):
        verify(compact, VerifyOptions(
            verifier_id="v", resolve_key=resolver, now=now, wit_subject="correct-iss",
        ))


def test_verify_iat_too_old():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="iss", aud=["v"], iat=now - 2000, exp=now + 3600,
        jti="jti-old", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda kid: key.public_key() if kid == "kid" else None
    with pytest.raises(ValueError, match="iat"):
        verify(compact, VerifyOptions(
            verifier_id="v", resolve_key=resolver, now=now, iat_max_age_sec=900,
        ))


def test_verify_unknown_key():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="iss", aud=["v"], iat=now, exp=now + 3600,
        jti="jti-k", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda kid: None  # unknown key
    with pytest.raises(ValueError, match="unknown key"):
        verify(compact, VerifyOptions(verifier_id="v", resolve_key=resolver, now=now))


def test_verify_resolve_key_required():
    key = generate_key()
    now = int(time.time())
    p = Payload(
        iss="iss", aud=["v"], iat=now, exp=now + 3600,
        jti="jti-r", exec_act="act", pred=[],
    )
    compact = create(p, key, CreateOptions(key_id="kid"))
    with pytest.raises(ValueError, match="ResolveKey"):
        verify(compact, VerifyOptions(verifier_id="v", resolve_key=None))


def test_verify_with_dag():
    from ect import MemoryLedger
    key = generate_key()
    ledger = MemoryLedger()
    now = int(time.time())
    root = Payload(
        iss="iss", aud=["v"], iat=now, exp=now + 3600,
        jti="jti-root", exec_act="act", pred=[],
    )
    compact_root = create(root, key, CreateOptions(key_id="kid"))
    resolver = lambda kid: key.public_key() if kid == "kid" else None
    opts = VerifyOptions(verifier_id="v", resolve_key=resolver, store=ledger, now=now)
    parsed = verify(compact_root, opts)
    ledger.append(compact_root, parsed.payload)
    child = Payload(
        iss="iss", aud=["v"], iat=now + 1, exp=now + 3600,
        jti="jti-child", exec_act="act2", pred=["jti-root"],
    )
    compact_child = create(child, key, CreateOptions(key_id="kid"))
    parsed2 = verify(compact_child, opts)
    assert parsed2.payload.jti == "jti-child"


def test_on_verify_attempt_callback():
    """Observability: on_verify_attempt is called with jti and error (or None)."""
    key = generate_key()
    now = int(time.time())
    p = Payload(iss="i", aud=["v"], iat=now, exp=now + 3600, jti="jti-obs", exec_act="a", pred=[])
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda k: key.public_key() if k == "kid" else None
    seen = []
    def hook(jti, err):
        seen.append((jti, err))
    opts = VerifyOptions(verifier_id="v", resolve_key=resolver, on_verify_attempt=hook)
    result = verify(compact, opts)
    assert result.payload.jti == "jti-obs"
    assert len(seen) == 1
    assert seen[0][0] == "jti-obs"
    assert seen[0][1] is None


def test_on_verify_attempt_called_on_failure():
    key = generate_key()
    now = int(time.time())
    p = Payload(iss="i", aud=["v"], iat=now, exp=now - 1, jti="jti-fail", exec_act="a", pred=[])
    compact = create(p, key, CreateOptions(key_id="kid"))
    resolver = lambda k: key.public_key() if k == "kid" else None
    seen = []
    opts = VerifyOptions(verifier_id="v", resolve_key=resolver, now=now, on_verify_attempt=lambda jti, err: seen.append((jti, err)))
    with pytest.raises(ValueError, match="expired"):
        verify(compact, opts)
    assert len(seen) == 1
    assert seen[0][0] == "jti-fail"
    assert seen[0][1] is not None
