"""Tests for talk history replay functionality.

Spec source: vectl step ``talk-history-replay``
When `anima talk` attaches to a running agent, replay prior conversation
history from `log.jsonl` before live mode.
"""

import json
from pathlib import Path
from unittest.mock import MagicMock

import pytest
from rich.console import Console

from anima.core.talk_history import (
    filter_conversation_events,
    filter_response_events,
    format_conversation_turns,
    format_history_turns,
)
from anima.shell.cli_commands.talk_deps import default_deps
from anima.shell.cli_commands.talk_session import replay_talk_history


class TestFilterConversationEvents:
    """Tests for filter_conversation_events pure function."""

    def test_empty_list_returns_empty(self):
        """Empty input returns empty output."""
        assert filter_conversation_events([]) == []

    def test_no_conversation_events_returns_empty(self):
        """Non-conversation events are filtered out."""
        entries = [
            {"event": "created", "details": {}},
            {"event": "state_transition", "details": {}},
            {"event": "completed", "details": {}},
        ]
        assert filter_conversation_events(entries) == []

    def test_single_user_message_event(self):
        """Single user_message event is preserved."""
        entries = [
            {
                "event": "user_message",
                "details": {"message_text": "hello", "turn_number": 1},
            },
        ]
        result = filter_conversation_events(entries)
        assert result == entries

    def test_single_response_event(self):
        """Single response event is preserved."""
        entries = [
            {
                "event": "response",
                "details": {"response_text": "hello", "turn_number": 1},
            },
        ]
        result = filter_conversation_events(entries)
        assert result == entries

    def test_interleaved_user_and_response(self):
        """User messages and responses are interleaved."""
        entries = [
            {"event": "created"},
            {
                "event": "user_message",
                "details": {"message_text": "question", "turn_number": 1},
            },
            {
                "event": "response",
                "details": {"response_text": "answer", "turn_number": 1},
            },
            {"event": "state_transition"},
            {
                "event": "user_message",
                "details": {"message_text": "followup", "turn_number": 2},
            },
        ]
        result = filter_conversation_events(entries)
        assert len(result) == 3
        assert result[0]["event"] == "user_message"
        assert result[1]["event"] == "response"
        assert result[2]["event"] == "user_message"

    def test_preserves_order(self):
        """Conversation events preserve original order."""
        entries = [
            {"event": "user_message", "details": {"turn_number": 1}},
            {"event": "response", "details": {"turn_number": 1}},
            {"event": "user_message", "details": {"turn_number": 2}},
        ]
        result = filter_conversation_events(entries)
        assert result == entries


class TestFilterResponseEvents:
    """Tests for filter_response_events pure function."""

    def test_empty_list_returns_empty(self):
        """Empty input returns empty output."""
        assert filter_response_events([]) == []

    def test_no_response_events_returns_empty(self):
        """Non-response events are filtered out."""
        entries = [
            {"event": "created", "details": {}},
            {"event": "state_transition", "details": {}},
            {"event": "completed", "details": {}},
        ]
        assert filter_response_events(entries) == []

    def test_single_response_event(self):
        """Single response event is preserved."""
        entries = [
            {
                "event": "response",
                "details": {"response_text": "hello", "turn_number": 1},
            },
        ]
        result = filter_response_events(entries)
        assert result == entries

    def test_mixed_events_filters_correctly(self):
        """Mixed events filter to only response events."""
        entries = [
            {"event": "created"},
            {
                "event": "response",
                "details": {"response_text": "hello", "turn_number": 1},
            },
            {"event": "state_transition"},
            {
                "event": "response",
                "details": {"response_text": "world", "turn_number": 2},
            },
            {"event": "approval_requested"},
        ]
        result = filter_response_events(entries)
        assert len(result) == 2
        assert result[0]["event"] == "response"
        assert result[1]["event"] == "response"

    def test_preserves_order(self):
        """Response events preserve original order."""
        entries = [
            {"event": "response", "details": {"turn_number": 5}},
            {"event": "response", "details": {"turn_number": 1}},
            {"event": "response", "details": {"turn_number": 3}},
        ]
        result = filter_response_events(entries)
        assert result == entries


