"""OpenAI Chat Completions schema conformance tests.

Verifies that aurora-lens proxy responses conform to the OpenAI API schema
across all governance outcomes, error shapes, and streaming formats.
"""

import pytest

from aurora_lens.lens import LensResult
from aurora_lens.pef.span import Span
from aurora_lens.govern.decision import GovernanceDecision, InterventionAction
from aurora_lens.verify.flags import Flag, FlagType
from aurora_lens.proxy.openai_compat import format_chat_response


# ── Shared fixtures ───────────────────────────────────────────────────────────

def _pass_result(response: str = "Sure, here you go.") -> LensResult:
    return LensResult(
        response=response,
        flags=[],
        pef_snapshot="",
        turn=1,
        span=Span.PRESENT,
        action=InterventionAction.PASS,
        model="mock",
    )


def _hard_stop_result() -> LensResult:
    flags = [Flag(
        flag_type=FlagType.SELF_HARM_INSTRUCTION,
        entity_name="self_harm",
        claim="Procedural self-harm guidance detected",
        evidence="how to kill yourself",
        severity="error",
    )]
    decision = GovernanceDecision(
        action=InterventionAction.HARD_STOP,
        flags=flags,
        rationale="Self-harm instruction detected",
        policy="strict",
        resource="a crisis helpline",
    )
    return LensResult(
        response="This response has been blocked by governance policy.",
        flags=flags,
        pef_snapshot="",
        turn=1,
        span=Span.PRESENT,
        action=InterventionAction.HARD_STOP,
        decision=decision,
        original_response="Here is how to kill yourself...",
        model="mock",
    )


def _make_proxy_app(monkeypatch):
    """Build a TestClient-ready proxy app backed by a minimal echo adapter."""
    try:
        from starlette.testclient import TestClient
    except ImportError:
        pytest.skip("Starlette not installed")

    from aurora_lens.proxy.config import ProxyConfig
    from aurora_lens.proxy.app import create_app
    from aurora_lens.adapters.base import LLMAdapter, AdapterResponse

    class _EchoAdapter(LLMAdapter):
        async def generate(self, messages, **kwargs):
            return AdapterResponse(text="test response", model="mock")

        async def generate_stream(self, messages, **kwargs):
            for chunk in ["test", " response"]:
                yield ({"choices": [{"delta": {"content": chunk}, "index": 0}]}, chunk)

    monkeypatch.setattr(
        "aurora_lens.proxy.app._build_provider_adapters",
        lambda cfg: (_EchoAdapter(), _EchoAdapter()),
    )
    cfg = ProxyConfig.from_mapping({
        "upstream": {"provider": "openai", "api_key": "sk-test", "model": "gpt-4"},
        "governance": {"audit_log": None},
        "extraction": {"backend": "spacy"},
    })
    return TestClient(create_app(cfg))


# ── Response schema ───────────────────────────────────────────────────────────

