"""realtime.py 实时 L0 维护测试"""

from __future__ import annotations

import json
import time
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from index1.cognitive.db_cog import CogDatabase
from index1.cognitive.realtime import RealtimeUpdater
from index1.cognitive.rules import (
    looks_project_specific,
    match_edge_rule,
    match_triple_rule,
    scope_overlap,
)
from index1.config import Config


# ── Fixtures ──


@pytest.fixture()
def cog_db(tmp_path: Path) -> CogDatabase:
    db = CogDatabase(tmp_path / "realtime_test.db", embedding_dim=4)
    db.connect()
    yield db
    db.close()


@pytest.fixture()
def config() -> Config:
    return Config(
        cognitive_decay_days=7,
        cognitive_gc_days=90,
        cognitive_gc_min_confidence=0.1,
    )


@pytest.fixture()
def updater(cog_db: CogDatabase, config: Config) -> RealtimeUpdater:
    return RealtimeUpdater(cog_db, config)


# ── 共享规则函数测试 ──


class TestLooksProjectSpecific:
    def test_src_path(self):
        assert looks_project_specific("Fixed bug in src/main.py") is True

    def test_tests_path(self):
        assert looks_project_specific("Updated tests/test_db.py") is True

    def test_dotslash_path(self):
        assert looks_project_specific("Changed ./config.toml") is True

    def test_module_reference(self):
        assert looks_project_specific("index1.cognitive.db_cog") is True

    def test_bare_filename(self):
        assert looks_project_specific("Fixed bug in db_cog.py line 42") is True

    def test_utils_path(self):
        assert looks_project_specific("Updated utils/helpers.py") is True

    def test_common_lib_not_specific(self):
        assert looks_project_specific(
            "Use logging.handlers.RotatingFileHandler"
        ) is False

    def test_generic_assertion(self):
        assert looks_project_specific(
            "Always validate user input before processing"
        ) is False

    def test_simple_pattern(self):
        assert looks_project_specific(
            "SQLite WAL mode improves concurrent reads"
        ) is False


class TestScopeOverlap:
    def test_overlap(self):
        a = {"scope_files": '["src/a.py", "src/b.py"]'}
        b = {"scope_files": '["src/b.py", "src/c.py"]'}
        assert scope_overlap(a, b) is True

    def test_no_overlap(self):
        a = {"scope_files": '["src/a.py"]'}
        b = {"scope_files": '["src/c.py"]'}
        assert scope_overlap(a, b) is False

    def test_empty_scope(self):
        a = {"scope_files": "[]"}
        b = {"scope_files": '["src/a.py"]'}
        assert scope_overlap(a, b) is False

    def test_missing_scope(self):
        a = {}
        b = {"scope_files": '["src/a.py"]'}
        assert scope_overlap(a, b) is False

    def test_invalid_json(self):
        a = {"scope_files": "not json"}
        b = {"scope_files": '["src/a.py"]'}
        assert scope_overlap(a, b) is False


class TestMatchEdgeRule:
    def test_root_cause_to_test_pass(self):
        prev = {"category": "root_cause", "assertion": "Missing import"}
        curr = {"assertion": "Tests passed after fix"}
        result = match_edge_rule(prev, curr)
        assert result is not None
        assert result[0] == "causes"
        assert result[1] == 0.8

    def test_modified_to_committed_with_overlap(self):
        prev = {"assertion": "Modified src/main.py", "scope_files": '["src/main.py"]'}
        curr = {"assertion": "Committed changes", "scope_files": '["src/main.py"]'}
        result = match_edge_rule(prev, curr)
        assert result is not None
        assert result[0] == "leads_to"

    def test_modified_to_committed_no_overlap(self):
        prev = {"assertion": "Modified src/a.py", "scope_files": '["src/a.py"]'}
        curr = {"assertion": "Committed changes", "scope_files": '["src/b.py"]'}
        result = match_edge_rule(prev, curr)
        assert result is None

    def test_install_to_test_pass(self):
        prev = {"assertion": "pip install pytest"}
        curr = {"assertion": "Tests passed"}
        result = match_edge_rule(prev, curr)
        assert result is not None
        assert result[0] == "supports"

    def test_no_match(self):
        prev = {"assertion": "Read file"}
        curr = {"assertion": "Search results"}
        result = match_edge_rule(prev, curr)
        assert result is None


