"""Unit tests for the adapter-agnostic dispatch core."""

import asyncio

import pytest
from pydantic import BaseModel

from mizan_core.auth import AuthConfig, JWTConfig, INVALID, authenticate, create_access_token
from mizan_core.authguard import enforce_auth
from mizan_core.client.function import client
from mizan_core.dispatch import CacheOrchestrator, DispatchRequest, dispatch_call
from mizan_core.errors import Forbidden, Unauthorized
from mizan_core.invalidation import format_invalidate_header, resolve_invalidation
from mizan_core.registry import clear_registry, register


class Ident:
    def __init__(self, authed=True, staff=False, su=False, pk=1):
        self.is_authenticated = authed
        self.is_staff = staff
        self.is_superuser = su
        self.pk = pk


# ─── authguard ──────────────────────────────────────────────────────────────


def test_auth_required_anonymous():
    with pytest.raises(Unauthorized):
        enforce_auth(None, True)


def test_auth_required_authenticated():
    enforce_auth(Ident(), True)  # no raise


def test_auth_staff_denied_then_allowed():
    with pytest.raises(Forbidden):
        enforce_auth(Ident(staff=False), "staff")
    enforce_auth(Ident(staff=True), "staff")


def test_auth_superuser():
    with pytest.raises(Forbidden):
        enforce_auth(Ident(su=False), "superuser")
    enforce_auth(Ident(su=True), "superuser")


def test_auth_callable_false_and_raise():
    with pytest.raises(Forbidden):
        enforce_auth(Ident(), lambda r: False)
    with pytest.raises(Forbidden, match="custom"):
        enforce_auth(Ident(), lambda r: (_ for _ in ()).throw(PermissionError("custom")))


# ─── authenticate / INVALID sentinel ────────────────────────────────────────


def _cfg():
    return AuthConfig(jwt=JWTConfig(private_key="k" * 32, public_key="k" * 32))


def test_authenticate_jwt_ok():
    cfg = _cfg()
    tok = create_access_token("7", "sess", cfg.jwt, is_staff=True)
    ident = authenticate({"Authorization": f"Bearer {tok}"}, cfg)
    assert ident.pk == 7 and ident.is_staff and ident.is_authenticated


def test_authenticate_bad_token_is_invalid_sentinel():
    assert authenticate({"Authorization": "Bearer garbage"}, _cfg()) is INVALID


def test_authenticate_no_token_is_none():
    assert authenticate({}, _cfg()) is None


# ─── invalidation + header ──────────────────────────────────────────────────


def test_invalidation_three_tier_and_header():
    clear_registry()
    UserCtx = "user"

    class Out(BaseModel):
        ok: bool

    @client(context=UserCtx)
    def user_profile(request, user_id: int) -> Out:
        return Out(ok=True)

    @client(affects=UserCtx)
    def update_profile(request, user_id: int, name: str) -> Out:
        return Out(ok=True)

    register(user_profile, "user_profile")
    register(update_profile, "update_profile")

    # Tier 1: user_id matches context param → scoped
    inv = resolve_invalidation(update_profile, {"user_id": 5, "name": "x"})
    assert inv == [{"context": "user", "params": {"user_id": 5}}]
    assert format_invalidate_header(inv) == "user;user_id=5"

    # Tier 3: no matching param → broad
    inv2 = resolve_invalidation(update_profile, {"name": "x"})
    assert inv2 == ["user"]
    clear_registry()


# ─── dispatch_call end to end ───────────────────────────────────────────────


def test_dispatch_call_auth_and_invalidation():
    clear_registry()

    class Out(BaseModel):
        ok: bool

    @client(context="user")
    def user_profile(request, user_id: int) -> Out:
        return Out(ok=True)

    @client(affects="user", auth="staff")
    def secure_update(request, user_id: int) -> Out:
        return Out(ok=True)

    register(user_profile, "user_profile")
    register(secure_update, "secure_update")

    cache = CacheOrchestrator(None, None)

    # non-staff rejected
    with pytest.raises(Forbidden):
        asyncio.run(dispatch_call(
            DispatchRequest(identity=Ident(staff=False), args={"user_id": 1}),
            "secure_update", cache,
        ))

    # staff passes, invalidation resolved
    res = asyncio.run(dispatch_call(
        DispatchRequest(identity=Ident(staff=True), args={"user_id": 1}),
        "secure_update", cache,
    ))
    assert res.kind == "rpc" and res.data == {"ok": True}
    assert res.invalidate == [{"context": "user", "params": {"user_id": 1}}]
    assert res.invalidate_header == "user;user_id=1"
    clear_registry()
