"""Issue #81: 跨项目知识迁移 — scope + include_universal + promote 测试"""

from __future__ import annotations

import asyncio
import json
from pathlib import Path

import pytest

from index1.cognitive.db_cog import CogDatabase
from index1.cognitive.maintenance import MaintenanceEngine
from index1.cognitive.rules import looks_project_specific
from index1.cognitive.search_cog import _check_promote_on_access, recall
from index1.config import Config


@pytest.fixture()
def cog_db(tmp_path: Path) -> CogDatabase:
    db = CogDatabase(tmp_path / "cross_project_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,
        cognition_model="llama3.2:3b",
    )


@pytest.fixture()
def engine(cog_db, config) -> MaintenanceEngine:
    return MaintenanceEngine(db_cog=cog_db, config=config, embedder=None)


def _make_fact(**overrides) -> dict:
    base = {
        "collection": "test-proj",
        "assertion": "Always validate user input before processing",
        "category": "decision",
        "session_id": "s1",
        "confidence": 0.9,
        "source": "hook",
    }
    base.update(overrides)
    return base


# ── Schema ──


class TestSchemaScope:
    def test_scope_column_exists(self, cog_db: CogDatabase):
        """scope 列存在于 facts 表"""
        row = cog_db.conn.execute(
            "PRAGMA table_info(facts)"
        ).fetchall()
        col_names = [r[1] for r in row]
        assert "scope" in col_names

    def test_scope_default_is_project(self, cog_db: CogDatabase):
        """scope 默认值为 'project'"""
        fid = cog_db.insert_fact(_make_fact())
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "project"

    def test_scope_index_exists(self, cog_db: CogDatabase):
        """idx_facts_scope 索引存在"""
        indexes = cog_db.conn.execute(
            "SELECT name FROM sqlite_master WHERE type='index'"
        ).fetchall()
        idx_names = {r[0] for r in indexes}
        assert "idx_facts_scope" in idx_names


# ── Insert with scope ──


class TestInsertWithScope:
    def test_insert_default_project_scope(self, cog_db: CogDatabase):
        """不指定 scope 时默认 project"""
        fid = cog_db.insert_fact(_make_fact())
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "project"

    def test_insert_universal_scope(self, cog_db: CogDatabase):
        """指定 scope='universal'"""
        fid = cog_db.insert_fact(_make_fact(scope="universal"))
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "universal"

    def test_update_scope(self, cog_db: CogDatabase):
        """update_fact 可以修改 scope"""
        fid = cog_db.insert_fact(_make_fact())
        cog_db.update_fact(fid, scope="universal")
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "universal"


# ── Search with include_universal ──


class TestSearchWithUniversal:
    def _setup_facts(self, cog_db: CogDatabase):
        """创建 project + universal facts"""
        cog_db.insert_fact(_make_fact(
            assertion="Project-specific database connection pooling strategy",
        ))
        cog_db.insert_fact(_make_fact(
            assertion="Universal database indexing best practice for performance",
            scope="universal",
            collection="other-proj",  # 不同项目但 universal
        ))

    def test_fts_without_universal(self, cog_db: CogDatabase):
        """FTS 搜索不含 universal 时只返回本项目"""
        self._setup_facts(cog_db)
        results = cog_db.fts_search_facts(
            "database", "test-proj", include_universal=False,
        )
        assert all(r["collection"] == "test-proj" for r in results)

    def test_fts_with_universal(self, cog_db: CogDatabase):
        """FTS 搜索含 universal 时同时返回跨项目的 universal facts"""
        self._setup_facts(cog_db)
        results = cog_db.fts_search_facts(
            "database", "test-proj", include_universal=True,
        )
        collections = {r["collection"] for r in results}
        # 应该包含本项目 + other-proj 的 universal fact
        assert "test-proj" in collections
        assert "other-proj" in collections

    def test_get_top_facts_with_universal(self, cog_db: CogDatabase):
        """get_top_facts 含 universal"""
        self._setup_facts(cog_db)
        results = cog_db.get_top_facts(
            "test-proj", min_confidence=0.5, include_universal=True,
        )
        scopes = {r["scope"] for r in results}
        assert "universal" in scopes

    def test_get_top_facts_without_universal(self, cog_db: CogDatabase):
        """get_top_facts 不含 universal"""
        self._setup_facts(cog_db)
        results = cog_db.get_top_facts(
            "test-proj", min_confidence=0.5, include_universal=False,
        )
        assert all(r["scope"] == "project" for r in results)

    def test_recall_includes_universal_by_default(self, cog_db: CogDatabase):
        """recall 默认 include_universal=True"""
        self._setup_facts(cog_db)
        result = asyncio.run(recall(
            "database", cog_db, collection="test-proj",
        ))
        collections = {f["collection"] for f in result.facts}
        assert "other-proj" in collections  # universal fact 被召回


