"""Tests for Feature 19: Session Persistence & Resume.

Tests the SessionStore class and message serialization/deserialization
round-trips for all LangChain message types.

Note: langchain_core may not be installed in the test environment,
so we use lightweight mock message classes for serialization tests
and skip deserialization tests that require the real import.
"""

import json
import sys
import time
import types
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from unittest.mock import patch

import pytest

# ---------------------------------------------------------------------------
# Mock LangChain message classes (lightweight stand-ins)
# ---------------------------------------------------------------------------

@dataclass
class _MockHumanMessage:
    content: str = ""
    additional_kwargs: dict = field(default_factory=dict)

    @property
    def __class_name__(self):
        return "HumanMessage"

# Trick: make type(msg).__name__ == "HumanMessage"
_MockHumanMessage.__name__ = "HumanMessage"
_MockHumanMessage.__qualname__ = "HumanMessage"


@dataclass
class _MockAIMessage:
    content: str = ""
    tool_calls: list = field(default_factory=list)
    additional_kwargs: dict = field(default_factory=dict)

_MockAIMessage.__name__ = "AIMessage"
_MockAIMessage.__qualname__ = "AIMessage"


@dataclass
class _MockToolMessage:
    content: str = ""
    tool_call_id: str = ""
    additional_kwargs: dict = field(default_factory=dict)

_MockToolMessage.__name__ = "ToolMessage"
_MockToolMessage.__qualname__ = "ToolMessage"


@dataclass
class _MockSystemMessage:
    content: str = ""
    additional_kwargs: dict = field(default_factory=dict)

_MockSystemMessage.__name__ = "SystemMessage"
_MockSystemMessage.__qualname__ = "SystemMessage"


def _install_mock_langchain():
    """Install a mock langchain_core.messages module so _deserialize_message works."""
    mock_messages = types.ModuleType("langchain_core.messages")
    mock_messages.HumanMessage = _MockHumanMessage
    mock_messages.AIMessage = _MockAIMessage
    mock_messages.ToolMessage = _MockToolMessage
    mock_messages.SystemMessage = _MockSystemMessage

    mock_core = types.ModuleType("langchain_core")
    mock_core.messages = mock_messages

    sys.modules["langchain_core"] = mock_core
    sys.modules["langchain_core.messages"] = mock_messages


# Install mocks before importing session_store (which has a lazy import in _deserialize_message)
_HAS_LANGCHAIN = "langchain_core" in sys.modules
if not _HAS_LANGCHAIN:
    try:
        import langchain_core  # noqa: F401
        _HAS_LANGCHAIN = True
    except ImportError:
        _install_mock_langchain()

