"""Cross-SDK Conformance Tests

These tests verify that the Python and TypeScript SDKs produce identical
behavior for the same inputs. Each test here has an exact counterpart in
the TypeScript SDK at tests/conformance.test.ts.

If a test fails here, the same scenario MUST also be verified in the TypeScript
SDK. Any behavioral difference between the two SDKs is a bug.
"""

from __future__ import annotations

import hashlib
import json
import os
import tempfile
from typing import Any
from unittest.mock import MagicMock

import pytest

from genosis import Genosis
from genosis.client import _sha256
from genosis.http import HttpResponse

import httpx


# ── Helpers ───────────────────────────────────────────────────────────────────

_test_counter = 0


def make_client(**kwargs) -> tuple[Genosis, list[dict]]:
    global _test_counter
    _test_counter += 1
    defaults = {
        "api_key": "gns_test_conformance",
        "base_url": "http://localhost:3001",
        "manifest_refresh_interval": 0,
        "buffer_path": os.path.join(tempfile.gettempdir(), f"genosis_conformance_py_{os.getpid()}_{_test_counter}.db"),
        "memoization_enabled": False,
    }
    defaults.update(kwargs)
    g = Genosis(**defaults)
    g._worker.shutdown(timeout=0.5)

    buffer_events: list[dict] = []
    original_put = g._buffer.put

    def capture_put(event_type: str, payload: dict) -> str:
        buffer_events.append({"type": event_type, "payload": payload})
        return original_put(event_type, payload)

    g._buffer.put = capture_put  # type: ignore
    return g, buffer_events


def set_manifest(g: Genosis, provider: str, model: str, manifest: dict) -> None:
    import time
    g._manifest_data[f"{provider}/{model}"] = manifest
    g._last_manifest_fetch[f"{provider}/{model}"] = time.time()