class TestFormatConversationTurns:
    """Tests for format_conversation_turns pure function."""

    def test_empty_list_returns_empty(self):
        """Empty input returns empty output."""
        assert format_conversation_turns([]) == []

    def test_single_user_turn(self):
        """Single user message formats to tuple."""
        entries = [
            {
                "event": "user_message",
                "details": {"message_text": "hello", "turn_number": 1},
            },
        ]
        result = format_conversation_turns(entries)
        assert result == [("user", 1, "hello")]

    def test_single_agent_turn(self):
        """Single response formats to tuple."""
        entries = [
            {
                "event": "response",
                "details": {"response_text": "hello", "turn_number": 1},
            },
        ]
        result = format_conversation_turns(entries)
        assert result == [("agent", 1, "hello")]

    def test_interleaved_turns(self):
        """Interleaved user and agent messages format correctly."""
        entries = [
            {
                "event": "user_message",
                "details": {"message_text": "question", "turn_number": 1},
            },
            {
                "event": "response",
                "details": {"response_text": "answer", "turn_number": 1},
            },
            {
                "event": "user_message",
                "details": {"message_text": "followup", "turn_number": 2},
            },
            {
                "event": "response",
                "details": {"response_text": "response2", "turn_number": 2},
            },
        ]
        result = format_conversation_turns(entries)
        assert result == [
            ("user", 1, "question"),
            ("agent", 1, "answer"),
            ("user", 2, "followup"),
            ("agent", 2, "response2"),
        ]

    def test_missing_details_graceful_placeholder(self):
        """Missing message_text uses placeholder."""
        entries = [
            {"event": "user_message", "details": {"turn_number": 1}},
            {"event": "response", "details": {"turn_number": 2}},
        ]
        result = format_conversation_turns(entries)
        assert result == [
            ("user", 1, "[no message text]"),
            ("agent", 2, "[no response text]"),
        ]

    def test_missing_details_skipped(self):
        """Entries without details dict are skipped."""
        entries = [
            {"event": "user_message"},
            {"event": "response"},
            {
                "event": "user_message",
                "details": {"message_text": "valid", "turn_number": 1},
            },
        ]
        result = format_conversation_turns(entries)
        assert result == [("user", 1, "valid")]

    def test_legacy_top_level_turn_number_uses_placeholder(self):
        """Legacy user events with top-level turn_number are replayed."""
        entries = [{"event": "user_message", "turn_number": 1}]
        result = format_conversation_turns(entries)
        assert result == [("user", 1, "[no message text]")]

    def test_legacy_top_level_response_fields_replayed(self):
        """Legacy response events with top-level fields are replayed."""
        entries = [
            {"event": "response", "turn_number": 2, "response_text": "legacy output"}
        ]
        result = format_conversation_turns(entries)
        assert result == [("agent", 2, "legacy output")]

    def test_legacy_content_and_turn_fields_replayed(self):
        """Legacy logs using content+turn are replayed without omission."""
        entries = [
            {"event": "user_message", "details": {"content": "old user", "turn": 1}},
            {"event": "response", "details": {"text": "old reply", "turn": 1}},
        ]
        with pytest.warns(DeprecationWarning, match="Legacy talk history field"):
            result = format_conversation_turns(entries)
        assert result == [("user", 1, "old user"), ("agent", 1, "old reply")]

    def test_missing_turn_number_skipped(self):
        """Entries without turn_number are skipped."""
        entries = [
            {"event": "user_message", "details": {"message_text": "no turn"}},
            {"event": "response", "details": {"response_text": "also no turn"}},
            {
                "event": "user_message",
                "details": {"message_text": "valid", "turn_number": 3},
            },
        ]
        result = format_conversation_turns(entries)
        assert result == [("user", 3, "valid")]

    def test_wrong_types_graceful_placeholder(self):
        """Wrong types for fields use placeholders."""
        entries = [
            {
                "event": "user_message",
                "details": {"message_text": "valid", "turn_number": 1},
            },
            {
                "event": "user_message",
                "details": {"message_text": 123, "turn_number": 2},
            },
            {
                "event": "response",
                "details": {"response_text": None, "turn_number": 3},
            },
        ]
        result = format_conversation_turns(entries)
        assert result == [
            ("user", 1, "valid"),
            ("user", 2, "[no message text]"),
            ("agent", 3, "[no response text]"),
        ]