class TestMatchTripleRule:
    def test_fail_modify_pass(self):
        prev2 = {"id": 1, "assertion": "Tests failed in module"}
        prev = {"id": 2, "assertion": "Modified fix file", "scope_files": '["src/a.py"]'}
        curr = {"id": 3, "assertion": "Tests passed now", "scope_files": '["src/a.py"]'}
        result = match_triple_rule(prev2, prev, curr)
        assert result is not None
        assert len(result) == 2
        assert result[0] == (1, 2, "causes", 0.7)
        assert result[1] == (2, 3, "causes", 0.7)

    def test_missing_ids(self):
        prev2 = {"assertion": "Tests failed"}
        prev = {"id": 2, "assertion": "Modified file"}
        curr = {"id": 3, "assertion": "Tests passed"}
        result = match_triple_rule(prev2, prev, curr)
        assert result is None

    def test_no_match_pattern(self):
        prev2 = {"id": 1, "assertion": "Read file"}
        prev = {"id": 2, "assertion": "Search results"}
        curr = {"id": 3, "assertion": "Another read"}
        result = match_triple_rule(prev2, prev, curr)
        assert result is None


# ── RealtimeUpdater 测试 ──


class TestRealtimeUpdater:
    def test_on_fact_saved_infers_edges(self, cog_db, updater):
        """新 fact 与最近 facts 的因果推断"""
        # 插入 session facts
        session_id = "test-session-edges"
        cog_db.insert_fact({
            "assertion": "Root cause: missing import",
            "category": "root_cause",
            "collection": "test",
            "session_id": session_id,
            "confidence": 0.8,
            "source": "test",
            "status": "active",
            "scope_files": "[]",
            "scope_modules": "[]",
            "scope_entities": "[]",
            "context_json": "{}",
        })

        new_fact = {
            "id": 999,  # 会被 insert_fact 覆盖，但 on_fact_saved 用
            "assertion": "Tests passed successfully",
            "category": "code",
            "collection": "test",
            "session_id": session_id,
            "confidence": 0.8,
            "source": "test",
            "status": "active",
            "scope_files": "[]",
            "scope_modules": "[]",
            "scope_entities": "[]",
            "context_json": "{}",
        }
        result = cog_db.insert_fact(new_fact)
        if result is not None:
            new_fact["id"] = result

        # 调用 on_fact_saved — 应该推断出 causes edge
        updater.on_fact_saved(new_fact, "test", session_id)
        # 验证不抛异常即可（边的存在取决于 get_session_facts 返回）

    def test_on_fact_saved_exception_safe(self, updater):
        """on_fact_saved 异常不传播"""
        # 传入无效数据，不应抛异常
        updater.on_fact_saved({}, "test", None)
        updater.on_fact_saved({"id": 1}, "test", "session-1")

    def test_periodic_runs_first_time(self, cog_db, config, tmp_path):
        """首次调用（无 marker 文件）应执行 L0 步骤"""
        updater = RealtimeUpdater(cog_db, config)
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            updater.periodic_l0("test", "session-1")
            # marker 文件应被创建
            marker = tmp_path / ".periodic_last_run_test"
            assert marker.exists()

    def test_periodic_skips_within_interval(self, cog_db, config, tmp_path):
        """5 分钟内第二次调用应跳过"""
        updater = RealtimeUpdater(cog_db, config)
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            updater.periodic_l0("test", "session-1")
            # 记录 marker 时间戳
            marker = tmp_path / ".periodic_last_run_test"
            first_ts = marker.read_text().strip()
            # 再次调用 — 应该跳过（marker 没变）
            updater.periodic_l0("test", "session-1")
            assert marker.read_text().strip() == first_ts

    def test_periodic_runs_after_interval(self, cog_db, config, tmp_path):
        """过了 5 分钟间隔后应重新执行"""
        updater = RealtimeUpdater(cog_db, config)
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            # 写一个旧 marker
            marker = tmp_path / ".periodic_last_run_test"
            marker.write_text(str(time.time() - 600))  # 10 分钟前
            updater.periodic_l0("test", "session-1")
            # marker 应被更新为近期
            new_ts = float(marker.read_text().strip())
            assert time.time() - new_ts < 5

    def test_periodic_exception_safe(self, config, tmp_path):
        """periodic_l0 异常不传播"""
        mock_db = MagicMock()
        mock_db.decay_facts.side_effect = RuntimeError("DB error")
        mock_db.garbage_collect.side_effect = RuntimeError("GC error")
        updater = RealtimeUpdater(mock_db, config)
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            # 不应抛异常
            updater.periodic_l0("test", "session-1")