def sha256(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()


# ── Shared test data ─────────────────────────────────────────────────────────
# These exact strings and hashes must be identical in the TypeScript conformance tests.

BLOCK_A = "You are a helpful customer support assistant for Acme Corp."
BLOCK_B = "Product catalog: Widget A costs $10, Widget B costs $20, Widget C costs $50. All widgets come with a 30-day warranty."
BLOCK_C = "Return policy: Items may be returned within 30 days of purchase for a full refund. Items must be in original packaging."
BLOCK_D = "Current promotion: 20% off all Widget B purchases this week only."
BLOCK_E = "Tool: lookup_order(order_id: string) -> Returns order details including status and tracking."

HASH_A = sha256(BLOCK_A)
HASH_B = sha256(BLOCK_B)
HASH_C = sha256(BLOCK_C)
HASH_D = sha256(BLOCK_D)
HASH_E = sha256(BLOCK_E)

USER_MSG = "What is the return policy?"


# ── C1: Anthropic breakpoint placement ────────────────────────────────────────


class TestC1AnthropicBreakpoints:
    def test_places_breakpoints_on_all_cached_blocks_with_min_spacing_zero(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [
                {"hash": HASH_A, "tokens": 100, "priority": 0.9, "position": 0},
                {"hash": HASH_B, "tokens": 200, "priority": 0.8, "position": 1},
                {"hash": HASH_C, "tokens": 150, "priority": 0.7, "position": 2},
            ],
            "provider_hints": {"anthropic": {"breakpoint_positions": [0, 1, 2], "cache_type": "ephemeral"}},
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {
                "model": "claude-sonnet-4-6",
                "system": [
                    {"type": "text", "text": BLOCK_A},
                    {"type": "text", "text": BLOCK_B},
                    {"type": "text", "text": BLOCK_C},
                    {"type": "text", "text": BLOCK_D},  # not in cache_train
                ],
                "messages": [],
            },
            fn,
        )

        system = called_with[0]["system"]
        assert system[0].get("cache_control") == {"type": "ephemeral"}
        assert system[1].get("cache_control") == {"type": "ephemeral"}
        assert system[2].get("cache_control") == {"type": "ephemeral"}
        assert system[3].get("cache_control") is None

    def test_skips_breakpoint_when_below_min_breakpoint_spacing(self):
        g, _ = make_client()

        # breakpoint_positions=[] means no blocks get breakpoints
        manifest = {
            "cache_train": [
                {"hash": HASH_A, "tokens": 15, "priority": 0.9, "position": 0},
                {"hash": HASH_B, "tokens": 30, "priority": 0.8, "position": 1},
            ],
            "provider_hints": {"anthropic": {"breakpoint_positions": [], "cache_type": "ephemeral"}},
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {"model": "claude-sonnet-4-6", "system": [BLOCK_A, BLOCK_B], "messages": []},
            fn,
        )

        system = called_with[0]["system"]
        # No breakpoint_positions → no cache_control added
        for block in system:
            if isinstance(block, dict):
                assert "cache_control" not in block

    def test_uses_cache_type_from_provider_hints(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [{"hash": HASH_B, "tokens": 200, "priority": 0.9, "position": 0}],
            "provider_hints": {"anthropic": {"breakpoint_positions": [0], "cache_type": "persistent"}},
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {"model": "claude-sonnet-4-6", "system": [{"type": "text", "text": BLOCK_B}], "messages": []},
            fn,
        )

        system = called_with[0]["system"]
        assert system[0]["cache_control"] == {"type": "persistent"}

    def test_no_match_string_system_passes_through(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [{"hash": HASH_B, "tokens": 200, "priority": 0.9, "position": 0}],
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {"model": "claude-sonnet-4-6", "system": BLOCK_A, "messages": []},
            fn,
        )

        # String system with no match stays as string
        assert called_with[0]["system"] == BLOCK_A


# ── C2: OpenAI block reordering ──────────────────────────────────────────────


class TestC2OpenAIReordering:
    def test_reorders_system_content_by_manifest_frequency(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [
                {"hash": HASH_B, "tokens": 200, "priority": 0.9, "position": 0},
                {"hash": HASH_A, "tokens": 100, "priority": 0.8, "position": 1},
                {"hash": HASH_C, "tokens": 150, "priority": 0.7, "position": 2},
            ],
        }
        set_manifest(g, "openai", "gpt-4o", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"prompt_tokens": 100, "completion_tokens": 50}}

        g.call(
            {
                "model": "gpt-4o",
                "messages": [
                    {"role": "system", "content": [
                        {"type": "text", "text": BLOCK_A},
                        {"type": "text", "text": BLOCK_C},
                        {"type": "text", "text": BLOCK_B},
                        {"type": "text", "text": BLOCK_D},
                    ]},
                    {"role": "user", "content": USER_MSG},
                ],
            },
            fn,
        )

        content = called_with[0]["messages"][0]["content"]
        assert content[0]["text"] == BLOCK_B  # pos 0 in manifest
        assert content[1]["text"] == BLOCK_A  # pos 1 in manifest
        assert content[2]["text"] == BLOCK_C  # pos 2 in manifest
        assert content[3]["text"] == BLOCK_D  # not in manifest → end

    def test_pushes_uncached_blocks_to_end(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [{"hash": HASH_A, "tokens": 100, "priority": 0.9, "position": 0}],
        }
        set_manifest(g, "openai", "gpt-4o", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"prompt_tokens": 100, "completion_tokens": 50}}

        g.call(
            {
                "model": "gpt-4o",
                "messages": [
                    {"role": "system", "content": [
                        {"type": "text", "text": BLOCK_D},
                        {"type": "text", "text": BLOCK_A},
                        {"type": "text", "text": BLOCK_C},
                    ]},
                    {"role": "user", "content": USER_MSG},
                ],
            },
            fn,
        )

        content = called_with[0]["messages"][0]["content"]
        assert content[0]["text"] == BLOCK_A  # cached → first

    def test_leaves_single_item_unchanged(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [{"hash": HASH_A, "tokens": 100, "priority": 0.9, "position": 0}],
        }
        set_manifest(g, "openai", "gpt-4o", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"prompt_tokens": 100, "completion_tokens": 50}}

        g.call(
            {
                "model": "gpt-4o",
                "messages": [
                    {"role": "system", "content": [{"type": "text", "text": BLOCK_A}]},
                    {"role": "user", "content": USER_MSG},
                ],
            },
            fn,
        )

        content = called_with[0]["messages"][0]["content"]
        assert len(content) == 1
        assert content[0]["text"] == BLOCK_A

    def test_leaves_string_content_unchanged(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [{"hash": HASH_A, "tokens": 100, "priority": 0.9, "position": 0}],
        }
        set_manifest(g, "openai", "gpt-4o", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"prompt_tokens": 100, "completion_tokens": 50}}

        g.call(
            {
                "model": "gpt-4o",
                "messages": [
                    {"role": "system", "content": BLOCK_A},
                    {"role": "user", "content": USER_MSG},
                ],
            },
            fn,
        )

        assert called_with[0]["messages"][0]["content"] == BLOCK_A


