import json
import time

import pytest

from andrea_svc_auth.core import OIDCValidator, authenticate, verify_api_key


def hg(d):
    return lambda h: d.get(h, "")


# ── API-key gate ────────────────────────────────────────────────────────────
def test_verify_api_key_constant_time_and_failclosed():
    assert verify_api_key("s3cret", "s3cret") is True
    assert verify_api_key("s3cret", "nope") is False
    assert verify_api_key("", "s3cret") is False
    assert verify_api_key("s3cret", "") is False     # unconfigured never authenticates
    assert verify_api_key("", "") is False


def test_service_key_single_and_map():
    r = authenticate(hg({"X-Agent-Key": "k1"}), service_keys="k1")
    assert r.ok and r.principal.kind == "service" and r.principal.id == "default"
    r2 = authenticate(hg({"X-Agent-Key": "bb"}), service_keys={"alice": "aa", "bob": "bb"})
    assert r2.ok and r2.principal.id == "bob"


def test_wrong_key_is_403_not_anonymous():
    r = authenticate(hg({"X-Agent-Key": "wrong"}), service_keys="k1")
    assert not r.ok and r.status == 403            # presented-but-wrong != anonymous


def test_no_credential_is_401():
    r = authenticate(hg({}), service_keys="k1")
    assert not r.ok and r.status == 401 and r.reason == "no credential"


def test_empty_config_failclosed():
    # a key is presented but the service configured none -> deny, never allow
    r = authenticate(hg({"X-Agent-Key": "anything"}), service_keys=None)
    assert not r.ok and r.status == 403


# ── user/OIDC path (combiner with a stub validator) ─────────────────────────
class _StubOIDC:
    def __init__(self, claims):
        self._c = claims

    def validate(self, token):
        if token == "bad":
            raise ValueError("expired")
        return self._c


def test_bearer_user():
    oidc = _StubOIDC({"sub": "u-123", "preferred_username": "alice"})
    r = authenticate(hg({"Authorization": "Bearer good"}), oidc=oidc)
    assert r.ok and r.principal.kind == "user" and r.principal.id == "u-123"
    assert r.principal.claims["username"] == "alice" and r.principal.token == "good"


def test_bad_bearer_denied_401():
    r = authenticate(hg({"Authorization": "Bearer bad"}), oidc=_StubOIDC({}))
    assert not r.ok and r.status == 401


def test_api_key_precedes_bearer():
    r = authenticate(hg({"X-Agent-Key": "k1", "Authorization": "Bearer good"}),
                     service_keys="k1", oidc=_StubOIDC({"sub": "x"}))
    assert r.principal.kind == "service"


def test_bearer_without_oidc_configured_denied():
    r = authenticate(hg({"Authorization": "Bearer good"}), oidc=None)
    assert not r.ok and r.reason == "no credential"


# ── real RS256 + JWKS round-trip (skips if PyJWT/cryptography absent) ────────
def test_oidc_validator_roundtrip(monkeypatch):
    jwt = pytest.importorskip("jwt")
    pytest.importorskip("cryptography")
    from cryptography.hazmat.primitives.asymmetric import rsa

    key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    pub_jwk = json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(key.public_key()))
    pub_jwk.update({"kid": "kid1", "alg": "RS256", "use": "sig"})

    v = OIDCValidator(issuer="https://kc/realms/andrea", jwks_url="https://kc/jwks")

    def fake_refresh():
        v._jwks = {"keys": [pub_jwk]}
        v._fetched_at = time.time()
    monkeypatch.setattr(v, "_refresh", fake_refresh)

    good = jwt.encode({"sub": "u9", "iss": "https://kc/realms/andrea",
                       "exp": int(time.time()) + 60}, key, algorithm="RS256",
                      headers={"kid": "kid1"})
    assert v.validate(good)["sub"] == "u9"

    expired = jwt.encode({"sub": "u9", "iss": "https://kc/realms/andrea",
                          "exp": int(time.time()) - 60}, key, algorithm="RS256",
                         headers={"kid": "kid1"})
    with pytest.raises(Exception):
        v.validate(expired)

    wrong_iss = jwt.encode({"sub": "u9", "iss": "https://evil/realms/x",
                            "exp": int(time.time()) + 60}, key, algorithm="RS256",
                           headers={"kid": "kid1"})
    with pytest.raises(Exception):
        v.validate(wrong_iss)