class TestResponseSchema:
    """format_chat_response output conforms to OpenAI chat.completion schema."""

    def test_required_fields_present(self):
        """All required OpenAI Chat Completion top-level fields present."""
        body = format_chat_response(_pass_result())
        for field in ("id", "object", "created", "model", "choices", "usage", "aurora"):
            assert field in body, f"Missing field: {field}"

    def test_object_is_chat_completion(self):
        """object field is always 'chat.completion'."""
        assert format_chat_response(_pass_result())["object"] == "chat.completion"

    def test_id_is_nonempty_string(self):
        """id is a non-empty string."""
        body = format_chat_response(_pass_result())
        assert isinstance(body["id"], str) and len(body["id"]) > 0

    def test_created_is_integer_timestamp(self):
        """created is an integer Unix timestamp (after 2023)."""
        body = format_chat_response(_pass_result())
        assert isinstance(body["created"], int)
        assert body["created"] > 1_700_000_000

    def test_choices_message_has_role_and_content(self):
        """choices[0].message has role == 'assistant' and correct content."""
        body = format_chat_response(_pass_result("Hello!"))
        msg = body["choices"][0]["message"]
        assert msg["role"] == "assistant"
        assert msg["content"] == "Hello!"

    def test_finish_reason_is_stop(self):
        """finish_reason is 'stop' for both PASS and HARD_STOP outcomes."""
        for result in [_pass_result(), _hard_stop_result()]:
            body = format_chat_response(result)
            assert body["choices"][0]["finish_reason"] == "stop"

    def test_usage_fields_are_integers(self):
        """prompt_tokens, completion_tokens, total_tokens are all integers."""
        usage = format_chat_response(_pass_result())["usage"]
        for key in ("prompt_tokens", "completion_tokens", "total_tokens"):
            assert key in usage, f"Missing usage field: {key}"
            assert isinstance(usage[key], int), f"{key} must be int, got {type(usage[key])}"

    def test_aurora_always_present_on_pass(self):
        """aurora key is always present, even on PASS with no flags."""
        body = format_chat_response(_pass_result())
        assert "aurora" in body
        assert body["aurora"]["governance"] == "PASS"

    def test_aurora_hard_stop_user_plane_only_by_default(self):
        """Default: aurora on HARD_STOP contains governance action only (two-plane contract)."""
        body = format_chat_response(_hard_stop_result())
        aurora = body["aurora"]
        assert aurora["governance"] == "HARD_STOP"
        assert "flags" not in aurora
        assert "rationale" not in aurora
        assert "original_response" not in aurora

    def test_aurora_hard_stop_operator_detail_when_enabled(self):
        """include_operator_detail=True exposes flags, rationale, original_response."""
        body = format_chat_response(_hard_stop_result(), include_operator_detail=True)
        aurora = body["aurora"]
        assert aurora["governance"] == "HARD_STOP"
        assert "SELF_HARM_INSTRUCTION" in aurora["flags"]
        assert "rationale" in aurora
        assert "original_response" in aurora

    def test_session_id_in_user_plane(self):
        """session_id routing handle is always in user-plane aurora when provided."""
        body = format_chat_response(_pass_result(), session_id="sess-abc123")
        assert body["aurora"]["session_id"] == "sess-abc123"

    def test_session_id_absent_when_not_provided(self):
        """session_id absent from aurora when not passed (stateless or test context)."""
        body = format_chat_response(_pass_result())
        assert "session_id" not in body["aurora"]

    def test_soft_correct_unverified_hint(self):
        """SOFT_CORRECT adds aurora.unverified=True; PASS does not."""
        from aurora_lens.govern.decision import InterventionAction as IA
        from aurora_lens.verify.flags import Flag, FlagType
        from aurora_lens.govern.decision import GovernanceDecision
        from aurora_lens.pef.span import Span
        from aurora_lens.lens import LensResult
        sc_result = LensResult(
            response="General knowledge response.",
            flags=[Flag(FlagType.UNRESOLVED_REFERENT, "e", "c", "ev", "warning")],
            pef_snapshot="",
            turn=1,
            span=Span.PRESENT,
            action=IA.SOFT_CORRECT,
            decision=GovernanceDecision(action=IA.SOFT_CORRECT, flags=[], rationale="r"),
        )
        body = format_chat_response(sc_result)
        assert body["aurora"]["unverified"] is True

        # PASS has no unverified hint
        pass_body = format_chat_response(_pass_result())
        assert "unverified" not in pass_body["aurora"]

    def test_audit_log_records_governance_note(self):
        """governance_note is written to the audit log even when absent from response body."""
        import tempfile, json
        from pathlib import Path
        from aurora_lens.govern.bridge import BuiltinBridge
        from aurora_lens.govern.decision import GovernanceDecision, InterventionAction
        from aurora_lens.verify.flags import Flag, FlagType

        with tempfile.NamedTemporaryFile(suffix=".jsonl", delete=False, mode="w") as f:
            audit_path = f.name

        bridge = BuiltinBridge(audit_path=audit_path)
        decision = GovernanceDecision(
            action=InterventionAction.SOFT_CORRECT,
            flags=[],
            rationale="test",
            governance_note="This response draws on general knowledge, not the conversation.",
        )
        decision.original_response = "Some response."
        bridge.log_decision(decision)

        entries = [json.loads(line) for line in Path(audit_path).read_text().splitlines() if line.strip()]
        assert len(entries) == 1
        assert entries[0]["governance_note"] == "This response draws on general knowledge, not the conversation."

    def test_model_field_from_result(self):
        """model field reflects the result's model name."""
        body = format_chat_response(_pass_result(), request_model="gpt-4")
        assert body["model"] == "gpt-4"


