"""认知模块集成测试 — 完整生命周期

测试流程：seed facts → awaken → orient → observe → MCP tools → maintenance → 下次 awaken
覆盖 Issue #58
"""

from __future__ import annotations

import asyncio
import json
from pathlib import Path

import pytest

from index1.cognitive.capture import extract
from index1.cognitive.db_cog import CogDatabase
from index1.cognitive.hooks import HookHandler
from index1.cognitive.maintenance import MaintenanceEngine
from index1.cognitive.search_cog import recall
from index1.config import Config


@pytest.fixture()
def cog_db(tmp_path: Path) -> CogDatabase:
    db = CogDatabase(tmp_path / "integration_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,
        observer_enabled=False,  # 测试用 L0 规则，不启动 LLM observer
    )


@pytest.fixture()
def handler(cog_db, config) -> HookHandler:
    return HookHandler(db_cog=cog_db, config=config)


class TestFullLifecycle:
    """完整生命周期：awaken → orient → observe → recall → maintenance → awaken"""

    def test_first_session_cold_start(self, handler, cog_db):
        """第一次会话 — 冷启动，无历史数据"""
        # Step 1: awaken（冷启动）
        awaken_result = handler.awaken({
            "session_id": "session-1",
            "cwd": "/project/my-app",
        })
        assert "output" in awaken_result
        assert "my-app" in awaken_result["output"]

    def test_observe_captures_facts(self, handler, cog_db):
        """observe 应从工具输出中捕获 facts"""
        # 模拟 Write 工具
        handler.observe({
            "tool_name": "Write",
            "tool_input": {"file_path": "src/auth/login.py"},
            "tool_response": "File written successfully",
            "session_id": "session-1",
            "cwd": "/project/my-app",
        })

        # 模拟 Bash 工具（测试通过）
        handler.observe({
            "tool_name": "Bash",
            "tool_input": {"command": "pytest tests/"},
            "tool_response": "5 passed in 1.23s",
            "session_id": "session-1",
            "cwd": "/project/my-app",
        })

        # 验证 facts 已捕获
        facts = cog_db.get_session_facts("session-1", "my-app")
        assert len(facts) >= 1

    def test_orient_returns_relevant_facts(self, handler, cog_db):
        """orient 应返回与用户 prompt 相关的 facts"""
        # 先插入一些 facts
        cog_db.insert_fact({
            "collection": "my-app",
            "assertion": "Authentication uses JWT tokens for session management",
            "confidence": 0.9,
            "session_id": "session-1",
        })

        # orient 搜索
        result = handler.orient({
            "prompt": "How does authentication work in this project?",
            "cwd": "/project/my-app",
        })
        # 可能有结果也可能没有（取决于 FTS5 分词），但不应崩溃
        assert isinstance(result, dict)

    def test_mcp_tools_save_and_recall(self, cog_db):
        """MCP 工具：save → recall 往返"""
        # 存储 fact
        fid = cog_db.insert_fact({
            "collection": "my-app",
            "assertion": "The database uses PostgreSQL with pgvector extension",
            "confidence": 0.95,
            "source": "claude",
        })
        assert fid >= 1

        # 存储 mental model
        mid = cog_db.upsert_mental_model(
            collection="my-app",
            scope="src/db/",
            subject="Database layer using PostgreSQL",
        )
        assert mid >= 1

        # recall 搜索
        result = asyncio.run(recall(
            "database PostgreSQL",
            cog_db,
            collection="my-app",
        ))
        assert result.total >= 1
        assert result.mode == "bm25_only"

        # 获取 mental model
        model = cog_db.get_mental_model("src/db/", "my-app")
        assert model is not None
        assert "PostgreSQL" in model["subject"]

    def test_maintenance_then_awaken(self, cog_db, config):
        """maintenance → session_review → 下次 awaken 能看到 [prev_session]（#260）"""
        # 插入 session facts
        for i in range(3):
            cog_db.insert_fact({
                "collection": "my-app",
                "assertion": f"Important discovery {i} about the codebase",
                "confidence": 0.8 + i * 0.05,
                "session_id": "session-1",
                "source": "hook",
            })

        # 运行维护（无 chat 模型，模板降级）
        # #260: generate_brief 步骤已废弃，不再断言
        engine = MaintenanceEngine(cog_db, config, None)
        engine._chat_available = False
        results = asyncio.run(engine.run("my-app", "session-1"))
        assert "session_review" in results

        # 插入 session_record（模拟 session_review 的输出）
        cog_db.insert_fact({
            "collection": "my-app",
            "assertion": "Session record for session-1",
            "category": "session_record",
            "confidence": 1.0,
            "session_id": "session-1",
            "context_json": json.dumps({
                "session_record": {
                    "files_changed": ["src/main.py"],
                    "key_changes": [],
                    "patterns": ["discovery"],
                    "outcome": "completed",
                },
            }),
        })

        # 下次 awaken 能看到 [prev_session] 结构化标签
        handler = HookHandler(db_cog=cog_db, config=config)
        result = handler.awaken({
            "session_id": "session-2",
            "cwd": "/project/my-app",
        })
        assert "output" in result
        assert "[prev_session]" in result["output"]

    def test_decay_and_gc_lifecycle(self, cog_db, config):
        """衰减 + GC 完整生命周期"""
        # 插入 3 类 facts
        # 1. 低置信度，从未访问
        low_id = cog_db.insert_fact({
            "collection": "my-app",
            "assertion": "Low confidence fact for GC",
            "confidence": 0.05,
        })
        # 2. 高置信度，正常
        high_id = cog_db.insert_fact({
            "collection": "my-app",
            "assertion": "High confidence fact survives",
            "confidence": 0.95,
        })
        # 3. 旧 fact，需衰减
        old_id = cog_db.insert_fact({
            "collection": "my-app",
            "assertion": "Old fact that decays",
            "confidence": 0.8,
        })
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-15 days') WHERE id=?",
            [old_id],
        )
        cog_db.conn.commit()

        # 运行维护
        engine = MaintenanceEngine(cog_db, config, None)
        engine._chat_available = False
        results = asyncio.run(engine.run("my-app"))

        # 验证
        low_fact = cog_db.get_fact(low_id)
        assert low_fact["status"] == "invalidated"  # GC 回收

        high_fact = cog_db.get_fact(high_id)
        assert high_fact["status"] == "active"  # 存活
        assert high_fact["confidence"] == 0.95  # 未衰减（新建的）

        old_fact = cog_db.get_fact(old_id)
        assert old_fact["confidence"] < 0.8  # 已衰减