class TestFormatHistoryTurns:
    """Tests for format_history_turns pure function."""

    def test_empty_list_returns_empty(self):
        """Empty input returns empty output."""
        assert format_history_turns([]) == []

    def test_single_turn(self):
        """Single response formats to tuple."""
        entries = [
            {
                "event": "response",
                "details": {"response_text": "hello", "turn_number": 1},
            },
        ]
        result = format_history_turns(entries)
        assert result == [(1, "hello")]

    def test_multiple_turns(self):
        """Multiple responses format to list of tuples."""
        entries = [
            {
                "event": "response",
                "details": {"response_text": "first", "turn_number": 1},
            },
            {
                "event": "response",
                "details": {"response_text": "second", "turn_number": 2},
            },
            {
                "event": "response",
                "details": {"response_text": "third", "turn_number": 3},
            },
        ]
        result = format_history_turns(entries)
        assert result == [(1, "first"), (2, "second"), (3, "third")]

    def test_missing_details_skipped(self):
        """Entries without details dict are skipped."""
        entries = [
            {"event": "response"},
            {
                "event": "response",
                "details": {"response_text": "valid", "turn_number": 1},
            },
        ]
        result = format_history_turns(entries)
        assert result == [(1, "valid")]

    def test_missing_fields_skipped(self):
        """Entries with missing required fields are skipped."""
        entries = [
            {"event": "response", "details": {"response_text": "no turn"}},
            {"event": "response", "details": {"turn_number": 5}},
            {
                "event": "response",
                "details": {"response_text": "valid", "turn_number": 2},
            },
        ]
        result = format_history_turns(entries)
        assert result == [(2, "valid")]

    def test_wrong_types_skipped(self):
        """Entries with wrong types for fields are skipped."""
        entries = [
            {
                "event": "response",
                "details": {"response_text": "valid", "turn_number": "not an int"},
            },
            {"event": "response", "details": {"response_text": 123, "turn_number": 1}},
            {
                "event": "response",
                "details": {"response_text": "valid", "turn_number": 3},
            },
        ]
        result = format_history_turns(entries)
        assert result == [(3, "valid")]