class TestMarkerManagement:
    def test_write_and_check(self, updater, tmp_path):
        """写入 marker 后可检查到"""
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            updater.write_marker("test-coll")
            assert RealtimeUpdater.check_marker("test-coll") is True

    def test_clear_marker(self, updater, tmp_path):
        """清除 marker 后检查不到"""
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            updater.write_marker("test-coll")
            RealtimeUpdater.clear_marker("test-coll")
            assert RealtimeUpdater.check_marker("test-coll") is False

    def test_no_marker(self, tmp_path):
        """无 marker 时返回 False"""
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            assert RealtimeUpdater.check_marker("nonexistent") is False

    def test_expired_marker(self, updater, tmp_path):
        """过期 marker（>15 分钟）自动清除"""
        with patch("index1.cognitive.realtime.INDEX1_HOME", tmp_path):
            updater.write_marker("test-coll")
            # 修改 marker 内容，设置旧时间戳
            marker = tmp_path / ".realtime_active_test-coll"
            data = json.loads(marker.read_text())
            data["timestamp"] = time.time() - 1200  # 20 分钟前
            marker.write_text(json.dumps(data))

            assert RealtimeUpdater.check_marker("test-coll") is False


class TestCheckPromote:
    def test_promote_candidate(self, cog_db, updater):
        """满足条件的 fact 被提升为 universal"""
        # 插入一个高 confidence + 高 access 的通用 fact
        cog_db.insert_fact({
            "assertion": "Always validate user input before database operations",
            "category": "decision",
            "collection": "test",
            "session_id": "s1",
            "confidence": 0.9,
            "source": "test",
            "status": "active",
            "scope_files": "[]",
            "scope_modules": "[]",
            "scope_entities": "[]",
            "context_json": "{}",
        })
        # 手动增加 access_count（通常由 touch_fact 累积）
        conn = cog_db.conn
        conn.execute("UPDATE facts SET access_count = 5 WHERE collection = 'test'")
        conn.commit()

        # 调用 _check_promote
        updater._check_promote("test")
        # 验证不抛异常即可

    def test_skip_project_specific(self, cog_db, updater):
        """包含项目路径的不提升"""
        cog_db.insert_fact({
            "assertion": "Fixed bug in src/index1/cognitive/db_cog.py line 42",
            "category": "root_cause",
            "collection": "test",
            "session_id": "s1",
            "confidence": 0.95,
            "source": "test",
            "status": "active",
            "scope_files": "[]",
            "scope_modules": "[]",
            "scope_entities": "[]",
            "context_json": "{}",
        })
        conn = cog_db.conn
        conn.execute("UPDATE facts SET access_count = 10 WHERE collection = 'test'")
        conn.commit()

        # _check_promote 不应提升（包含项目路径）
        updater._check_promote("test")


class TestExtractTacticsL0:
    def test_extract_from_repeated_root_cause(self, cog_db, updater):
        """重复 root_cause 模式应提取 tactic"""
        scope = '["src/auth/login.py"]'
        for i in range(3):
            cog_db.insert_fact({
                "assertion": f"Root cause #{i}: auth token expired",
                "category": "root_cause",
                "collection": "test",
                "session_id": f"s{i}",
                "confidence": 0.8,
                "source": "test",
                "status": "active",
                "scope_files": scope,
                "scope_modules": "[]",
                "scope_entities": "[]",
                "context_json": "{}",
            })

        count = updater._extract_tactics_l0("test")
        assert count >= 1

    def test_no_extraction_below_threshold(self, cog_db, updater):
        """低于阈值不提取"""
        cog_db.insert_fact({
            "assertion": "Single root cause",
            "category": "root_cause",
            "collection": "test",
            "session_id": "s1",
            "confidence": 0.8,
            "source": "test",
            "status": "active",
            "scope_files": '["src/single.py"]',
            "scope_modules": "[]",
            "scope_entities": "[]",
            "context_json": "{}",
        })

        count = updater._extract_tactics_l0("test")
        assert count == 0


# ── P3-6: Eager Embedding 测试 ──