class TestTacticLifecycle:
    """策略记录完整生命周期"""

    def test_save_use_report(self, cog_db):
        """保存策略 → 查找 → 使用 → 报告结果"""
        # 保存策略
        tid = cog_db.insert_tactic(
            trigger_pattern="import error in pytest",
            steps="1. Check __init__.py\n2. Check PYTHONPATH",
            collection="my-app",
        )

        # 查找策略
        tactics = cog_db.search_tactics("import error", collection="my-app")
        assert len(tactics) >= 1
        assert tactics[0]["id"] == tid

        # 使用并报告成功
        cog_db.update_tactic_outcome(tid, success=True)
        tactic = cog_db.conn.execute(
            "SELECT * FROM tactics WHERE id=?", [tid]
        ).fetchone()
        assert dict(tactic)["times_used"] == 1
        assert dict(tactic)["times_succeeded"] == 1
        assert dict(tactic)["success_rate"] == 1.0

        # 第二次使用，失败
        cog_db.update_tactic_outcome(tid, success=False)
        tactic = cog_db.conn.execute(
            "SELECT * FROM tactics WHERE id=?", [tid]
        ).fetchone()
        assert dict(tactic)["times_used"] == 2
        assert dict(tactic)["success_rate"] == 0.5


class TestCaptureToRecall:
    """捕获 → 存储 → 搜索 端到端"""

    def test_capture_write_recall(self, cog_db):
        """Write 工具 → capture → insert → recall"""
        # capture 提取
        facts = extract(
            tool_name="Write",
            tool_input={"file_path": "src/config.py"},
            tool_output="File written successfully",
            collection="my-app",
            session_id="s1",
        )
        assert len(facts) >= 1

        # 插入 DB
        for f in facts:
            cog_db.insert_fact(f)

        # recall 搜索
        result = asyncio.run(recall(
            "config", cog_db, collection="my-app",
        ))
        assert result.total >= 1

    def test_capture_test_results(self, cog_db):
        """Bash 测试结果 → capture → insert → recall"""
        facts = extract(
            tool_name="Bash",
            tool_input={"command": "pytest tests/ -x"},
            tool_output="15 passed, 2 failed in 3.45s",
            collection="my-app",
            session_id="s1",
        )

        for f in facts:
            cog_db.insert_fact(f)

        result = asyncio.run(recall(
            "test", cog_db, collection="my-app",
        ))
        # 可能匹配也可能不匹配（取决于 FTS5 分词）
        assert isinstance(result.total, int)


