"""SessionContext 模块测试 — Phase 3a"""

from __future__ import annotations

import json
from unittest.mock import MagicMock

import pytest

from index1.session_context import (
    SessionContext,
    _extract_entities_from_edit,
)


# ── 辅助函数 ──


def _make_event(
    tool_name: str,
    tool_input: dict,
    collection: str = "test",
) -> dict:
    """构造 raw_event 记录"""
    return {
        "event_id": 1,
        "tool_name": tool_name,
        "collection": collection,
        "payload": json.dumps({"tool_input": tool_input}),
        "timestamp": "2026-02-12T10:00:00",
    }


def _make_ctx(
    files: list[str] | None = None,
    entities: list[str] | None = None,
    collection: str | None = None,
) -> SessionContext:
    """快速构造 SessionContext"""
    return SessionContext(
        recent_files=files or [],
        recent_entities=entities or [],
        recent_queries=[],
        session_collection=collection,
    )


# ── SessionContext 核心测试 ──


class TestSessionContext:
    def test_cold_start_not_warm(self):
        ctx = _make_ctx()
        assert ctx.is_warm is False

    def test_cold_start_skip_coreference(self):
        ctx = _make_ctx()
        assert ctx.resolve_coreferences("这个文件有问题") == "这个文件有问题"

    def test_cold_start_no_expansion(self):
        ctx = _make_ctx()
        assert ctx.get_expansion_terms() == []

    def test_warm_with_one_file(self):
        ctx = _make_ctx(files=["/src/hooks.py"])
        assert ctx.is_warm is True

    def test_warm_with_two_entities(self):
        ctx = _make_ctx(entities=["search", "rerank"])
        assert ctx.is_warm is True

    def test_one_entity_not_warm(self):
        ctx = _make_ctx(entities=["search"])
        assert ctx.is_warm is False


# ── 共指消解测试 ──


class TestCoreference:
    def test_chinese_file(self):
        ctx = _make_ctx(files=["/src/index1/hooks.py"])
        result = ctx.resolve_coreferences("这个文件有什么问题")
        assert "hooks.py" in result
        assert "这个文件" not in result

    def test_chinese_that_file(self):
        ctx = _make_ctx(files=["/src/index1/db.py"])
        result = ctx.resolve_coreferences("那个文件怎么改")
        assert "db.py" in result

    def test_chinese_function(self):
        ctx = _make_ctx(files=["/f.py"], entities=["search"])
        result = ctx.resolve_coreferences("这个函数做了什么")
        assert "search" in result
        assert "这个函数" not in result

    def test_chinese_class(self):
        ctx = _make_ctx(files=["/f.py"], entities=["Database"])
        result = ctx.resolve_coreferences("这个类的结构")
        assert "Database" in result

    def test_english_this_file(self):
        ctx = _make_ctx(files=["/src/search.py"])
        result = ctx.resolve_coreferences("what does this file do")
        assert "search.py" in result
        assert "this file" not in result

    def test_english_that_function(self):
        ctx = _make_ctx(files=["/f.py"], entities=["_rerank"])
        result = ctx.resolve_coreferences("explain that function")
        assert "_rerank" in result

    def test_english_this_class(self):
        ctx = _make_ctx(files=["/f.py"], entities=["Config"])
        result = ctx.resolve_coreferences("this class has a bug")
        assert "Config" in result

    def test_english_this_bug(self):
        ctx = _make_ctx(files=["/src/db.py"])
        result = ctx.resolve_coreferences("fix this bug")
        assert "db.py" in result

    def test_no_english_it(self):
        """Phase 3a 不匹配英文 'it'"""
        ctx = _make_ctx(files=["/src/hooks.py"], entities=["search"])
        result = ctx.resolve_coreferences("it has a problem")
        assert result == "it has a problem"

    def test_no_match_passthrough(self):
        ctx = _make_ctx(files=["/src/hooks.py"])
        result = ctx.resolve_coreferences("liquidation API")
        assert result == "liquidation API"

    def test_cold_start_passthrough(self):
        ctx = _make_ctx()
        result = ctx.resolve_coreferences("这个文件有问题")
        assert result == "这个文件有问题"

    def test_only_first_match(self):
        """只替换第一个匹配"""
        ctx = _make_ctx(files=["/src/hooks.py"], entities=["search"])
        result = ctx.resolve_coreferences("这个文件和这个函数")
        # 只替换了第一个 "这个文件"
        assert "hooks.py" in result
        assert "这个函数" in result  # 第二个不替换

    def test_no_replacement_available(self):
        """有实体但无文件时，文件指代不替换"""
        ctx = _make_ctx(entities=["search", "rerank"])
        result = ctx.resolve_coreferences("这个文件有问题")
        # is_warm=True 但 recent_files 为空，无法替换
        assert result == "这个文件有问题"

    def test_case_insensitive_english(self):
        ctx = _make_ctx(files=["/src/db.py"])
        result = ctx.resolve_coreferences("This File is important")
        assert "db.py" in result


# ── 扩展词测试 ──


