"""
Tests for cortex/hippo/transcript_repair.py

Covers:
- Orphaned toolResult is dropped
- Missing toolResult gets synthetic error inserted
- Duplicate toolResult is dropped
- Reasoning blocks after toolCall are reordered (OpenAI shape)
- Clean transcript passes through unchanged (same object returned)
- Assistant message with stopReason=error is not repaired
"""

import pytest
from cortex.hippo.transcript_repair import sanitize_tool_use_result_pairing


# -- Helpers --

def make_assistant_with_tool_call(tool_call_id: str = "call_1", tool_type: str = "toolCall") -> dict:
    return {
        "role": "assistant",
        "content": [
            {"type": tool_type, "id": tool_call_id, "name": "search"},
        ],
    }


def make_tool_result(tool_call_id: str = "call_1") -> dict:
    return {
        "role": "toolResult",
        "toolCallId": tool_call_id,
        "content": [{"type": "text", "text": "result"}],
    }


def make_user_msg(text: str = "hello") -> dict:
    return {"role": "user", "content": text}


def make_assistant_text(text: str = "hi there") -> dict:
    return {"role": "assistant", "content": text}


# -- Tests --

class TestOrphanedToolResult:
    def test_orphaned_tool_result_is_dropped(self):
        """toolResult with no preceding assistant tool call is dropped."""
        messages = [
            make_user_msg(),
            make_tool_result("call_orphan"),  # no assistant before this
            make_assistant_text(),
        ]
        result = sanitize_tool_use_result_pairing(messages)
        assert all(m.get("role") != "toolResult" for m in result)
        assert len(result) == 2  # user + assistant text

    def test_orphaned_tool_result_mid_conversation_is_dropped(self):
        """toolResult appearing after a non-tool assistant message is dropped."""
        messages = [
            make_assistant_text("I said something"),
            make_tool_result("call_orphan"),
            make_user_msg("ok"),
        ]
        result = sanitize_tool_use_result_pairing(messages)
        roles = [m.get("role") for m in result]
        assert "toolResult" not in roles
        assert len(result) == 2


class TestMissingToolResult:
    def test_missing_tool_result_gets_synthetic_error(self):
        """When assistant has toolCall but no matching toolResult follows, insert synthetic error."""
        messages = [
            make_assistant_with_tool_call("call_1"),
            make_user_msg("next turn"),
        ]
        result = sanitize_tool_use_result_pairing(messages)
        # Should have: assistant, synthetic toolResult, user
        assert len(result) == 3
        synth = result[1]
        assert synth["role"] == "toolResult"
        assert synth["toolCallId"] == "call_1"
        assert synth.get("isError") is True
        assert "missing tool result" in synth["content"][0]["text"]

    def test_missing_tool_result_at_end_gets_synthetic_error(self):
        """Tool call at end of transcript with no result."""
        messages = [make_assistant_with_tool_call("call_end")]
        result = sanitize_tool_use_result_pairing(messages)
        assert len(result) == 2
        assert result[1]["role"] == "toolResult"
        assert result[1]["isError"] is True

    def test_multiple_tool_calls_all_get_results(self):
        """Each tool call ID gets its own result (existing or synthetic)."""
        asst = {
            "role": "assistant",
            "content": [
                {"type": "toolCall", "id": "c1", "name": "fn1"},
                {"type": "toolCall", "id": "c2", "name": "fn2"},
            ],
        }
        result_c1 = {"role": "toolResult", "toolCallId": "c1", "content": []}
        # c2 has no result

        messages = [asst, result_c1]
        result = sanitize_tool_use_result_pairing(messages)
        # assistant + result_c1 + synthetic_c2
        assert len(result) == 3
        ids = [m.get("toolCallId") for m in result if m.get("role") == "toolResult"]
        assert "c1" in ids
        assert "c2" in ids
        # c2 should be synthetic/error
        c2_result = next(m for m in result if m.get("toolCallId") == "c2")
        assert c2_result.get("isError") is True


class TestDuplicateToolResult:
    def test_duplicate_tool_result_is_dropped(self):
        """Second toolResult for same toolCallId is dropped."""
        asst = make_assistant_with_tool_call("call_1")
        r1 = {"role": "toolResult", "toolCallId": "call_1", "content": [{"type": "text", "text": "first"}]}
        r2 = {"role": "toolResult", "toolCallId": "call_1", "content": [{"type": "text", "text": "duplicate"}]}

        messages = [asst, r1, r2]
        result = sanitize_tool_use_result_pairing(messages)
        tool_results = [m for m in result if m.get("role") == "toolResult"]
        assert len(tool_results) == 1
        assert tool_results[0]["content"][0]["text"] == "first"