# ── C3: Provider detection ────────────────────────────────────────────────────


class TestC3ProviderDetection:
    def test_detects_anthropic(self):
        g, _ = make_client()
        assert g._detect_provider({"model": "claude-sonnet-4-6"}) == "anthropic"
        assert g._detect_provider({"model": "claude-3-5-haiku-20241022"}) == "anthropic"

    def test_detects_openai(self):
        g, _ = make_client()
        assert g._detect_provider({"model": "gpt-4o"}) == "openai"
        assert g._detect_provider({"model": "o1-preview"}) == "openai"
        assert g._detect_provider({"model": "o3-mini"}) == "openai"
        assert g._detect_provider({"model": "o4-mini"}) == "openai"

    def test_detects_google(self):
        g, _ = make_client()
        assert g._detect_provider({"model": "gemini-2.5-flash"}) == "google"

    def test_detects_bedrock_anthropic(self):
        g, _ = make_client()
        assert g._detect_provider({"model": "anthropic.claude-sonnet-4-6-20250514-v1:0"}) == "anthropic"
        assert g._detect_provider({"model": "us.anthropic.claude-3-5-haiku-20241022-v1:0"}) == "anthropic"

    def test_falls_back_to_request_shape_anthropic(self):
        g, _ = make_client()
        assert g._detect_provider({"model": "custom-model", "system": "hello"}) == "anthropic"

    def test_falls_back_to_request_shape_google(self):
        g, _ = make_client()
        assert g._detect_provider({"model": "custom-model", "systemInstruction": {}}) == "google"


# ── C4: Model normalization ───────────────────────────────────────────────────


class TestC4ModelNormalization:
    def test_normalizes_bedrock_arn(self):
        assert Genosis._normalize_model("anthropic.claude-sonnet-4-6-20250514-v1:0") == "claude-sonnet-4-6-20250514"
        assert Genosis._normalize_model("us.anthropic.claude-3-5-haiku-20241022-v1:0") == "claude-3-5-haiku-20241022"

    def test_passes_through_non_bedrock(self):
        assert Genosis._normalize_model("claude-sonnet-4-6") == "claude-sonnet-4-6"
        assert Genosis._normalize_model("gpt-4o") == "gpt-4o"
        assert Genosis._normalize_model("gemini-2.5-flash") == "gemini-2.5-flash"


# ── C5: Fingerprint computation ───────────────────────────────────────────────


class TestC5Fingerprint:
    def test_deterministic_fingerprint(self):
        g, buffer_events = make_client()
        set_manifest(g, "anthropic", "claude-sonnet-4-6", {"cache_train": []})

        def fn(params: dict) -> dict:
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        # Run twice with same content
        g.call(
            {"model": "claude-sonnet-4-6", "system": [BLOCK_A, BLOCK_B], "messages": [{"role": "user", "content": USER_MSG}]},
            fn,
        )
        g.call(
            {"model": "claude-sonnet-4-6", "system": [BLOCK_A, BLOCK_B], "messages": [{"role": "user", "content": USER_MSG}]},
            fn,
        )

        events = [e for e in buffer_events if e["type"] == "telemetry"]
        assert len(events) == 2
        assert events[0]["payload"]["fingerprint"]
        assert events[0]["payload"]["fingerprint"] == events[1]["payload"]["fingerprint"]

        # Verify the expected fingerprint value
        expected_combined = HASH_A + "+" + HASH_B + "+" + sha256(USER_MSG)
        expected_fingerprint = sha256(expected_combined)
        assert events[0]["payload"]["fingerprint"] == expected_fingerprint

    def test_different_user_message_different_fingerprint(self):
        g, buffer_events = make_client()
        set_manifest(g, "anthropic", "claude-sonnet-4-6", {"cache_train": []})

        def fn(params: dict) -> dict:
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {"model": "claude-sonnet-4-6", "system": [BLOCK_A], "messages": [{"role": "user", "content": "Question A"}]},
            fn,
        )
        g.call(
            {"model": "claude-sonnet-4-6", "system": [BLOCK_A], "messages": [{"role": "user", "content": "Question B"}]},
            fn,
        )

        events = [e for e in buffer_events if e["type"] == "telemetry"]
        assert events[0]["payload"]["fingerprint"] != events[1]["payload"]["fingerprint"]


