"""Tests for the Anthropic runtime adapter.

Uses a fake Anthropic-shaped client (no SDK dependency) to prove:
  * instrumenting records a model call into a valid provenance chain
  * the chain verifies (hash-chain integrity)
  * the gate denies a model that isn't in the signed manifest
"""
from __future__ import annotations

import pytest

from abom import sign
from abom.adapters import instrument
from abom.adapters.anthropic import AnthropicAdapter
from abom.audit import verify_chain
from abom.bom import build_composition
from abom.gate import ActionDenied


# --- a minimal fake of the anthropic SDK surface -----------------------------
class _Usage:
    def __init__(self, i, o):
        self.input_tokens, self.output_tokens = i, o


class _Block:
    def __init__(self, text):
        self.type, self.text = "text", text


class _Response:
    def __init__(self, text, i=10, o=5):
        self.content = [_Block(text)]
        self.usage = _Usage(i, o)


class _Messages:
    def create(self, *, model, messages, **kw):
        return _Response(f"reply from {model}")


class FakeAnthropic:
    __module__ = "anthropic"  # so the adapter auto-detects it

    def __init__(self):
        self.messages = _Messages()


# --- fixtures ----------------------------------------------------------------
MODEL = "claude-3-5-sonnet-20241022"


def _manifest(model=MODEL):
    key = sign.load_or_create_key()
    return build_composition(
        agent={"name": "test-agent", "version": "1.0.0"},
        components=[{"type": "model", "name": model}],
        controls={},
        key=key,
    )


# --- tests -------------------------------------------------------------------
def test_adapter_autodetects_anthropic():
    assert AnthropicAdapter().matches(FakeAnthropic())


def test_records_model_call_into_valid_chain():
    client = instrument(FakeAnthropic(), manifest=_manifest())
    resp = client.messages.create(model=MODEL, messages=[{"role": "user", "content": "hi"}])

    assert "reply from" in resp.content[0].text  # original call still works
    chain = client.abom.chain
    assert len(chain) == 1

    mc = chain[0]["data"]["model_calls"][0]
    assert mc["model"] == MODEL
    assert mc["tokens"] == 15  # 10 input + 5 output
    assert mc["output_sha256"] is not None

    # the provenance chain is hash-valid
    assert verify_chain(chain)["valid"]


def test_chain_binds_to_manifest():
    m = _manifest()
    client = instrument(FakeAnthropic(), manifest=m)
    client.messages.create(model=MODEL, messages=[])
    assert client.abom.chain[0]["data"]["composition_sha256"] == m["composition_sha256"]


def test_multiple_calls_extend_the_chain():
    client = instrument(FakeAnthropic(), manifest=_manifest())
    for _ in range(3):
        client.messages.create(model=MODEL, messages=[])
    chain = client.abom.chain
    assert [r["seq"] for r in chain] == [0, 1, 2]
    assert verify_chain(chain)["valid"]


def test_gate_allows_declared_model():
    client = instrument(FakeAnthropic(), manifest=_manifest(), gate=True)
    # MODEL is declared in the manifest → allowed, call proceeds
    resp = client.messages.create(model=MODEL, messages=[])
    assert resp is not None
    assert len(client.abom.chain) == 1


def test_gate_denies_undeclared_model():
    # manifest declares a DIFFERENT model; calling MODEL must be blocked
    client = instrument(FakeAnthropic(), manifest=_manifest(model="some-other-model"), gate=True)
    with pytest.raises(ActionDenied):
        client.messages.create(model=MODEL, messages=[])
    # the denied call left no provenance record (it never ran)
    assert client.abom.chain == []


def test_force_provider_selection():
    client = instrument(FakeAnthropic(), manifest=_manifest(), provider="anthropic")
    client.messages.create(model=MODEL, messages=[])
    assert len(client.abom.chain) == 1