class TestReplayTalkHistory:
    """Tests for replay_talk_history shell function."""

    def test_no_history_outputs_nothing(self, tmp_path: Path):
        """Agent with no log outputs nothing."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)

        console = MagicMock(spec=Console)
        replay_talk_history(
            agent_id="test-agent",
            json_output=False,
            console=console,
            deps=default_deps(),
        )

        # No calls to console.print
        assert console.print.call_count == 0
        assert console.print_json.call_count == 0

    def test_empty_history_outputs_nothing(self, tmp_path: Path, monkeypatch):
        """Agent with empty log outputs nothing."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"
        log_file.write_text("")

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        replay_talk_history(
            agent_id="test-agent",
            json_output=False,
            console=console,
            deps=default_deps(),
        )

        # No calls to console.print
        assert console.print.call_count == 0

    def test_single_conversation_turn_human_output(self, tmp_path: Path, monkeypatch):
        """Single conversation event outputs turn with speaker."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"

        log_file.write_text(
            json.dumps({"event": "created", "details": {}})
            + "\n"
            + json.dumps(
                {
                    "event": "user_message",
                    "details": {"message_text": "Hello", "turn_number": 1},
                }
            )
            + "\n"
            + json.dumps(
                {
                    "event": "response",
                    "details": {"response_text": "World", "turn_number": 1},
                }
            )
            + "\n"
        )

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        replay_talk_history(
            agent_id="test-agent",
            json_output=False,
            console=console,
            deps=default_deps(),
        )

        # New visual format prints user line + persona label + markdown + separator
        assert console.print.call_count == 4
        calls = [str(call) for call in console.print.call_args_list]
        assert "--- Hello" in calls[0]
        assert "Hello" in calls[0]
        assert "agent" in calls[1].lower()
        assert "World" in calls[2]
        assert "--------- live ---------" in calls[3]

    def test_multiple_turns_human_output(self, tmp_path: Path, monkeypatch):
        """Multiple turns output all with speaker labels."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"

        entries = [
            {"event": "created"},
            {
                "event": "user_message",
                "details": {"message_text": "Q1", "turn_number": 1},
            },
            {
                "event": "response",
                "details": {"response_text": "A1", "turn_number": 1},
            },
            {"event": "state_transition"},
            {
                "event": "user_message",
                "details": {"message_text": "Q2", "turn_number": 2},
            },
            {
                "event": "response",
                "details": {"response_text": "A2", "turn_number": 2},
            },
        ]
        log_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        replay_talk_history(
            agent_id="test-agent",
            json_output=False,
            console=console,
            deps=default_deps(),
        )

        # New visual format prints each agent turn as label + markdown line
        assert console.print.call_count == 7
        calls = [str(call) for call in console.print.call_args_list]
        assert "--- Q1" in calls[0]
        assert "Q1" in calls[0]
        assert "agent" in calls[1].lower()
        assert "A1" in calls[2]
        assert "--- Q2" in calls[3]
        assert "Q2" in calls[3]
        assert "agent" in calls[4].lower()
        assert "A2" in calls[5]
        assert "--------- live ---------" in calls[6]

    def test_json_output_format(self, tmp_path: Path, monkeypatch):
        """JSON output formats history as structured data."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"

        entries = [
            {
                "event": "user_message",
                "details": {"message_text": "Question", "turn_number": 1},
            },
            {
                "event": "response",
                "details": {"response_text": "Answer", "turn_number": 1},
            },
        ]
        log_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        # Create a mock file object to capture writes
        console.file = MagicMock()
        console.file.write = MagicMock()
        replay_talk_history(
            agent_id="test-agent",
            json_output=True,
            console=console,
            deps=default_deps(),
        )

        # Should call file.write twice: history_replay + history_replay_complete
        assert console.file.write.call_count == 2

        # First call: history_replay event
        first_write = console.file.write.call_args_list[0][0][0]
        first_data = json.loads(first_write)
        assert first_data["event"] == "history_replay"
        assert first_data["agent_id"] == "test-agent"
        assert len(first_data["turns"]) == 2
        assert first_data["turns"][0] == {
            "speaker": "user",
            "turn_number": 1,
            "text": "Question",
        }
        assert first_data["turns"][1] == {
            "speaker": "agent",
            "turn_number": 1,
            "text": "Answer",
        }

        # Second call: history_replay_complete event
        second_write = console.file.write.call_args_list[1][0][0]
        second_data = json.loads(second_write)
        assert second_data["event"] == "history_replay_complete"

    def test_old_log_without_message_text_graceful_placeholder(
        self, tmp_path: Path, monkeypatch
    ):
        """Old logs without message_text show placeholder."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"

        entries = [
            {"event": "user_message", "details": {"turn_number": 1}},
            {
                "event": "response",
                "details": {"response_text": "Still works", "turn_number": 1},
            },
        ]
        log_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        replay_talk_history(
            agent_id="test-agent",
            json_output=False,
            console=console,
            deps=default_deps(),
        )

        # New visual format prints user line + persona label + markdown + separator
        assert console.print.call_count == 4
        calls = [str(call) for call in console.print.call_args_list]
        assert "---" in calls[0]
        assert "no message text" in calls[0]
        assert "agent" in calls[1].lower()
        assert "Still works" in calls[2]

    def test_legacy_log_top_level_turn_number_includes_user_message(
        self, tmp_path: Path, monkeypatch
    ):
        """Legacy user_message without details still appears in replay."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"

        entries = [
            {"event": "user_message", "turn_number": 1},
            {
                "event": "response",
                "details": {"response_text": "Reply", "turn_number": 1},
            },
        ]
        log_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        replay_talk_history(
            agent_id="test-agent",
            json_output=False,
            console=console,
            deps=default_deps(),
        )

        assert console.print.call_count == 4
        calls = [str(call) for call in console.print.call_args_list]
        assert "---" in calls[0]
        assert "no message text" in calls[0]
        assert "agent" in calls[1].lower()
        assert "Reply" in calls[2]

    def test_legacy_log_content_field_includes_user_message(
        self, tmp_path: Path, monkeypatch
    ):
        """Legacy logs using content field preserve user replay text."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"

        entries = [
            {
                "event": "user_message",
                "details": {"content": "Legacy hello", "turn": 1},
            },
            {"event": "response", "details": {"response_text": "Reply", "turn": 1}},
        ]
        log_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        with pytest.warns(DeprecationWarning, match="Legacy talk history field"):
            replay_talk_history(
                agent_id="test-agent",
                json_output=False,
                console=console,
                deps=default_deps(),
            )

        assert console.print.call_count == 4
        calls = [str(call) for call in console.print.call_args_list]
        assert "--- Legacy hello" in calls[0]
        assert "Legacy hello" in calls[0]
        assert "agent" in calls[1].lower()
        assert "Reply" in calls[2]

    def test_mixed_events_filters_correctly(self, tmp_path: Path, monkeypatch):
        """Only conversation events (user_message, response) are shown."""
        agent_dir = tmp_path / "agents" / "test-agent"
        agent_dir.mkdir(parents=True)
        log_file = agent_dir / "log.jsonl"

        entries = [
            {"event": "created"},
            {
                "event": "user_message",
                "details": {"message_text": "Visible", "turn_number": 1},
            },
            {"event": "approval_requested"},
            {
                "event": "response",
                "details": {"response_text": "Also visible", "turn_number": 1},
            },
            {"event": "state_transition"},
        ]
        log_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")

        monkeypatch.setenv("ANIMA_STATE", str(tmp_path))

        console = MagicMock(spec=Console)
        replay_talk_history(
            agent_id="test-agent",
            json_output=False,
            console=console,
            deps=default_deps(),
        )

        # New visual format prints user line + persona label + markdown + separator
        assert console.print.call_count == 4
        calls = [str(call) for call in console.print.call_args_list]
        assert "Visible" in calls[0]
        assert "agent" in calls[1].lower()
        assert "Also visible" in calls[2]
