"""Tests for the OpenAI runtime adapter.

Uses a fake OpenAI-shaped client (no SDK dependency) to prove the same
guarantees as the Anthropic adapter: records into a valid chain, binds to the
manifest, and the gate allows declared / denies undeclared models.
"""
from __future__ import annotations

import pytest

from abom import sign
from abom.adapters import instrument
from abom.adapters.openai import OpenAIAdapter
from abom.audit import verify_chain
from abom.bom import build_composition
from abom.gate import ActionDenied


# --- a minimal fake of the openai SDK surface --------------------------------
class _Usage:
    def __init__(self, total):
        self.total_tokens = total
        self.prompt_tokens, self.completion_tokens = total - 5, 5


class _Msg:
    def __init__(self, content):
        self.content = content


class _Choice:
    def __init__(self, content):
        self.message = _Msg(content)


class _Response:
    def __init__(self, model, text, total=20):
        self.model = model + "-2024-07-18"  # API echoes a snapshot id
        self.choices = [_Choice(text)]
        self.usage = _Usage(total)


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


class _Chat:
    def __init__(self):
        self.completions = _Completions()


class FakeOpenAI:
    __module__ = "openai"  # so the adapter auto-detects it

    def __init__(self):
        self.chat = _Chat()


MODEL = "gpt-4o-mini"


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_openai():
    assert OpenAIAdapter().matches(FakeOpenAI())


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

    assert "reply from" in resp.choices[0].message.content
    chain = client.abom.chain
    assert len(chain) == 1

    mc = chain[0]["data"]["model_calls"][0]
    # records the REQUESTED model, not the snapshot id the API echoed back
    assert mc["model"] == MODEL
    assert mc["tokens"] == 20
    assert mc["output_sha256"] is not None
    assert verify_chain(chain)["valid"]


def test_gate_allows_declared_and_denies_undeclared():
    ok = instrument(FakeOpenAI(), manifest=_manifest(), gate=True)
    ok.chat.completions.create(model=MODEL, messages=[])
    assert len(ok.abom.chain) == 1

    bad = instrument(FakeOpenAI(), manifest=_manifest(model="other-model"), gate=True)
    with pytest.raises(ActionDenied):
        bad.chat.completions.create(model=MODEL, messages=[])
    assert bad.abom.chain == []


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


# --- parse-helper coverage: dict-shaped responses + edge cases ---------------
from abom.adapters.openai import parse, _output_text, _tokens


def test_parse_dict_shaped_response():
    """OpenAI responses can arrive as plain dicts (e.g. via .model_dump())."""
    resp = {
        "choices": [{"message": {"content": "hello"}}],
        "usage": {"total_tokens": 42},
    }
    mc = parse("gpt-4o", resp)
    assert mc.model == "gpt-4o"
    assert mc.tokens == 42
    assert mc.output_sha256 is not None
    assert mc.endpoint == "api.openai.com"


def test_tokens_falls_back_to_prompt_plus_completion():
    resp = {"usage": {"prompt_tokens": 7, "completion_tokens": 3}}  # no total
    assert _tokens(resp) == 10


def test_tokens_none_when_no_usage():
    assert _tokens({}) is None
    assert _tokens({"usage": {}}) is None


def test_output_text_empty_when_no_choices():
    assert _output_text({}) == ""
    assert _output_text({"choices": []}) == ""


def test_wrap_rejects_non_openai_client():
    class NotAClient:
        pass

    with pytest.raises(TypeError, match="chat.completions.create"):
        OpenAIAdapter().wrap(NotAClient(), run=None)