class TestExpansionTerms:
    def test_file_stems(self):
        ctx = _make_ctx(files=["/src/hooks.py", "/src/db.py", "/src/search.py"])
        terms = ctx.get_expansion_terms()
        assert "hooks" in terms
        assert "db" in terms
        assert "search" in terms

    def test_entities(self):
        ctx = _make_ctx(files=["/f.py"], entities=["_rerank", "Config", "search"])
        terms = ctx.get_expansion_terms()
        assert "_rerank" in terms
        assert "Config" in terms

    def test_cold_start_empty(self):
        ctx = _make_ctx()
        assert ctx.get_expansion_terms() == []

    def test_limit_files(self):
        """最多取 3 个文件的 stem"""
        ctx = _make_ctx(
            files=[f"/src/f{i}.py" for i in range(10)],
        )
        terms = ctx.get_expansion_terms()
        # 3 个 file stems
        file_stems = [f"f{i}" for i in range(3)]
        for s in file_stems:
            assert s in terms

    def test_limit_entities(self):
        """最多取 5 个实体"""
        ctx = _make_ctx(
            files=["/f.py"],
            entities=[f"func_{i}" for i in range(20)],
        )
        terms = ctx.get_expansion_terms()
        # 1 file stem + 5 entities = 6
        assert len(terms) == 6


# ── from_db 重建测试 ──


class TestFromDb:
    def test_none_db(self):
        ctx = SessionContext.from_db(None, "session-1")
        assert ctx.is_warm is False

    def test_none_session(self):
        mock_db = MagicMock()
        ctx = SessionContext.from_db(mock_db, None)
        assert ctx.is_warm is False
        mock_db.get_recent_raw_events.assert_not_called()

    def test_parse_write_events(self):
        events = [
            _make_event("Write", {"file_path": "/src/hooks.py", "content": "def hello():\n    pass"}),
        ]
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = events

        ctx = SessionContext.from_db(mock_db, "session-1")
        assert "/src/hooks.py" in ctx.recent_files
        assert "hello" in ctx.recent_entities

    def test_parse_edit_events(self):
        events = [
            _make_event("Edit", {
                "file_path": "/src/search.py",
                "new_string": "class SearchEngine:\n    pass",
            }),
        ]
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = events

        ctx = SessionContext.from_db(mock_db, "session-1")
        assert "/src/search.py" in ctx.recent_files
        assert "SearchEngine" in ctx.recent_entities

    def test_parse_read_events(self):
        events = [
            _make_event("Read", {"file_path": "/src/config.py"}),
        ]
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = events

        ctx = SessionContext.from_db(mock_db, "session-1")
        assert "/src/config.py" in ctx.recent_files
        # Read 不提取实体
        assert ctx.recent_entities == []

    def test_collection_filter(self):
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = []

        SessionContext.from_db(mock_db, "session-1", collection="project-a")
        mock_db.get_recent_raw_events.assert_called_once_with(
            "session-1", collection="project-a", limit=20,
        )

    def test_file_dedup(self):
        events = [
            _make_event("Edit", {"file_path": "/src/hooks.py", "new_string": "x=1"}),
            _make_event("Edit", {"file_path": "/src/hooks.py", "new_string": "x=2"}),
            _make_event("Read", {"file_path": "/src/hooks.py"}),
        ]
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = events

        ctx = SessionContext.from_db(mock_db, "session-1")
        # 同一路径只出现一次
        assert ctx.recent_files.count("/src/hooks.py") == 1

    def test_ring_buffer_limit(self):
        """超过 20 个文件时截断"""
        events = [
            _make_event("Write", {"file_path": f"/src/f{i}.py", "content": ""})
            for i in range(30)
        ]
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = events

        ctx = SessionContext.from_db(mock_db, "session-1")
        assert len(ctx.recent_files) == 20

    def test_entity_extraction(self):
        events = [
            _make_event("Edit", {
                "file_path": "/f.py",
                "new_string": "def search():\n    pass\n\nclass Config:\n    pass\n\ndef _rerank():\n    pass",
            }),
        ]
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = events

        ctx = SessionContext.from_db(mock_db, "session-1")
        assert "search" in ctx.recent_entities
        assert "Config" in ctx.recent_entities
        assert "_rerank" in ctx.recent_entities

    def test_db_exception_graceful(self):
        """DB 查询异常 → 返回冷启动"""
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.side_effect = RuntimeError("DB locked")

        ctx = SessionContext.from_db(mock_db, "session-1")
        assert ctx.is_warm is False

    def test_malformed_payload(self):
        """payload 格式错误 → 跳过该事件"""
        events = [
            {"event_id": 1, "tool_name": "Edit", "collection": "test",
             "payload": "not-json{{{", "timestamp": "2026-02-12T10:00:00"},
            _make_event("Read", {"file_path": "/src/good.py"}),
        ]
        mock_db = MagicMock()
        mock_db.get_recent_raw_events.return_value = events

        ctx = SessionContext.from_db(mock_db, "session-1")
        assert "/src/good.py" in ctx.recent_files


# ── 实体提取辅助函数测试 ──


class TestExtractEntities:
    def test_def(self):
        assert _extract_entities_from_edit({"new_string": "def hello():"}) == ["hello"]

    def test_class(self):
        assert _extract_entities_from_edit({"new_string": "class MyClass:"}) == ["MyClass"]

    def test_multiple(self):
        text = "def a():\n    pass\nclass B:\n    pass\ndef _c():\n    pass"
        result = _extract_entities_from_edit({"new_string": text})
        assert result == ["a", "B", "_c"]

    def test_empty(self):
        assert _extract_entities_from_edit({}) == []

    def test_content_fallback(self):
        """Write tool 使用 content 字段"""
        assert _extract_entities_from_edit({"content": "def write_func():"}) == ["write_func"]

    def test_fn_keyword(self):
        """Rust fn 关键字"""
        assert _extract_entities_from_edit({"new_string": "fn main() {"}) == ["main"]

    def test_function_keyword(self):
        """JavaScript function 关键字"""
        assert _extract_entities_from_edit({"new_string": "function render() {"}) == ["render"]