from autodebug.agent.session_store import (
    SessionMeta,
    SessionStore,
    _deserialize_message,
    _serialize_message,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture()
def tmp_sessions(tmp_path):
    """Patch _sessions_dir to use a temporary directory."""
    sessions_dir = tmp_path / "sessions"
    sessions_dir.mkdir()
    with patch("autodebug.agent.session_store._sessions_dir", return_value=sessions_dir):
        yield sessions_dir


@pytest.fixture()
def store(tmp_sessions):
    return SessionStore()


@pytest.fixture()
def sample_meta():
    return SessionMeta(
        session_id="abc123",
        created_at=1700000000.0,
        updated_at=1700000010.0,
        mode="debug",
        model="sonnet-4",
        cwd="/tmp/project",
        git_branch="main",
        preview="fix the login bug",
        turn_count=5,
        token_total=12345,
    )


# ---------------------------------------------------------------------------
# Message serialization round-trip
# ---------------------------------------------------------------------------

class TestMessageSerialization:

    def test_human_message_roundtrip(self):
        original = _MockHumanMessage(content="hello world")
        data = _serialize_message(original)
        assert data["type"] == "HumanMessage"
        assert data["content"] == "hello world"

        restored = _deserialize_message(data)
        assert type(restored).__name__ == "HumanMessage"
        assert restored.content == "hello world"

    def test_ai_message_roundtrip(self):
        original = _MockAIMessage(content="I'll help you fix that.")
        data = _serialize_message(original)
        assert data["type"] == "AIMessage"
        assert data["content"] == "I'll help you fix that."

        restored = _deserialize_message(data)
        assert type(restored).__name__ == "AIMessage"
        assert restored.content == "I'll help you fix that."

    def test_ai_message_with_tool_calls_roundtrip(self):
        original = _MockAIMessage(
            content="Let me check",
            tool_calls=[
                {"id": "call_123", "name": "read_file", "args": {"path": "foo.py"}},
            ],
        )
        data = _serialize_message(original)
        assert "tool_calls" in data
        assert data["tool_calls"][0]["name"] == "read_file"

        restored = _deserialize_message(data)
        assert type(restored).__name__ == "AIMessage"
        assert len(restored.tool_calls) == 1
        assert restored.tool_calls[0]["name"] == "read_file"
        assert restored.tool_calls[0]["args"]["path"] == "foo.py"

    def test_tool_message_roundtrip(self):
        original = _MockToolMessage(content="file contents here", tool_call_id="call_123")
        data = _serialize_message(original)
        assert data["tool_call_id"] == "call_123"

        restored = _deserialize_message(data)
        assert type(restored).__name__ == "ToolMessage"
        assert restored.content == "file contents here"
        assert restored.tool_call_id == "call_123"

    def test_system_message_roundtrip(self):
        original = _MockSystemMessage(content="You are a debugger.")
        data = _serialize_message(original)
        assert data["type"] == "SystemMessage"

        restored = _deserialize_message(data)
        assert type(restored).__name__ == "SystemMessage"
        assert restored.content == "You are a debugger."

    def test_unknown_type_falls_back_to_human(self):
        data = {"type": "SomeWeirdMessage", "content": "idk"}
        restored = _deserialize_message(data)
        assert type(restored).__name__ == "HumanMessage"
        assert restored.content == "idk"

    def test_additional_kwargs_preserved(self):
        original = _MockAIMessage(
            content="test",
            additional_kwargs={"model_id": "abc"},
        )
        data = _serialize_message(original)
        assert "additional_kwargs" in data
        assert "model_id" in data["additional_kwargs"]

    def test_empty_content_handled(self):
        original = _MockHumanMessage(content="")
        data = _serialize_message(original)
        restored = _deserialize_message(data)
        assert restored.content == ""

    def test_serialize_non_json_kwargs_filtered(self):
        """Non-JSON-serializable additional_kwargs should be dropped."""
        original = _MockAIMessage(
            content="test",
            additional_kwargs={"good": "value", "bad": object()},
        )
        data = _serialize_message(original)
        if "additional_kwargs" in data:
            assert "good" in data["additional_kwargs"]
            assert "bad" not in data["additional_kwargs"]


# ---------------------------------------------------------------------------
# SessionStore operations
# ---------------------------------------------------------------------------

class TestSessionStore:

    def test_new_session_id_format(self, store):
        sid = store.new_session_id()
        assert len(sid) == 12
        assert sid.isalnum()

    def test_new_session_ids_are_unique(self, store):
        ids = {store.new_session_id() for _ in range(100)}
        assert len(ids) == 100

    def test_save_and_load_meta(self, store, sample_meta):
        store.save_meta(sample_meta)
        meta, messages = store.load_session("abc123")
        assert meta is not None
        assert meta.session_id == "abc123"
        assert meta.mode == "debug"
        assert meta.model == "sonnet-4"
        assert meta.turn_count == 5
        assert meta.token_total == 12345
        assert meta.preview == "fix the login bug"
        assert messages == []

    def test_save_and_load_messages(self, store, sample_meta):
        store.save_meta(sample_meta)

        msgs = [
            _MockHumanMessage(content="fix the login bug"),
            _MockAIMessage(
                content="Let me check",
                tool_calls=[{"id": "c1", "name": "read_file", "args": {"path": "auth.py"}}],
            ),
            _MockToolMessage(content="def login():\n  pass", tool_call_id="c1"),
            _MockAIMessage(content="Found the issue."),
        ]
        store.save_messages("abc123", msgs)

        meta, loaded = store.load_session("abc123")
        assert meta is not None
        assert len(loaded) == 4
        assert type(loaded[0]).__name__ == "HumanMessage"
        assert type(loaded[1]).__name__ == "AIMessage"
        assert len(loaded[1].tool_calls) == 1
        assert type(loaded[2]).__name__ == "ToolMessage"
        assert loaded[2].tool_call_id == "c1"
        assert type(loaded[3]).__name__ == "AIMessage"

    def test_append_message(self, store, sample_meta):
        store.save_meta(sample_meta)
        store.save_messages("abc123", [_MockHumanMessage(content="first")])
        store.append_message("abc123", _MockHumanMessage(content="second"))

        _, loaded = store.load_session("abc123")
        assert len(loaded) == 2
        assert loaded[0].content == "first"
        assert loaded[1].content == "second"

    def test_list_sessions_ordered_by_updated_at(self, store):
        for i, ts in enumerate([1700000000.0, 1700000020.0, 1700000010.0]):
            meta = SessionMeta(
                session_id=f"s{i}",
                created_at=ts,
                updated_at=ts,
                mode="debug",
                model="sonnet",
                cwd="/tmp",
                git_branch="main",
                preview=f"session {i}",
            )
            store.save_meta(meta)

        sessions = store.list_sessions()
        assert len(sessions) == 3
        assert sessions[0].session_id == "s1"  # 1700000020
        assert sessions[1].session_id == "s2"  # 1700000010
        assert sessions[2].session_id == "s0"  # 1700000000

    def test_list_sessions_limit(self, store):
        for i in range(5):
            meta = SessionMeta(
                session_id=f"s{i}",
                created_at=1700000000.0 + i,
                updated_at=1700000000.0 + i,
                mode="chat",
                model="haiku",
                cwd="/tmp",
                git_branch="dev",
                preview=f"sess {i}",
            )
            store.save_meta(meta)

        sessions = store.list_sessions(limit=2)
        assert len(sessions) == 2

    def test_get_latest(self, store):
        for i in range(3):
            meta = SessionMeta(
                session_id=f"s{i}",
                created_at=1700000000.0 + i,
                updated_at=1700000000.0 + i,
                mode="chat",
                model="haiku",
                cwd="/tmp",
                git_branch="dev",
                preview=f"sess {i}",
            )
            store.save_meta(meta)

        latest = store.get_latest()
        assert latest is not None
        assert latest.session_id == "s2"

    def test_get_latest_empty_returns_none(self, store):
        assert store.get_latest() is None

    def test_delete_session(self, store, sample_meta):
        store.save_meta(sample_meta)
        store.save_messages("abc123", [_MockHumanMessage(content="hi")])

        store.delete_session("abc123")

        meta, messages = store.load_session("abc123")
        assert meta is None
        assert messages == []

    def test_load_nonexistent_session(self, store):
        meta, messages = store.load_session("doesnotexist")
        assert meta is None
        assert messages == []

    def test_save_messages_overwrites(self, store, sample_meta):
        store.save_meta(sample_meta)
        store.save_messages("abc123", [_MockHumanMessage(content="v1")])
        store.save_messages("abc123", [_MockHumanMessage(content="v2")])

        _, loaded = store.load_session("abc123")
        assert len(loaded) == 1
        assert loaded[0].content == "v2"

    def test_corrupted_meta_file_returns_none(self, store, tmp_sessions):
        meta_path = tmp_sessions / "bad123.meta.json"
        meta_path.write_text("not valid json {{{", encoding="utf-8")

        meta, messages = store.load_session("bad123")
        assert meta is None

    def test_corrupted_messages_file_returns_partial(self, store, tmp_sessions, sample_meta):
        store.save_meta(sample_meta)
        msg_path = tmp_sessions / "abc123.messages.jsonl"
        msg_path.write_text(
            '{"type":"HumanMessage","content":"ok"}\nnot-json\n',
            encoding="utf-8",
        )
        meta, messages = store.load_session("abc123")
        assert meta is not None

    def test_meta_json_is_valid_json(self, store, sample_meta):
        store.save_meta(sample_meta)
        meta_path = store._dir / "abc123.meta.json"
        data = json.loads(meta_path.read_text())
        assert data["session_id"] == "abc123"
        assert data["mode"] == "debug"

    def test_messages_jsonl_format(self, store, sample_meta):
        store.save_meta(sample_meta)
        store.save_messages("abc123", [
            _MockHumanMessage(content="hello"),
            _MockAIMessage(content="hi back"),
        ])
        msg_path = store._dir / "abc123.messages.jsonl"
        lines = msg_path.read_text().strip().split("\n")
        assert len(lines) == 2
        for line in lines:
            parsed = json.loads(line)
            assert "type" in parsed
            assert "content" in parsed


class TestGetGitBranch:

    def test_returns_branch_name(self):
        from autodebug.agent.session_store import _get_git_branch
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = type("R", (), {"returncode": 0, "stdout": "feature/login\n"})()
            assert _get_git_branch() == "feature/login"

    def test_returns_empty_on_failure(self):
        from autodebug.agent.session_store import _get_git_branch
        with patch("subprocess.run", side_effect=FileNotFoundError):
            assert _get_git_branch() == ""