# ── Promote to Universal ──


class TestPromoteToUniversal:
    def test_promote_changes_scope(self, cog_db: CogDatabase):
        """promote_to_universal 将 scope 改为 universal"""
        fid = cog_db.insert_fact(_make_fact(scope_files='["src/main.py"]'))
        ok = cog_db.promote_to_universal(fid)
        assert ok is True
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "universal"
        assert json.loads(fact["scope_files"]) == []

    def test_promote_idempotent(self, cog_db: CogDatabase):
        """已是 universal 的 fact 再次 promote 不报错"""
        fid = cog_db.insert_fact(_make_fact(scope="universal"))
        ok = cog_db.promote_to_universal(fid)
        # WHERE scope='project' 不匹配，不做任何更新，但事务仍成功
        assert ok is True
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "universal"

    def test_promote_inactive_rejected(self, cog_db: CogDatabase):
        """inactive fact 不会被提升"""
        fid = cog_db.insert_fact(_make_fact())
        cog_db.update_fact(fid, status="inactive")
        ok = cog_db.promote_to_universal(fid)
        # 事务成功但 WHERE 不匹配，scope 不变
        assert ok is True
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "project"

    def test_promote_clears_scope_files(self, cog_db: CogDatabase):
        """promote 清空 scope_files"""
        fid = cog_db.insert_fact(_make_fact(
            scope_files='["src/a.py", "src/b.py"]',
        ))
        cog_db.promote_to_universal(fid)
        fact = cog_db.get_fact(fid)
        assert json.loads(fact["scope_files"]) == []


# ── Universal Candidates ──


class TestUniversalCandidates:
    def _insert_candidate(self, cog_db, **overrides):
        fact = _make_fact(
            confidence=0.9,
            category="decision",
            scope="project",
        )
        fact.update(overrides)
        fid = cog_db.insert_fact(fact)
        # 增加 access_count 到 3
        for _ in range(3):
            cog_db.touch_fact(fid)
        return fid

    def test_high_confidence_high_access(self, cog_db: CogDatabase):
        """满足条件的 fact 出现在候选列表"""
        self._insert_candidate(cog_db)
        candidates = cog_db.get_universal_candidates("test-proj")
        assert len(candidates) >= 1

    def test_low_confidence_excluded(self, cog_db: CogDatabase):
        """低置信度被排除"""
        self._insert_candidate(cog_db, confidence=0.5)
        candidates = cog_db.get_universal_candidates("test-proj")
        assert len(candidates) == 0

    def test_low_access_count_excluded(self, cog_db: CogDatabase):
        """低访问次数被排除"""
        fid = cog_db.insert_fact(_make_fact(
            confidence=0.9, category="decision",
        ))
        # 只 touch 一次（不够 3 次）
        cog_db.touch_fact(fid)
        candidates = cog_db.get_universal_candidates("test-proj")
        assert len(candidates) == 0

    def test_wrong_category_excluded(self, cog_db: CogDatabase):
        """非决策/根因类的 category 被排除"""
        self._insert_candidate(cog_db, category="code")
        candidates = cog_db.get_universal_candidates("test-proj")
        assert len(candidates) == 0

    def test_already_universal_excluded(self, cog_db: CogDatabase):
        """已经是 universal 的不出现在候选列表"""
        self._insert_candidate(cog_db, scope="universal")
        candidates = cog_db.get_universal_candidates("test-proj")
        assert len(candidates) == 0


# ── Maintenance Promote Step ──