# ── All governance outcomes return HTTP 200 ───────────────────────────────────

class TestAllOutcomesReturn200:
    """Governance outcomes must never change the HTTP status code (OpenAI compat)."""

    def test_pass_returns_200(self, monkeypatch):
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            json={"model": "gpt-4", "messages": [{"role": "user", "content": "Hi"}]},
        )
        assert r.status_code == 200
        assert r.json()["aurora"]["governance"] == "PASS"

    def test_hard_stop_via_external_flag_returns_200(self, monkeypatch):
        """HARD_STOP from external_flags still returns HTTP 200."""
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            json={
                "model": "gpt-4",
                "messages": [{"role": "user", "content": "Hi"}],
                "aurora": {
                    "external_flags": [
                        {"type": "SELF_HARM_INSTRUCTION", "evidence": ["test"], "severity": "error"},
                    ],
                },
            },
        )
        assert r.status_code == 200
        assert r.json()["aurora"]["governance"] == "HARD_STOP"


# ── Error response shape ──────────────────────────────────────────────────────

class TestErrorResponseShape:
    """Error responses follow OpenAI error schema: {error: {message, ...}}."""

    def test_invalid_json_returns_400_with_error_key(self, monkeypatch):
        """400 body has top-level 'error' key with 'message'."""
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            content=b"not json",
            headers={"Content-Type": "application/json"},
        )
        assert r.status_code == 400
        body = r.json()
        assert "error" in body
        assert "message" in body["error"]

    def test_empty_messages_returns_422_with_error_key(self, monkeypatch):
        """422 body has top-level 'error' key with 'message'."""
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            json={"model": "gpt-4"},
        )
        assert r.status_code == 422
        body = r.json()
        assert "error" in body
        assert "message" in body["error"]


# ── Streaming format ──────────────────────────────────────────────────────────

class TestStreamingFormat:
    """Streaming responses are valid SSE terminating with data: [DONE]."""

    def test_stream_content_type_is_event_stream(self, monkeypatch):
        """Streaming response Content-Type is text/event-stream."""
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            json={"model": "gpt-4", "messages": [{"role": "user", "content": "Hi"}], "stream": True},
        )
        assert r.status_code == 200
        assert "text/event-stream" in r.headers.get("content-type", "")

    def test_stream_lines_are_sse_format(self, monkeypatch):
        """Non-empty lines in stream response start with 'data: '."""
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            json={"model": "gpt-4", "messages": [{"role": "user", "content": "Hi"}], "stream": True},
        )
        lines = [ln for ln in r.text.splitlines() if ln.strip()]
        for line in lines:
            assert line.startswith("data: "), f"SSE line malformed: {line!r}"

    def test_stream_terminates_with_done(self, monkeypatch):
        """SSE stream terminates with 'data: [DONE]'."""
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            json={"model": "gpt-4", "messages": [{"role": "user", "content": "Hi"}], "stream": True},
        )
        assert "data: [DONE]" in r.text

    def test_stream_contains_aurora_metadata_event(self, monkeypatch):
        """SSE stream contains a final aurora metadata event."""
        client = _make_proxy_app(monkeypatch)
        r = client.post(
            "/v1/chat/completions",
            json={"model": "gpt-4", "messages": [{"role": "user", "content": "Hi"}], "stream": True},
        )
        assert '"aurora"' in r.text
        assert '"governance"' in r.text
