"""Unit tests for getpatter.services.chat_context — ChatMessage and ChatContext."""

from __future__ import annotations

import time

import pytest

from getpatter.services.chat_context import ChatContext, ChatMessage


# ---------------------------------------------------------------------------
# Constructor
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestConstructor:
    """ChatContext constructor tests."""

    def test_empty_context(self) -> None:
        ctx = ChatContext()
        assert ctx.length == 0
        assert ctx.get_messages() == ()

    def test_with_system_prompt(self) -> None:
        ctx = ChatContext("You are a helpful assistant.")
        assert ctx.length == 1
        msgs = ctx.get_messages()
        assert msgs[0].role == "system"
        assert msgs[0].content == "You are a helpful assistant."


# ---------------------------------------------------------------------------
# Add messages
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAddMessages:
    """Tests for add_user, add_assistant, add_system, add_tool_result."""

    def test_add_user(self) -> None:
        ctx = ChatContext()
        msg = ctx.add_user("Hello")
        assert msg.role == "user"
        assert msg.content == "Hello"
        assert ctx.length == 1

    def test_add_assistant(self) -> None:
        ctx = ChatContext()
        msg = ctx.add_assistant("Hi there!")
        assert msg.role == "assistant"
        assert msg.content == "Hi there!"
        assert ctx.length == 1

    def test_add_system(self) -> None:
        ctx = ChatContext()
        msg = ctx.add_system("New instructions")
        assert msg.role == "system"
        assert msg.content == "New instructions"
        assert ctx.length == 1

    def test_add_tool_result(self) -> None:
        ctx = ChatContext()
        msg = ctx.add_tool_result("result data", "call_abc123")
        assert msg.role == "tool"
        assert msg.content == "result data"
        assert msg.tool_call_id == "call_abc123"
        assert ctx.length == 1


# ---------------------------------------------------------------------------
# Auto-generated IDs
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestAutoGeneratedIDs:
    """Messages should have unique auto-generated IDs."""

    def test_unique_ids(self) -> None:
        ctx = ChatContext()
        msg1 = ctx.add_user("one")
        msg2 = ctx.add_user("two")
        msg3 = ctx.add_assistant("three")
        assert msg1.id != msg2.id
        assert msg2.id != msg3.id
        assert msg1.id != msg3.id

    def test_id_format(self) -> None:
        ctx = ChatContext()
        msg = ctx.add_user("test")
        assert len(msg.id) == 12
        # Hex string
        int(msg.id, 16)


# ---------------------------------------------------------------------------
# Timestamps
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestTimestamps:
    """Messages should have timestamps close to now."""

    def test_timestamp_close_to_now(self) -> None:
        before = time.time()
        ctx = ChatContext()
        msg = ctx.add_user("test")
        after = time.time()
        assert before <= msg.timestamp <= after


# ---------------------------------------------------------------------------
# get_messages
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetMessages:
    """get_messages returns an immutable tuple."""

    def test_returns_tuple(self) -> None:
        ctx = ChatContext("system")
        ctx.add_user("hello")
        msgs = ctx.get_messages()
        assert isinstance(msgs, tuple)
        assert len(msgs) == 2

    def test_messages_are_frozen(self) -> None:
        ctx = ChatContext()
        msg = ctx.add_user("hello")
        with pytest.raises(AttributeError):
            msg.content = "modified"  # type: ignore[misc]


# ---------------------------------------------------------------------------
# get_last_n
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetLastN:
    """get_last_n returns the correct slice."""

    @pytest.fixture()
    def ctx(self) -> ChatContext:
        c = ChatContext("system prompt")
        c.add_user("msg1")
        c.add_assistant("msg2")
        c.add_user("msg3")
        return c

    def test_returns_last_n(self, ctx: ChatContext) -> None:
        last2 = ctx.get_last_n(2)
        assert len(last2) == 2
        assert last2[0].content == "msg2"
        assert last2[1].content == "msg3"

    def test_returns_all_when_n_exceeds_length(self, ctx: ChatContext) -> None:
        all_msgs = ctx.get_last_n(100)
        assert len(all_msgs) == 4

    def test_returns_empty_when_n_is_zero(self, ctx: ChatContext) -> None:
        assert ctx.get_last_n(0) == ()

    def test_returns_empty_when_n_is_negative(self, ctx: ChatContext) -> None:
        assert ctx.get_last_n(-1) == ()