class TestReasoningBlockReorder:
    def test_reasoning_after_tool_call_is_reordered(self):
        """Reasoning blocks appearing after a functionCall are moved to the front."""
        asst = {
            "role": "assistant",
            "content": [
                {"type": "functionCall", "id": "c1", "name": "fn"},
                {"type": "reasoning", "reasoning": "I should call fn"},
            ],
        }
        result_msg = {"role": "toolResult", "toolCallId": "c1", "content": []}

        messages = [asst, result_msg]
        result = sanitize_tool_use_result_pairing(messages)

        asst_out = result[0]
        assert isinstance(asst_out["content"], list)
        # reasoning should come first
        assert asst_out["content"][0]["type"] == "reasoning"
        assert asst_out["content"][1]["type"] == "functionCall"

    def test_thinking_after_tool_call_is_reordered(self):
        """'thinking' type blocks are also moved before tool calls."""
        asst = {
            "role": "assistant",
            "content": [
                {"type": "function_call", "id": "c1", "name": "fn"},
                {"type": "thinking", "thinking": "let me think"},
            ],
        }
        result_msg = {"role": "toolResult", "toolCallId": "c1", "content": []}

        messages = [asst, result_msg]
        result = sanitize_tool_use_result_pairing(messages)

        asst_out = result[0]
        assert asst_out["content"][0]["type"] == "thinking"

    def test_reasoning_before_tool_call_not_reordered(self):
        """If reasoning already comes before tool call, no change."""
        asst = {
            "role": "assistant",
            "content": [
                {"type": "reasoning", "reasoning": "thinking first"},
                {"type": "functionCall", "id": "c1", "name": "fn"},
            ],
        }
        result_msg = {"role": "toolResult", "toolCallId": "c1", "content": []}
        messages = [asst, result_msg]
        result = sanitize_tool_use_result_pairing(messages)
        asst_out = result[0]
        assert asst_out["content"][0]["type"] == "reasoning"
        assert asst_out["content"][1]["type"] == "functionCall"

    def test_multi_function_call_not_reordered(self):
        """Multi-call turns with interleaved reasoning are left untouched."""
        content = [
            {"type": "functionCall", "id": "c1", "name": "fn1"},
            {"type": "reasoning", "reasoning": "between calls"},
            {"type": "functionCall", "id": "c2", "name": "fn2"},
        ]
        asst = {"role": "assistant", "content": content}
        r1 = {"role": "toolResult", "toolCallId": "c1", "content": []}
        r2 = {"role": "toolResult", "toolCallId": "c2", "content": []}

        messages = [asst, r1, r2]
        result = sanitize_tool_use_result_pairing(messages)
        asst_out = result[0]
        # Content unchanged — same order
        assert asst_out["content"][0]["type"] == "functionCall"
        assert asst_out["content"][1]["type"] == "reasoning"
        assert asst_out["content"][2]["type"] == "functionCall"


class TestCleanTranscript:
    def test_clean_transcript_passes_through_unchanged(self):
        """A properly paired transcript with no non-toolResult messages after the result
        returns the same list object (no rebuilding needed)."""
        asst = make_assistant_with_tool_call("call_1")
        result_msg = make_tool_result("call_1")

        # No user message after — nothing causes 'moved' to be set
        messages = [asst, result_msg]
        result = sanitize_tool_use_result_pairing(messages)
        # Should return the same object (no copy) since nothing changed
        assert result is messages

    def test_clean_transcript_with_continuation_correct_order(self):
        """A properly ordered transcript still comes back correct even if rebuilt."""
        asst = make_assistant_with_tool_call("call_1")
        result_msg = make_tool_result("call_1")
        user = make_user_msg("thanks")

        messages = [asst, result_msg, user]
        result = sanitize_tool_use_result_pairing(messages)
        # Content should be correct regardless of whether it's the same object
        assert len(result) == 3
        assert result[0]["role"] == "assistant"
        assert result[1]["role"] == "toolResult"
        assert result[2]["role"] == "user"

    def test_text_only_transcript_unchanged(self):
        """Transcript with no tool calls passes through unchanged."""
        messages = [
            make_user_msg("hello"),
            make_assistant_text("hi"),
            make_user_msg("bye"),
        ]
        result = sanitize_tool_use_result_pairing(messages)
        assert result is messages

    def test_empty_list_unchanged(self):
        messages: list[dict] = []
        result = sanitize_tool_use_result_pairing(messages)
        assert result == []


class TestStopReasonError:
    def test_assistant_stop_reason_error_not_repaired(self):
        """Assistant message with stopReason=error — tool calls are NOT given synthetic results."""
        asst = {
            "role": "assistant",
            "stopReason": "error",
            "content": [
                {"type": "toolCall", "id": "call_err", "name": "fn"},
            ],
        }
        user = make_user_msg("after error")
        messages = [asst, user]
        result = sanitize_tool_use_result_pairing(messages)
        # No synthetic toolResult should be inserted
        roles = [m.get("role") for m in result]
        assert "toolResult" not in roles
        assert len(result) == 2

    def test_assistant_stop_reason_aborted_not_repaired(self):
        """Assistant message with stopReason=aborted is also skipped."""
        asst = {
            "role": "assistant",
            "stopReason": "aborted",
            "content": [
                {"type": "toolCall", "id": "call_abort", "name": "fn"},
            ],
        }
        messages = [asst]
        result = sanitize_tool_use_result_pairing(messages)
        roles = [m.get("role") for m in result]
        assert "toolResult" not in roles

    def test_normal_stop_reason_is_repaired(self):
        """Assistant without stopReason=error/aborted still gets repaired."""
        asst = {
            "role": "assistant",
            "stopReason": "tool_use",
            "content": [
                {"type": "toolCall", "id": "call_1", "name": "fn"},
            ],
        }
        messages = [asst]
        result = sanitize_tool_use_result_pairing(messages)
        assert len(result) == 2
        assert result[1]["isError"] is True


class TestToolUseIdAlias:
    def test_tool_use_id_alias_recognized(self):
        """toolUseId (Anthropic alias) works as well as toolCallId."""
        asst = {
            "role": "assistant",
            "content": [{"type": "tool_use", "id": "tu_1", "name": "fn"}],
        }
        result_msg = {
            "role": "toolResult",
            "toolUseId": "tu_1",
            "content": [{"type": "text", "text": "ok"}],
        }
        messages = [asst, result_msg]
        result = sanitize_tool_use_result_pairing(messages)
        # No synthetic result — the toolUseId matched
        assert result is messages
