"""Tests for ForkableThread."""

from __future__ import annotations

import pytest

from koan.memory.fork import ForkableThread


class TestForkableThread:
    def test_append_and_messages(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="Hello")
        thread.append(role="assistant", content="Hi!")

        assert len(thread) == 2
        assert thread.messages[0].content == "Hello"
        assert thread.messages[1].content == "Hi!"

    def test_checkpoint_and_rewind(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="msg-1")
        thread.checkpoint("cp1")
        thread.append(role="assistant", content="msg-2")
        thread.append(role="user", content="msg-3")

        assert len(thread) == 3

        thread.rewind_to("cp1")
        assert len(thread) == 1
        assert thread.messages[0].content == "msg-1"

    def test_rewind_removes_later_checkpoints(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="a")
        thread.checkpoint("cp1")
        thread.append(role="user", content="b")
        thread.checkpoint("cp2")
        thread.append(role="user", content="c")

        thread.rewind_to("cp1")
        # cp2 should be gone since it was after cp1
        with pytest.raises(KeyError, match="Checkpoint not found"):
            thread.rewind_to("cp2")

    def test_fork_at_creates_independent_branch(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="msg-1")
        thread.checkpoint("cp1")
        thread.append(role="assistant", content="msg-2")

        fork = thread.fork_at("cp1")
        assert len(fork) == 1
        assert fork.messages[0].content == "msg-1"

        # Modify fork
        fork.append(role="assistant", content="different response")
        assert len(fork) == 2

        # Original unchanged
        assert len(thread) == 2
        assert thread.messages[1].content == "msg-2"

    def test_fork_at_deep_copies(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="original", metadata={"key": "val"})
        thread.checkpoint("cp1")

        fork = thread.fork_at("cp1")
        # Modify fork's message metadata
        fork.messages[0].metadata["key"] = "modified"

        # Original should be unaffected (deep copy)
        assert thread.messages[0].metadata["key"] == "val"

    def test_fork_from_current_position(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="a")
        thread.append(role="user", content="b")

        fork = thread.fork()
        assert len(fork) == 2

        fork.append(role="user", content="c")
        assert len(fork) == 3
        assert len(thread) == 2

    def test_fork_at_unknown_checkpoint_raises(self) -> None:
        thread = ForkableThread("t1")
        with pytest.raises(KeyError, match="Checkpoint not found"):
            thread.fork_at("nonexistent")

    def test_rewind_unknown_checkpoint_raises(self) -> None:
        thread = ForkableThread("t1")
        with pytest.raises(KeyError, match="Checkpoint not found"):
            thread.rewind_to("nonexistent")

    def test_messages_returns_copy(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="test")

        msgs = thread.messages
        msgs.clear()
        # Internal state unaffected
        assert len(thread) == 1

    def test_append_returns_message(self) -> None:
        thread = ForkableThread("t1")
        msg = thread.append(role="user", content="hello")
        assert msg.role == "user"
        assert msg.content == "hello"
        assert msg.thread_id == "t1"

    def test_multiple_checkpoints(self) -> None:
        thread = ForkableThread("t1")
        thread.append(role="user", content="a")
        thread.checkpoint("cp1")
        thread.append(role="user", content="b")
        thread.checkpoint("cp2")
        thread.append(role="user", content="c")
        thread.checkpoint("cp3")

        thread.rewind_to("cp2")
        assert len(thread) == 2
        assert thread.messages[-1].content == "b"

        thread.rewind_to("cp1")
        assert len(thread) == 1


class TestForkableThreadCallbacks:
    def test_fork_at_fires_callback(self) -> None:
        events: list[tuple[str, str, str]] = []
        thread = ForkableThread("t1", on_fork=lambda *a: events.append(a))
        thread.append(role="user", content="msg")
        thread.checkpoint("cp1")
        thread.fork_at("cp1")
        assert len(events) == 1
        assert events[0] == ("t1", "fork", "cp1")

    def test_fork_fires_callback(self) -> None:
        events: list[tuple[str, str, str]] = []
        thread = ForkableThread("t1", on_fork=lambda *a: events.append(a))
        thread.append(role="user", content="msg")
        thread.fork()
        assert len(events) == 1
        assert events[0] == ("t1", "fork", "_current")

    def test_rewind_fires_callback(self) -> None:
        events: list[tuple[str, str, str]] = []
        thread = ForkableThread("t1", on_fork=lambda *a: events.append(a))
        thread.append(role="user", content="msg")
        thread.checkpoint("cp1")
        thread.append(role="assistant", content="resp")
        thread.rewind_to("cp1")
        assert len(events) == 1
        assert events[0] == ("t1", "rewind", "cp1")

    def test_no_callback_when_none(self) -> None:
        thread = ForkableThread("t1")  # no callback
        thread.append(role="user", content="msg")
        thread.checkpoint("cp1")
        thread.fork_at("cp1")
        thread.rewind_to("cp1")
        # Should not raise

    def test_multiple_events(self) -> None:
        events: list[tuple[str, str, str]] = []
        thread = ForkableThread("t1", on_fork=lambda *a: events.append(a))
        thread.append(role="user", content="msg")
        thread.checkpoint("cp1")
        thread.append(role="assistant", content="resp")
        thread.fork_at("cp1")
        thread.rewind_to("cp1")
        thread.fork()
        assert len(events) == 3