# ── C6: Error fallback ────────────────────────────────────────────────────────


class TestC6ErrorFallback:
    def test_returns_original_params_on_optimization_error(self):
        g, _ = make_client()

        g._manifest_data["anthropic/claude-sonnet-4-6"] = {
            "cache_train": [{"hash": "abc", "tokens": 100, "priority": 1, "position": 0}],
            "provider_hints": {"anthropic": {"breakpoint_positions": [0], "cache_type": "ephemeral"}},
        }

        import time
        g._last_manifest_fetch["anthropic/claude-sonnet-4-6"] = time.time()

        # Break the optimize method
        original_optimize = g._optimize
        def broken_optimize(params: dict) -> dict:
            raise RuntimeError("test error")
        g._optimize = broken_optimize  # type: ignore

        original_params = {
            "model": "claude-sonnet-4-6",
            "system": BLOCK_A,
            "messages": [{"role": "user", "content": USER_MSG}],
        }

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        result = g.call(original_params, fn)
        assert result.memoized is False
        assert len(called_with) == 1
        # Should receive original params, not optimized
        assert called_with[0] is original_params


# ── C7: Telemetry event structure ─────────────────────────────────────────────


class TestC7TelemetryStructure:
    def test_queues_telemetry_with_all_required_fields(self):
        g, buffer_events = make_client()

        set_manifest(g, "anthropic", "claude-sonnet-4-6", {
            "cache_train": [],
            "manifest_version": "v2026-03-18-120000-test",
            "manifest_token": "tok_test",
        })

        def fn(params: dict) -> dict:
            return {
                "usage": {
                    "input_tokens": 500,
                    "output_tokens": 200,
                    "cache_creation_input_tokens": 100,
                    "cache_read_input_tokens": 300,
                }
            }

        g.call(
            {"model": "claude-sonnet-4-6", "system": [BLOCK_A], "messages": [{"role": "user", "content": USER_MSG}]},
            fn,
        )

        events = [e for e in buffer_events if e["type"] == "telemetry"]
        assert len(events) == 1
        event = events[0]["payload"]

        assert event["event_id"]
        assert "T" in event["timestamp"]
        assert event["model"] == "claude-sonnet-4-6"
        assert event["provider"] == "anthropic"
        assert isinstance(event["blocks"], list)
        assert "hash" in event["blocks"][0]
        assert "tokens" in event["blocks"][0]
        assert "position" in event["blocks"][0]
        assert "cached" in event["blocks"][0]
        assert event["usage"] == {
            "input_tokens": 500,
            "output_tokens": 200,
            "cache_write_tokens": 100,
            "cache_read_tokens": 300,
        }
        assert event["fingerprint"]
        assert event["worker_id"]
        assert event["memoized"] is False
        assert event["manifest_version"] == "v2026-03-18-120000-test"
        assert event["manifest_token"] == "tok_test"


# ── C8: Usage extraction ─────────────────────────────────────────────────────


class TestC9HashConsistency:
    """These expected hashes must match exactly in the TypeScript tests."""

    def test_ascii_sha256(self):
        assert sha256("Hello, world!") == "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"

    def test_unicode_sha256(self):
        h = sha256("Ünïcödé tëxt with émojis 🎉🚀")
        assert h
        assert len(h) == 64

    def test_shared_block_hashes(self):
        assert HASH_A == sha256(BLOCK_A)
        assert HASH_B == sha256(BLOCK_B)
        assert HASH_C == sha256(BLOCK_C)
        assert HASH_D == sha256(BLOCK_D)
        assert HASH_E == sha256(BLOCK_E)


class TestC10StaleCacheControlRemoval:
    def test_removes_old_cache_control_when_block_not_in_manifest(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [{"hash": HASH_B, "tokens": 200, "priority": 0.9, "position": 0}],
            "provider_hints": {"anthropic": {"breakpoint_positions": [0], "cache_type": "ephemeral"}},
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {
                "model": "claude-sonnet-4-6",
                "system": [
                    {"type": "text", "text": BLOCK_A, "cache_control": {"type": "ephemeral"}},  # stale
                    {"type": "text", "text": BLOCK_B},
                ],
                "messages": [],
            },
            fn,
        )

        system = called_with[0]["system"]
        assert system[0].get("cache_control") is None  # stale removed
        assert system[1].get("cache_control") == {"type": "ephemeral"}  # from manifest


