"""Regression tests for PR #3665: tool_calls included in merge/dedup/visible keys.

Verifies that _session_message_merge_key, _session_message_dedup_key,
_session_message_visible_key, and _matching_visible_duplicate correctly
distinguish messages with different tool_calls arrays.

Without tool_calls in the key, assistant messages that invoke different
tools (but share empty content and same-second timestamp) collapse into
a single key, losing tool calls during merge.
"""
from __future__ import annotations

from api import models
from api.models import (
    _matching_visible_duplicate,
    _session_message_dedup_key,
    _session_message_merge_key,
    _session_message_visible_key,
    merge_session_messages_append_only,
)


def _assistant_tc(tc_id: str, fn_name: str, timestamp=1000) -> dict:
    """Assistant message with tool_calls but empty content."""
    return {
        "role": "assistant",
        "content": "",
        "timestamp": timestamp,
        "tool_calls": [
            {"id": tc_id, "function": {"name": fn_name, "arguments": "{}"}},
        ],
    }


def _tool_result(tc_id: str, name: str, content: str = "ok") -> dict:
    return {
        "role": "tool",
        "tool_call_id": tc_id,
        "name": name,
        "content": content,
    }


# ── _session_message_merge_key ──────────────────────────────────────────────


class TestMergeKeyToolCalls:
    def test_same_tool_calls_produce_same_key(self):
        a = _assistant_tc("call_1", "read_file")
        b = _assistant_tc("call_1", "read_file")
        assert _session_message_merge_key(a) == _session_message_merge_key(b)

    def test_different_tool_calls_produce_different_keys(self):
        a = _assistant_tc("call_1", "read_file")
        b = _assistant_tc("call_2", "terminal")
        assert _session_message_merge_key(a) != _session_message_merge_key(b)

    def test_empty_vs_nonempty_tool_calls_differ(self):
        empty = {"role": "assistant", "content": "", "timestamp": 1000}
        with_tc = _assistant_tc("call_1", "read_file")
        assert _session_message_merge_key(empty) != _session_message_merge_key(with_tc)


# ── _session_message_dedup_key ──────────────────────────────────────────────


class TestDedupKeyToolCalls:
    def test_same_tool_calls_produce_same_key(self):
        a = _assistant_tc("call_1", "read_file")
        b = _assistant_tc("call_1", "read_file")
        assert _session_message_dedup_key(a) == _session_message_dedup_key(b)

    def test_different_tool_calls_produce_different_keys(self):
        a = _assistant_tc("call_1", "read_file")
        b = _assistant_tc("call_2", "terminal")
        assert _session_message_dedup_key(a) != _session_message_dedup_key(b)


# ── _session_message_visible_key + _matching_visible_duplicate ──────────────


class TestVisibleKeyToolCalls:
    def test_same_tool_calls_match(self):
        a = _assistant_tc("call_1", "read_file")
        b = _assistant_tc("call_1", "read_file")
        ka = _session_message_visible_key(a)
        kb = _session_message_visible_key(b)
        assert ka == kb
        assert _matching_visible_duplicate(ka, {kb}) is not None

    def test_different_tool_calls_no_match(self):
        a = _assistant_tc("call_1", "read_file")
        b = _assistant_tc("call_2", "terminal")
        ka = _session_message_visible_key(a)
        kb = _session_message_visible_key(b)
        assert ka != kb
        assert _matching_visible_duplicate(ka, {kb}) is None


# ── merge_session_messages_append_only end-to-end ───────────────────────────


class TestMergeToolCallsEndToEnd:
    def test_same_tool_calls_sidecar_and_state_merge_to_one(self):
        """Sidecar and state.db have the same assistant message with identical
        tool_calls → merge must produce exactly one message (deduplicated)."""
        msg = _assistant_tc("call_1", "read_file", timestamp=1000)
        sidecar = [msg]
        state = [msg]
        result = merge_session_messages_append_only(sidecar, state)
        assert len(result) == 1, f"expected 1 (deduped), got {len(result)}"

    def test_different_tool_calls_sidecar_and_state_both_preserved(self):
        """Sidecar and state.db have assistant messages with different
        tool_calls → merge must preserve both (they are distinct turns)."""
        msg_a = _assistant_tc("call_1", "read_file", timestamp=1000)
        msg_b = _assistant_tc("call_2", "terminal", timestamp=1000)
        sidecar = [msg_a]
        state = [msg_b]
        result = merge_session_messages_append_only(sidecar, state)
        assert len(result) == 2, f"expected 2 (distinct), got {len(result)}"
        tc_ids = {
            m["tool_calls"][0]["id"]
            for m in result
            if m.get("tool_calls")
        }
        assert tc_ids == {"call_1", "call_2"}

    def test_no_tool_calls_still_deduped(self):
        """Messages without tool_calls are deduplicated by legacy key as before."""
        msg = {"role": "assistant", "content": "hello", "timestamp": 1000}
        result = merge_session_messages_append_only([msg], [msg])
        assert len(result) == 1


# ── large-payload duplicate matching performance ────────────────────────────


class TestVisibleDuplicateLargePayloadPerformance:
    def test_large_nonmatching_payload_skips_loose_normalizer(self, monkeypatch):
        """Giant tool/log payloads must not be regex-tokenized for fuzzy matching.

        Exact visible-key equality is checked before this path. For non-exact
        multi-hundred-KB payloads, fuzzy substring/token matching is too costly
        for the /api/session hot path and low-value for deduplication.
        """
        def fail_if_called(_content):
            raise AssertionError("large payloads should not hit loose normalizer")

        monkeypatch.setattr(models, "_loose_session_message_content", fail_if_called)

        large_state = ("state output\n" * 25_000).strip()
        large_sidecar = ("sidecar output\n" * 25_000).strip()
        visible_key = ("assistant", large_state, "")
        sidecar_key = ("assistant", large_sidecar, "")

        assert _matching_visible_duplicate(visible_key, {sidecar_key}) is None

    def test_small_nonmatching_payload_keeps_loose_matching(self, monkeypatch):
        """The large-payload guard must not disable legacy fuzzy matching."""
        calls = []

        def counted_loose(content):
            calls.append(content)
            return " ".join(str(content).lower().replace(",", "").replace("!", "").split())

        monkeypatch.setattr(models, "_loose_session_message_content", counted_loose)

        visible_key = ("assistant", "hello world", "")
        sidecar_key = ("assistant", "HELLO, WORLD!!", "")

        assert _matching_visible_duplicate(visible_key, {sidecar_key}) == sidecar_key
        assert len(calls) == 2