class TestEagerEmbed:
    """P3-6: 高 importance fact 同步 ONNX embedding"""

    def test_eager_embed_triggered(self, cog_db, config):
        """importance >= threshold + ONNX → embed 调用"""
        config.eager_embed_enabled = True
        config.eager_embed_min_importance = 0.7
        config.embed_backend = "onnx"
        updater = RealtimeUpdater(cog_db, config)

        fact_id = cog_db.insert_fact({
            "assertion": "Fixed critical bug in parser",
            "collection": "test",
            "category": "root_cause",
            "confidence": 0.8,
            "source": "test",
            "status": "active",
            "scope_files": "[]",
            "scope_modules": "[]",
            "scope_entities": "[]",
            "context_json": '{"changes": "refactored parser logic"}',
        })
        fact = {"id": fact_id, "assertion": "Fixed critical bug in parser",
                "context_json": '{"changes": "refactored parser logic"}'}

        with patch.object(updater, "_eager_embed_fact") as mock_embed:
            updater.on_fact_saved(fact, "test", "sess1", importance=0.9)
            mock_embed.assert_called_once_with(
                fact_id, "Fixed critical bug in parser",
                '{"changes": "refactored parser logic"}',
            )

    def test_importance_below_threshold_skips(self, cog_db, config):
        """importance < threshold → 不触发 embed"""
        config.eager_embed_enabled = True
        config.eager_embed_min_importance = 0.7
        config.embed_backend = "onnx"
        updater = RealtimeUpdater(cog_db, config)

        fact = {"id": 1, "assertion": "Minor edit", "context_json": "{}"}
        with patch.object(updater, "_eager_embed_fact") as mock_embed:
            updater.on_fact_saved(fact, "test", "sess1", importance=0.3)
            mock_embed.assert_not_called()

    def test_non_onnx_backend_skips(self, cog_db, config):
        """embed_backend != onnx → 不触发"""
        config.eager_embed_enabled = True
        config.eager_embed_min_importance = 0.7
        config.embed_backend = "ollama"
        updater = RealtimeUpdater(cog_db, config)

        fact = {"id": 1, "assertion": "Error fix", "context_json": "{}"}
        with patch.object(updater, "_eager_embed_fact") as mock_embed:
            updater.on_fact_saved(fact, "test", "sess1", importance=0.9)
            mock_embed.assert_not_called()

    def test_disabled_skips(self, cog_db, config):
        """eager_embed_enabled=False → 不触发"""
        config.eager_embed_enabled = False
        config.embed_backend = "onnx"
        updater = RealtimeUpdater(cog_db, config)

        fact = {"id": 1, "assertion": "Error fix", "context_json": "{}"}
        with patch.object(updater, "_eager_embed_fact") as mock_embed:
            updater.on_fact_saved(fact, "test", "sess1", importance=0.9)
            mock_embed.assert_not_called()

    def test_no_fact_id_skips(self, cog_db, config):
        """fact 没有 id → 不触发 embed"""
        config.eager_embed_enabled = True
        config.eager_embed_min_importance = 0.7
        config.embed_backend = "onnx"
        updater = RealtimeUpdater(cog_db, config)

        fact = {"assertion": "Error fix", "context_json": "{}"}  # no id
        with patch.object(updater, "_eager_embed_fact") as mock_embed:
            updater.on_fact_saved(fact, "test", "sess1", importance=0.9)
            mock_embed.assert_not_called()

    def test_embed_failure_silent(self, cog_db, config):
        """embed 失败 → 静默降级，不崩溃"""
        config.eager_embed_enabled = True
        config.eager_embed_min_importance = 0.7
        config.embed_backend = "onnx"
        updater = RealtimeUpdater(cog_db, config)

        fact = {"id": 1, "assertion": "Error fix", "context_json": "{}"}
        with patch.object(updater, "_eager_embed_fact", side_effect=RuntimeError("embed fail")):
            # 不应抛异常
            updater.on_fact_saved(fact, "test", "sess1", importance=0.9)

    def test_eager_embed_fact_constructs_text(self, cog_db, config):
        """_eager_embed_fact 正确构造 embedding 文本"""
        config.eager_embed_enabled = True
        config.embed_backend = "onnx"
        updater = RealtimeUpdater(cog_db, config)

        mock_embedder = MagicMock()
        mock_embedder.embed_one = MagicMock()

        import asyncio

        async def fake_embed(text):
            return [0.1, 0.2, 0.3, 0.4]

        mock_embedder.embed_one = fake_embed
        updater._embedder = mock_embedder

        with patch.object(cog_db, "update_fact_embedding") as mock_update:
            updater._eager_embed_fact(
                42, "Fixed parser bug",
                '{"changes": "refactored logic", "reasoning": "simpler flow"}',
            )
            mock_update.assert_called_once()
            call_args = mock_update.call_args
            assert call_args[0][0] == 42  # fact_id
            assert call_args[0][1] == [0.1, 0.2, 0.3, 0.4]  # embedding

    def test_eager_embed_fact_empty_assertion_skips(self, cog_db, config):
        """assertion 为空 → 直接返回"""
        config.eager_embed_enabled = True
        config.embed_backend = "onnx"
        updater = RealtimeUpdater(cog_db, config)

        with patch.object(cog_db, "update_fact_embedding") as mock_update:
            updater._eager_embed_fact(42, "", "{}")
            mock_update.assert_not_called()