class TestMaintenancePromoteStep:
    def _insert_promotable(self, cog_db: CogDatabase):
        """创建一个可提升的 fact"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Always use parameterized queries to prevent SQL injection",
            category="decision",
            confidence=0.9,
            scope_files="[]",
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        return fid

    def test_promotes_generic_fact(self, cog_db, engine):
        """通用 fact 被提升为 universal"""
        fid = self._insert_promotable(cog_db)
        promoted = asyncio.run(
            engine._step_promote_universal("test-proj", "s1")
        )
        assert promoted >= 1
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "universal"

    def test_skips_project_specific(self, cog_db, engine):
        """含项目特定路径的 fact 不被提升"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Fixed bug in src/index1/db.py line 42",
            category="root_cause",
            confidence=0.95,
            scope_files="[]",
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        promoted = asyncio.run(
            engine._step_promote_universal("test-proj", "s1")
        )
        assert promoted == 0
        fact = cog_db.get_fact(fid)
        assert fact["scope"] == "project"

    def test_skips_fact_with_scope_files(self, cog_db, engine):
        """有 scope_files 的 fact 不被提升"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Always use parameterized queries",
            category="decision",
            confidence=0.9,
            scope_files='["src/db.py"]',
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        promoted = asyncio.run(
            engine._step_promote_universal("test-proj", "s1")
        )
        assert promoted == 0

    def test_no_ollama_required(self, cog_db, config):
        """L0 纯规则：不需要 Ollama"""
        engine = MaintenanceEngine(db_cog=cog_db, config=config, embedder=None)
        fid = cog_db.insert_fact(_make_fact(
            assertion="JWT tokens should have short expiration times",
            category="architecture",
            confidence=0.9,
            scope_files="[]",
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        promoted = asyncio.run(
            engine._step_promote_universal("test-proj", "s1")
        )
        assert promoted >= 1


# ── looks_project_specific ──


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

    def test_test_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_bare_filename_rust(self):
        assert looks_project_specific("Updated helpers.rs") is True

    def test_utils_path(self):
        """utils/ 目录也视为项目特定"""
        assert looks_project_specific("Updated utils/helpers.py") is True

    def test_common_lib_not_project_specific(self):
        """通用库三段点分不视为项目特定"""
        assert looks_project_specific(
            "Use requests.adapters.HTTPAdapter for retry"
        ) is False

    def test_logging_handler_not_project_specific(self):
        assert looks_project_specific(
            "Configure logging.handlers.RotatingFileHandler"
        ) is False

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

    def test_general_best_practice(self):
        assert looks_project_specific(
            "Use parameterized queries to prevent SQL injection"
        ) is False


# ── Promote on Access ──


class TestPromoteOnAccess:
    def test_promotes_when_threshold_reached(self, cog_db: CogDatabase):
        """access_count 达阈值时自动提升为 universal"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Always use parameterized queries to prevent SQL injection",
            category="decision",
            confidence=0.9,
            scope_files="[]",
        ))
        # 模拟 3 次访问（DB 中 access_count=3，达到阈值）
        for _ in range(3):
            cog_db.touch_fact(fid)
        fact = cog_db.get_fact(fid)
        _check_promote_on_access(cog_db, fact)
        updated = cog_db.get_fact(fid)
        assert updated["scope"] == "universal"

    def test_skips_low_confidence(self, cog_db: CogDatabase):
        """低 confidence 不提升"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="General best practice for testing",
            category="decision",
            confidence=0.5,
            scope_files="[]",
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        fact = cog_db.get_fact(fid)
        _check_promote_on_access(cog_db, fact)
        assert cog_db.get_fact(fid)["scope"] == "project"

    def test_skips_project_specific(self, cog_db: CogDatabase):
        """含项目路径不提升"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Fixed critical bug in src/index1/db.py line 42",
            category="root_cause",
            confidence=0.95,
            scope_files="[]",
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        fact = cog_db.get_fact(fid)
        _check_promote_on_access(cog_db, fact)
        assert cog_db.get_fact(fid)["scope"] == "project"

    def test_skips_with_scope_files(self, cog_db: CogDatabase):
        """有 scope_files 不提升"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Always validate user input",
            category="decision",
            confidence=0.9,
            scope_files='["src/auth.py"]',
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        fact = cog_db.get_fact(fid)
        _check_promote_on_access(cog_db, fact)
        assert cog_db.get_fact(fid)["scope"] == "project"

    def test_skips_wrong_category(self, cog_db: CogDatabase):
        """非决策/根因类不提升"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="General coding practice",
            category="code",
            confidence=0.9,
            scope_files="[]",
        ))
        for _ in range(3):
            cog_db.touch_fact(fid)
        fact = cog_db.get_fact(fid)
        _check_promote_on_access(cog_db, fact)
        assert cog_db.get_fact(fid)["scope"] == "project"