class TestCrossSessions:
    """跨会话记忆持久化"""

    def test_facts_persist_across_sessions(self, cog_db, config):
        """Session 1 的 facts 在 Session 2 可见"""
        # Session 1: 存储 facts
        cog_db.insert_fact({
            "collection": "my-app",
            "assertion": "Auth module uses bcrypt for password hashing",
            "confidence": 0.9,
            "session_id": "session-1",
        })

        # 模拟维护
        engine = MaintenanceEngine(cog_db, config, None)
        engine._chat_available = False
        asyncio.run(engine.run("my-app", "session-1"))

        # Session 2: awaken 能看到
        handler = HookHandler(db_cog=cog_db, config=config)
        result = handler.awaken({
            "session_id": "session-2",
            "cwd": "/project/my-app",
        })
        assert "output" in result

        # Session 2: recall 能搜到
        recall_result = asyncio.run(recall(
            "bcrypt password", cog_db, collection="my-app",
        ))
        assert recall_result.total >= 1

    def test_session_counter_increments(self, cog_db):
        """会话计数器应递增"""
        s1 = cog_db.get_or_create_session("ext-1", "my-app")
        assert s1["session_count"] == 1

        s2 = cog_db.get_or_create_session("ext-1", "my-app")
        assert s2["session_count"] == 2


class TestStatsAccuracy:
    """统计数据准确性"""

    def test_stats_reflect_operations(self, cog_db):
        """统计应准确反映操作结果"""
        # 初始状态
        stats = cog_db.get_stats("my-app")
        assert stats["facts_total"] == 0

        # 插入 facts
        for i in range(3):
            cog_db.insert_fact({
                "collection": "my-app",
                "assertion": f"Stat test fact {i}",
                "confidence": 0.9,
                "source": "claude" if i < 2 else "hook",
            })

        stats = cog_db.get_stats("my-app")
        assert stats["facts_total"] == 3
        assert stats["facts_active"] == 3
        assert stats["facts_by_source"]["claude"] == 2
        assert stats["facts_by_source"]["hook"] == 1

        # invalidate 一条
        cog_db.invalidate_fact(1, "test")
        stats = cog_db.get_stats("my-app")
        assert stats["facts_active"] == 2


# ========== Issue #82 — Schema-Population Gap 测试 ==========