class TestC11OpenAIPreservesNonSystem:
    def test_reorders_system_without_affecting_other_messages(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [
                {"hash": HASH_B, "tokens": 200, "priority": 0.9, "position": 0},
                {"hash": HASH_A, "tokens": 100, "priority": 0.8, "position": 1},
            ],
        }
        set_manifest(g, "openai", "gpt-4o", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"prompt_tokens": 100, "completion_tokens": 50}}

        g.call(
            {
                "model": "gpt-4o",
                "messages": [
                    {"role": "system", "content": [{"type": "text", "text": BLOCK_A}, {"type": "text", "text": BLOCK_B}]},
                    {"role": "user", "content": "First question"},
                    {"role": "assistant", "content": "First answer"},
                    {"role": "user", "content": "Follow up"},
                ],
            },
            fn,
        )

        messages = called_with[0]["messages"]
        assert messages[0]["content"][0]["text"] == BLOCK_B
        assert messages[0]["content"][1]["text"] == BLOCK_A
        assert messages[1] == {"role": "user", "content": "First question"}
        assert messages[2] == {"role": "assistant", "content": "First answer"}
        assert messages[3] == {"role": "user", "content": "Follow up"}


class TestC12MixedStringDictSystem:
    def test_handles_mixed_string_and_dict_items(self):
        g, _ = make_client()

        manifest = {
            "cache_train": [
                {"hash": HASH_A, "tokens": 100, "priority": 0.9, "position": 0},
                {"hash": HASH_B, "tokens": 200, "priority": 0.8, "position": 1},
            ],
            "provider_hints": {"anthropic": {"breakpoint_positions": [0, 1], "cache_type": "ephemeral"}},
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {
                "model": "claude-sonnet-4-6",
                "system": [
                    BLOCK_A,  # plain string
                    {"type": "text", "text": BLOCK_B},  # dict
                ],
                "messages": [],
            },
            fn,
        )

        system = called_with[0]["system"]
        assert system[0].get("cache_control") == {"type": "ephemeral"}
        assert system[1].get("cache_control") == {"type": "ephemeral"}


def _sorted_stringify(obj: Any) -> str:
    """Replicates the SDK's sortedStringify for computing tool hashes in tests."""
    if obj is None or not isinstance(obj, (dict, list)):
        return json.dumps(obj, separators=(",", ":"))
    if isinstance(obj, list):
        return "[" + ",".join(_sorted_stringify(v) for v in obj) + "]"
    keys = sorted(obj.keys())
    return "{" + ",".join(json.dumps(k) + ":" + _sorted_stringify(obj[k]) for k in keys) + "}"


def _tool_hash(tool: dict) -> str:
    clean = {k: v for k, v in tool.items() if k != "cache_control"}
    return hashlib.sha256(_sorted_stringify(clean).encode()).hexdigest()