# ---------------------------------------------------------------------------
# truncate
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestTruncate:
    """Truncation preserves system message + last N."""

    def test_preserves_system_plus_last_n(self) -> None:
        ctx = ChatContext("You are helpful.")
        ctx.add_user("msg1")
        ctx.add_assistant("msg2")
        ctx.add_user("msg3")
        ctx.add_assistant("msg4")

        ctx.truncate(2)

        msgs = ctx.get_messages()
        assert len(msgs) == 3  # system + last 2
        assert msgs[0].role == "system"
        assert msgs[0].content == "You are helpful."
        assert msgs[1].content == "msg3"
        assert msgs[2].content == "msg4"

    def test_keeps_last_n_without_system(self) -> None:
        ctx = ChatContext()
        ctx.add_user("msg1")
        ctx.add_assistant("msg2")
        ctx.add_user("msg3")

        ctx.truncate(2)

        msgs = ctx.get_messages()
        assert len(msgs) == 2
        assert msgs[0].content == "msg2"
        assert msgs[1].content == "msg3"

    def test_noop_when_max_exceeds_count(self) -> None:
        ctx = ChatContext("sys")
        ctx.add_user("hi")
        ctx.truncate(100)
        assert ctx.length == 2

    def test_keeps_only_system_when_max_is_zero(self) -> None:
        ctx = ChatContext("sys")
        ctx.add_user("msg1")
        ctx.add_assistant("msg2")

        ctx.truncate(0)

        msgs = ctx.get_messages()
        assert len(msgs) == 1
        assert msgs[0].role == "system"

    def test_empties_without_system_when_max_is_zero(self) -> None:
        ctx = ChatContext()
        ctx.add_user("msg1")

        ctx.truncate(0)

        assert ctx.length == 0

    def test_negative_max_is_noop(self) -> None:
        ctx = ChatContext()
        ctx.add_user("msg1")
        ctx.truncate(-1)
        assert ctx.length == 1


# ---------------------------------------------------------------------------
# to_openai
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestToOpenAI:
    """to_openai converts messages to OpenAI format."""

    def test_basic_conversion(self) -> None:
        ctx = ChatContext("You are helpful.")
        ctx.add_user("Hello")
        ctx.add_assistant("Hi!")

        openai = ctx.to_openai()

        assert openai == [
            {"role": "system", "content": "You are helpful."},
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi!"},
        ]

    def test_includes_tool_call_id(self) -> None:
        ctx = ChatContext()
        ctx.add_tool_result("result", "call_123")

        openai = ctx.to_openai()

        assert openai[0] == {
            "role": "tool",
            "content": "result",
            "tool_call_id": "call_123",
        }

    def test_omits_optional_fields_when_not_set(self) -> None:
        ctx = ChatContext()
        ctx.add_user("hello")

        openai = ctx.to_openai()

        assert openai[0] == {"role": "user", "content": "hello"}
        assert "name" not in openai[0]
        assert "tool_call_id" not in openai[0]