class TestPatterns:
    """行为模式匹配器 patterns.py 集成测试"""

    def test_investigate_decide_pattern(self, cog_db):
        """模式 1: Read×N → Edit (调查-决策)"""
        from index1.cognitive.patterns import detect_patterns

        events = [
            {"type": "file_read", "files": ["src/a.py"]},
            {"type": "file_read", "files": ["src/b.py"]},
            {"type": "file_read", "files": ["src/c.py"]},
            {"type": "file_modified", "files": ["src/b.py"]},
        ]
        facts = detect_patterns(events, "my-app", "s1")
        assert len(facts) >= 1
        assert any("Investigated" in f["assertion"] for f in facts)
        assert facts[0]["source"] == "pattern"
        ctx = json.loads(facts[0]["context_json"])
        assert ctx.get("source_pattern") is True

    def test_error_fix_pattern(self, cog_db):
        """模式 2: test_failed → Edit → test_passed (错误-修复)"""
        from index1.cognitive.patterns import detect_patterns

        events = [
            {"type": "test_failed", "detail": "assert x == 1"},
            {"type": "file_read", "files": ["src/fix.py"]},
            {"type": "file_modified", "files": ["src/fix.py"]},
            {"type": "test_passed", "detail": "5 passed"},
        ]
        facts = detect_patterns(events, "my-app", "s1")
        assert len(facts) >= 1
        assert any("Fixed" in f["assertion"] for f in facts)
        assert facts[0]["category"] == "root_cause"

    def test_dependency_intro_pattern(self, cog_db):
        """模式 3: pip install → Edit (依赖引入)"""
        from index1.cognitive.patterns import detect_patterns

        events = [
            {"type": "bash", "detail": "pip install requests"},
            {"type": "file_modified", "files": ["src/api.py"]},
        ]
        facts = detect_patterns(events, "my-app", "s1")
        assert len(facts) >= 1
        assert any("requests" in f["assertion"] for f in facts)

    def test_iterative_refine_pattern(self, cog_db):
        """模式 5: Edit same file ×3+ (迭代修改)"""
        from index1.cognitive.patterns import detect_patterns

        events = [
            {"type": "file_modified", "files": ["src/main.py"]},
            {"type": "file_modified", "files": ["src/main.py"]},
            {"type": "file_modified", "files": ["src/main.py"]},
        ]
        facts = detect_patterns(events, "my-app", "s1")
        assert len(facts) >= 1
        assert any("Iteratively" in f["assertion"] for f in facts)

    def test_empty_events_no_crash(self, cog_db):
        """空事件列表不应崩溃"""
        from index1.cognitive.patterns import detect_patterns

        assert detect_patterns([], "my-app", "s1") == []
        assert detect_patterns([{"type": "file_read"}], "my-app", "s1") == []

    def test_pattern_deduplication(self, cog_db):
        """重复模式不应产生重复 facts"""
        from index1.cognitive.patterns import detect_patterns

        events = [
            {"type": "file_modified", "files": ["src/main.py"]},
            {"type": "file_modified", "files": ["src/main.py"]},
            {"type": "file_modified", "files": ["src/main.py"]},
            {"type": "file_modified", "files": ["src/main.py"]},
        ]
        facts = detect_patterns(events, "my-app", "s1")
        # 同一文件 iterative refine 只应产生 1 个 fact
        iterative = [f for f in facts if "Iteratively" in f["assertion"]]
        assert len(iterative) == 1


class TestAwakenNoTip:
    """Issue #82 T3: awaken 不再包含 Tip"""

    def test_awaken_output_no_tip(self, handler, cog_db):
        """awaken 输出不应包含 cog_save_fact Tip"""
        # 插入一些 facts（确保 facts > 5）
        for i in range(8):
            cog_db.insert_fact({
                "collection": "my-app",
                "assertion": f"Tip test fact {i}",
                "confidence": 0.9,
            })

        result = handler.awaken({
            "session_id": "tip-test",
            "cwd": "/project/my-app",
        })

        output = result.get("output", "")
        assert "cog_save_fact" not in output
        assert "Tip:" not in output