class TestC13AnthropicBlockReordering:
    def test_reorders_tools_to_match_manifest_position(self):
        """User sends tools in order [C, A, B]; manifest prescribes [A, B, C].
        SDK must reorder to [A, B, C] so Anthropic's cache prefix is consistent."""
        g, _ = make_client()

        TOOL_A = {"name": "tool_a", "description": "Tool A", "input_schema": {"type": "object", "properties": {}}}
        TOOL_B = {"name": "tool_b", "description": "Tool B", "input_schema": {"type": "object", "properties": {}}}
        TOOL_C = {"name": "tool_c", "description": "Tool C", "input_schema": {"type": "object", "properties": {}}}

        manifest = {
            "cache_train": [
                {"hash": _tool_hash(TOOL_A), "tokens": 20, "priority": 0.9, "position": 0, "source": "tool"},
                {"hash": _tool_hash(TOOL_B), "tokens": 20, "priority": 0.8, "position": 1, "source": "tool"},
                {"hash": _tool_hash(TOOL_C), "tokens": 20, "priority": 0.7, "position": 2, "source": "tool"},
            ],
            "provider_hints": {"anthropic": {"breakpoint_positions": [2], "cache_type": "ephemeral"}},
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {
                "model": "claude-sonnet-4-6",
                "system": "System prompt",
                "tools": [TOOL_C, TOOL_A, TOOL_B],  # caller sends in wrong order
                "messages": [],
            },
            fn,
        )

        tools = called_with[0]["tools"]
        # Must be reordered to [A=0, B=1, C=2]
        assert tools[0]["name"] == "tool_a"
        assert tools[1]["name"] == "tool_b"
        assert tools[2]["name"] == "tool_c"
        # C (position 2) is the breakpoint
        assert tools[2].get("cache_control") == {"type": "ephemeral"}

    def test_reorders_system_blocks_to_match_manifest_position(self):
        """User sends system blocks in order [B, A]; manifest prescribes [A, B].
        SDK must reorder to [A, B] so Anthropic's cache prefix is consistent."""
        g, _ = make_client()

        BLOCK_X = "System block X. " * 30
        BLOCK_Y = "System block Y. " * 30

        manifest = {
            "cache_train": [
                {"hash": sha256(BLOCK_X), "tokens": 120, "priority": 0.9, "position": 0, "source": "system"},
                {"hash": sha256(BLOCK_Y), "tokens": 120, "priority": 0.8, "position": 1, "source": "system"},
            ],
            "provider_hints": {"anthropic": {"breakpoint_positions": [1], "cache_type": "ephemeral"}},
        }
        set_manifest(g, "anthropic", "claude-sonnet-4-6", manifest)

        called_with: list[dict] = []
        def fn(params: dict) -> dict:
            called_with.append(params)
            return {"usage": {"input_tokens": 100, "output_tokens": 50}}

        g.call(
            {
                "model": "claude-sonnet-4-6",
                "system": [
                    {"type": "text", "text": BLOCK_Y},  # wrong order
                    {"type": "text", "text": BLOCK_X},
                ],
                "messages": [],
            },
            fn,
        )

        system = called_with[0]["system"]
        # Must be reordered to [X, Y]
        assert system[0]["text"] == BLOCK_X
        assert system[1]["text"] == BLOCK_Y
        # Y (position 1) is the breakpoint
        assert system[0].get("cache_control") is None
        assert system[1].get("cache_control") == {"type": "ephemeral"}


class TestC8UsageExtraction:
    def test_anthropic_usage(self):
        g, buffer_events = make_client()
        set_manifest(g, "anthropic", "claude-sonnet-4-6", {"cache_train": []})

        def fn(params: dict) -> dict:
            return {"usage": {"input_tokens": 500, "output_tokens": 200, "cache_creation_input_tokens": 100, "cache_read_input_tokens": 300}}

        g.call({"model": "claude-sonnet-4-6", "system": BLOCK_A, "messages": []}, fn)

        usage = [e for e in buffer_events if e["type"] == "telemetry"][0]["payload"]["usage"]
        assert usage == {"input_tokens": 500, "output_tokens": 200, "cache_write_tokens": 100, "cache_read_tokens": 300}

    def test_openai_usage(self):
        g, buffer_events = make_client()
        set_manifest(g, "openai", "gpt-4o", {"cache_train": []})

        def fn(params: dict) -> dict:
            return {"usage": {"prompt_tokens": 500, "completion_tokens": 200, "prompt_tokens_details": {"cached_tokens": 300}}}

        g.call({"model": "gpt-4o", "messages": [{"role": "system", "content": BLOCK_A}, {"role": "user", "content": USER_MSG}]}, fn)

        usage = [e for e in buffer_events if e["type"] == "telemetry"][0]["payload"]["usage"]
        assert usage == {"input_tokens": 500, "output_tokens": 200, "cache_write_tokens": 0, "cache_read_tokens": 300}

    def test_google_usage(self):
        g, buffer_events = make_client()
        set_manifest(g, "google", "gemini-2.5-flash", {"cache_train": []})

        def fn(params: dict) -> dict:
            return {"usageMetadata": {"promptTokenCount": 500, "candidatesTokenCount": 200, "cachedContentTokenCount": 300}}

        g.call({"model": "gemini-2.5-flash", "systemInstruction": {"parts": [{"text": BLOCK_A}]}, "contents": [{"role": "user", "parts": [{"text": USER_MSG}]}], "messages": []}, fn)

        usage = [e for e in buffer_events if e["type"] == "telemetry"][0]["payload"]["usage"]
        assert usage == {"input_tokens": 500, "output_tokens": 200, "cache_write_tokens": 0, "cache_read_tokens": 300}