# ---------------------------------------------------------------------------
# to_anthropic
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestToAnthropic:
    """to_anthropic separates system message."""

    def test_separates_system(self) -> None:
        ctx = ChatContext("You are helpful.")
        ctx.add_user("Hello")
        ctx.add_assistant("Hi!")

        result = ctx.to_anthropic()

        assert result["system"] == "You are helpful."
        assert result["messages"] == [
            {"role": "user", "content": "Hello"},
            {"role": "assistant", "content": "Hi!"},
        ]

    def test_none_system_when_absent(self) -> None:
        ctx = ChatContext()
        ctx.add_user("Hello")

        result = ctx.to_anthropic()

        assert result["system"] is None
        assert len(result["messages"]) == 1

    def test_uses_first_system_only(self) -> None:
        ctx = ChatContext("first system")
        ctx.add_user("user msg")
        ctx.add_system("second system")

        result = ctx.to_anthropic()

        assert result["system"] == "first system"
        # Second system message is excluded
        assert len(result["messages"]) == 1
        assert result["messages"][0]["content"] == "user msg"

    def test_tool_messages_included(self) -> None:
        ctx = ChatContext()
        ctx.add_user("call the tool")
        ctx.add_tool_result("tool output", "call_456")

        result = ctx.to_anthropic()

        assert result["messages"][1] == {"role": "tool", "content": "tool output"}


# ---------------------------------------------------------------------------
# copy
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestCopy:
    """copy creates an independent deep copy."""

    def test_independent_copy(self) -> None:
        ctx = ChatContext("system")
        ctx.add_user("hello")
        ctx.add_assistant("hi")

        cloned = ctx.copy()

        assert cloned.length == ctx.length
        assert [m.content for m in cloned.get_messages()] == [
            m.content for m in ctx.get_messages()
        ]

        # Mutating clone does not affect original
        cloned.add_user("new message")
        assert cloned.length == 4
        assert ctx.length == 3

    def test_preserves_message_ids(self) -> None:
        ctx = ChatContext()
        msg = ctx.add_user("test")
        cloned = ctx.copy()
        assert cloned.get_messages()[0].id == msg.id


# ---------------------------------------------------------------------------
# Serialization (to_json / from_json)
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestSerialization:
    """Round-trip serialization."""

    def test_round_trip(self) -> None:
        ctx = ChatContext("system prompt")
        ctx.add_user("hello")
        ctx.add_assistant("world")
        ctx.add_tool_result("data", "call_789")

        json_data = ctx.to_json()
        restored = ChatContext.from_json(json_data)

        assert restored.length == ctx.length

        original_msgs = ctx.get_messages()
        restored_msgs = restored.get_messages()

        for i in range(len(original_msgs)):
            assert restored_msgs[i].id == original_msgs[i].id
            assert restored_msgs[i].role == original_msgs[i].role
            assert restored_msgs[i].content == original_msgs[i].content
            assert restored_msgs[i].timestamp == original_msgs[i].timestamp
            assert restored_msgs[i].tool_call_id == original_msgs[i].tool_call_id

    def test_from_json_empty_messages(self) -> None:
        ctx = ChatContext.from_json({"messages": []})
        assert ctx.length == 0

    def test_from_json_missing_messages_key(self) -> None:
        ctx = ChatContext.from_json({})
        assert ctx.length == 0


# ---------------------------------------------------------------------------
# Empty context
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestEmptyContext:
    """Edge cases on an empty context."""

    def test_get_messages_empty(self) -> None:
        ctx = ChatContext()
        assert ctx.get_messages() == ()

    def test_get_last_n_empty(self) -> None:
        ctx = ChatContext()
        assert ctx.get_last_n(5) == ()

    def test_length_zero(self) -> None:
        ctx = ChatContext()
        assert ctx.length == 0

    def test_truncate_safe(self) -> None:
        ctx = ChatContext()
        ctx.truncate(5)
        assert ctx.length == 0

    def test_to_openai_empty(self) -> None:
        ctx = ChatContext()
        assert ctx.to_openai() == []

    def test_to_anthropic_empty(self) -> None:
        ctx = ChatContext()
        result = ctx.to_anthropic()
        assert result["system"] is None
        assert result["messages"] == []

    def test_copy_empty_independent(self) -> None:
        ctx = ChatContext()
        cloned = ctx.copy()
        cloned.add_user("test")
        assert ctx.length == 0
        assert cloned.length == 1