class TestContextEnrichment:
    """Issue #82 T6: observe 自动富化 context_json"""

    def test_context_json_enriched(self, handler, cog_db):
        """observe 应自动富化 fact 的 context_json"""
        # 先设置 current_task
        cog_db.get_or_create_session("enrich-test", "my-app")
        cog_db.update_session_context(
            external_id="enrich-test",
            current_task="Fix auth bug",
        )

        # observe 一个 Write 事件
        handler.observe({
            "tool_name": "Write",
            "tool_input": {"file_path": "src/auth/login.py"},
            "tool_response": "File written successfully",
            "session_id": "enrich-test",
            "cwd": "/project/my-app",
        })

        facts = cog_db.get_session_facts("enrich-test", "my-app")
        if facts:
            for f in facts:
                # 跳过 observer 产出的 facts（有独立 context 结构）
                if (f.get("source") or "").startswith("observer_"):
                    continue
                ctx = json.loads(f.get("context_json") or "{}")
                # 至少应有 session_phase 或 tool_sequence
                if ctx and not ctx.get("source_pattern"):
                    # context 被富化了
                    assert any(
                        k in ctx for k in ("task", "related_files", "tool_sequence", "session_phase")
                    )

    def test_pattern_facts_not_overwritten(self, handler, cog_db):
        """来自 patterns.py 的 fact 不应被 enrich 覆盖"""
        from index1.cognitive.patterns import _make_pattern_fact

        fact = _make_pattern_fact(
            assertion="Test pattern fact for no-overwrite",
            category="decision",
            collection="my-app",
            session_id="s1",
            related_files=["a.py"],
            tool_sequence="Read→Edit",
        )
        ctx = json.loads(fact["context_json"])
        assert ctx["source_pattern"] is True
        assert ctx["tool_sequence"] == "Read→Edit"


class TestScopeModulesAutoFill:
    """Issue #82 T6: scope_modules 从 scope_files 自动推断"""

    def test_scope_modules_auto_derived(self, handler, cog_db):
        """scope_modules 应从 scope_files 的父目录自动推断"""
        handler.observe({
            "tool_name": "Write",
            "tool_input": {"file_path": "src/auth/login.py"},
            "tool_response": "Written",
            "session_id": "mod-test",
            "cwd": "/project/my-app",
        })

        facts = cog_db.get_session_facts("mod-test", "my-app")
        for f in facts:
            scope_files = json.loads(f.get("scope_files") or "[]")
            scope_modules = json.loads(f.get("scope_modules") or "[]")
            if scope_files:
                # 有 scope_files 就应该有 scope_modules
                assert isinstance(scope_modules, list)


class TestEventSummaryPersist:
    """Issue #82 T4: session 事件摘要持久化"""

    def test_event_summary_persisted(self, handler, cog_db):
        """reflect 应将事件摘要持久化到 sessions.event_summary"""
        sid = "evsum-test"
        cog_db.get_or_create_session(sid, "my-app")

        # 模拟几个工具调用
        for tool in ["Read", "Write", "Bash"]:
            handler.observe({
                "tool_name": tool,
                "tool_input": {"file_path": "src/x.py"} if tool != "Bash" else {"command": "pytest"},
                "tool_response": "ok",
                "session_id": sid,
                "cwd": "/project/my-app",
            })

        # reflect 持久化摘要
        handler.reflect({
            "session_id": sid,
            "cwd": "/project/my-app",
        })

        # 读取 sessions 表验证
        row = cog_db.conn.execute(
            "SELECT event_summary FROM sessions WHERE external_id=?", [sid]
        ).fetchone()
        if row and row[0]:
            summary = json.loads(row[0])
            assert "fact_count" in summary
            assert "category_counts" in summary
            assert summary["fact_count"] >= 1


