"""Shared fixtures for ACT <-> ECT interop tests.

Provides keypairs (ES256 + Ed25519), token builders with overlapping
claims, and a dual key resolver that works for both refimpls.
"""

from __future__ import annotations

import time
import uuid
from typing import Any, Callable

import pytest

from act.crypto import (
    generate_ed25519_keypair,
    generate_p256_keypair,
    sign as act_sign,
)
from act.token import (
    ACTMandate,
    ACTRecord,
    Capability,
    TaskClaim,
    encode_jws,
)
from ect.create import create as ect_create, CreateOptions
from ect.types import Payload


# --- Keypair fixtures ---


@pytest.fixture
def es256_keypair():
    """Shared ES256 (P-256) keypair usable by both ACT and ECT."""
    priv, pub = generate_p256_keypair()
    return priv, pub


@pytest.fixture
def ed25519_keypair():
    """Ed25519 keypair — ACT supports this, ECT does not."""
    priv, pub = generate_ed25519_keypair()
    return priv, pub


@pytest.fixture
def base_time() -> int:
    return int(time.time())


# --- Builders for overlapping claim fixtures ---


def _new_uuid() -> str:
    return str(uuid.uuid4())


@pytest.fixture
def act_record_builder(es256_keypair, base_time):
    """Build a signed Phase 2 ACTRecord compact string (ES256).

    The builder accepts overrides (alg, key, jti, wid, pred, exec_act,
    inp_hash, out_hash, aud) so individual tests can shape the record
    exactly how they want.
    """
    priv_default, _ = es256_keypair

    def _build(
        *,
        alg: str = "ES256",
        priv=None,
        kid: str = "act-key-1",
        jti: str | None = None,
        wid: str | None = None,
        pred: list[str] | None = None,
        exec_act: str = "read.data",
        cap_actions: list[str] | None = None,
        inp_hash: str | None = None,
        out_hash: str | None = None,
        aud: str | list[str] = "agent-b",
        iss: str = "agent-a",
        sub: str = "agent-b",
        iat: int | None = None,
        exp: int | None = None,
        exec_ts: int | None = None,
        status: str = "completed",
        extra_claims: dict[str, Any] | None = None,
    ) -> tuple[str, ACTRecord]:
        iat = iat or base_time
        exp = exp or (base_time + 900)
        exec_ts = exec_ts or base_time
        jti = jti or _new_uuid()
        cap_actions = cap_actions or [exec_act]

        record = ACTRecord(
            alg=alg,
            kid=kid,
            iss=iss,
            sub=sub,
            aud=aud,
            iat=iat,
            exp=exp,
            jti=jti,
            wid=wid,
            task=TaskClaim(purpose="interop_test"),
            cap=[Capability(action=a) for a in cap_actions],
            exec_act=exec_act,
            pred=pred if pred is not None else [],
            exec_ts=exec_ts,
            status=status,
            inp_hash=inp_hash,
            out_hash=out_hash,
        )

        signing_priv = priv if priv is not None else priv_default
        signing_input = record.signing_input()

        # extra_claims injection requires re-assembly because ACTRecord
        # is a dataclass with a fixed shape. If a test wants to add
        # arbitrary top-level claims it must build the compact manually.
        if extra_claims:
            import base64
            import json

            header = record.to_header()
            claims = record.to_claims()
            claims.update(extra_claims)

            def b64(b: bytes) -> str:
                return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")

            h = b64(json.dumps(header, separators=(",", ":")).encode())
            p = b64(json.dumps(claims, separators=(",", ":")).encode())
            signing_input = f"{h}.{p}".encode("ascii")
            sig = act_sign(signing_priv, signing_input)
            compact = f"{h}.{p}.{b64(sig)}"
            return compact, record

        sig = act_sign(signing_priv, signing_input)
        compact = encode_jws(record, sig)
        return compact, record

    return _build


@pytest.fixture
def ect_payload_builder(es256_keypair, base_time):
    """Build a signed ECT compact string."""
    priv_default, _ = es256_keypair

    def _build(
        *,
        priv=None,
        kid: str = "ect-key-1",
        jti: str | None = None,
        wid: str = "",
        pred: list[str] | None = None,
        exec_act: str = "read.data",
        inp_hash: str = "",
        out_hash: str = "",
        aud: list[str] | None = None,
        iss: str = "spiffe://example.com/agent/a",
        iat: int | None = None,
        exp: int | None = None,
    ) -> tuple[str, Payload]:
        iat = iat or base_time
        exp = exp or (base_time + 600)
        jti = jti or _new_uuid()
        aud = aud or ["spiffe://example.com/agent/b"]

        payload = Payload(
            iss=iss,
            aud=aud,
            iat=iat,
            exp=exp,
            jti=jti,
            exec_act=exec_act,
            pred=pred if pred is not None else [],
            wid=wid,
            inp_hash=inp_hash,
            out_hash=out_hash,
        )
        signing_priv = priv if priv is not None else priv_default
        compact = ect_create(payload, signing_priv, CreateOptions(key_id=kid))
        return compact, payload

    return _build


@pytest.fixture
def dual_resolver(es256_keypair):
    """Return an (act_resolver_obj, ect_resolver_callable) bound to one
    shared ES256 key for both refimpls.

    ACT uses an ACTKeyResolver object (with .resolve(kid, header=)).
    ECT uses a plain Callable[[str], Optional[EllipticCurvePublicKey]].
    """
    from act.crypto import ACTKeyResolver, KeyRegistry

    _, pub = es256_keypair
    # Register the same key under both kids so either refimpl's token
    # resolves successfully during the compatibility tests.
    reg = KeyRegistry()
    reg.register("act-key-1", pub)
    reg.register("ect-key-1", pub)
    act_resolver = ACTKeyResolver(registry=reg)

    def ect_resolver(kid: str):
        if kid in ("act-key-1", "ect-key-1"):
            return pub
        return None

    return act_resolver, ect_resolver