class TestDiagnosis:
    """Issue #82 T1: cog doctor 诊断脚本"""

    def test_diagnosis_empty_db(self, cog_db):
        """空数据库诊断不应崩溃"""
        from index1.cognitive.diagnosis import run_diagnosis, format_report

        report = run_diagnosis(cog_db)
        assert "tables" in report
        assert "summary" in report
        text = format_report(report)
        assert "Schema Population Report" in text

    def test_diagnosis_with_data(self, cog_db):
        """有数据的诊断应显示正确的填充率"""
        from index1.cognitive.diagnosis import run_diagnosis

        # 插入一些 facts
        for i in range(5):
            cog_db.insert_fact({
                "collection": "diag-test",
                "assertion": f"Diagnosis test fact {i}",
                "confidence": 0.9,
                "scope_files": json.dumps(["src/a.py"]),
                "context_json": json.dumps({"key": "value"}),
            })

        report = run_diagnosis(cog_db, "diag-test")
        facts = report["tables"]["facts"]
        assert facts["total"] == 5
        assert facts["fields"]["assertion"]["rate"] == 1.0
        assert facts["fields"]["context_json"]["rate"] == 1.0

    def test_diagnosis_json_meaningful(self, cog_db):
        """诊断应区分空 JSON 和有意义 JSON"""
        from index1.cognitive.diagnosis import run_diagnosis

        # 一半有 context_json，一半为空
        for i in range(4):
            ctx = json.dumps({"data": i}) if i < 2 else "{}"
            cog_db.insert_fact({
                "collection": "json-test",
                "assertion": f"JSON test {i}",
                "confidence": 0.9,
                "context_json": ctx,
            })

        report = run_diagnosis(cog_db, "json-test")
        ctx_field = report["tables"]["facts"]["fields"]["context_json"]
        # 4 条记录，context_json 都非空（{}也是非空字符串），但 meaningful 只有 2 条
        assert ctx_field["meaningful"] == 2


class TestMentalModelAutoCreate:
    """Issue #82 T7: mental_models L0 自动创建"""

    def test_auto_create_on_threshold(self, cog_db, config):
        """≥5 个 facts 同一 scope 应自动创建 mental model"""
        # 插入 6 个 facts 到同一 scope
        for i in range(6):
            cog_db.insert_fact({
                "collection": "model-test",
                "assertion": f"Model auto-create fact {i}",
                "confidence": 0.9,
                "scope_files": json.dumps(["src/index1/cognitive/hooks.py"]),
            })

        engine = MaintenanceEngine(cog_db, config, None)
        engine._chat_available = False
        results = asyncio.run(engine.run("model-test"))

        # 检查 mental_models 表
        models = cog_db.get_active_models("model-test")
        # 至少应有一个自动创建的 model
        assert len(models) >= 1
        model = models[0]
        assert model["understanding_level"] > 0
        assert "Module" in model.get("subject", "")

    def test_no_create_below_threshold(self, cog_db, config):
        """<5 个 facts 不应自动创建 model"""
        for i in range(3):
            cog_db.insert_fact({
                "collection": "no-model",
                "assertion": f"Below threshold {i}",
                "confidence": 0.9,
                "scope_files": json.dumps(["src/only/here.py"]),
            })

        engine = MaintenanceEngine(cog_db, config, None)
        engine._chat_available = False
        asyncio.run(engine.run("no-model"))

        models = cog_db.get_active_models("no-model")
        assert len(models) == 0


class TestTacticsAutoExtract:
    """Issue #82 T8: tactics L0 自动提取"""

    def test_tactics_from_repeated_root_cause(self, cog_db, config):
        """同一 scope 出现 ≥2 个 root_cause → 自动提取 tactic"""
        scope = json.dumps(["src/auth/login.py"])
        for i in range(3):
            cog_db.insert_fact({
                "collection": "tactic-test",
                "assertion": f"Root cause in login module variant {i}",
                "category": "root_cause",
                "confidence": 0.8,
                "scope_files": scope,
                "context_json": json.dumps({"tool_sequence": "Read→Edit→Bash"}),
            })

        engine = MaintenanceEngine(cog_db, config, None)
        engine._chat_available = False
        asyncio.run(engine.run("tactic-test"))

        tactics = cog_db.search_tactics(scope, "tactic-test")
        assert len(tactics) >= 1
