"""dedup.py 单元测试 — Jaccard 相似度 / 多层去重管道"""

from pathlib import Path
from unittest.mock import MagicMock

import pytest

from index1.cognitive.db_cog import CogDatabase
from index1.cognitive.dedup import (
    COSINE_DUP_THRESHOLD,
    JACCARD_DUP_THRESHOLD,
    JACCARD_SIMILAR_THRESHOLD,
    DedupResult,
    _jaccard_similarity,
    check_duplicate,
)


# ── fixtures ──


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


def _make_fact(**overrides) -> dict:
    base = {
        "collection": "test-proj",
        "assertion": "Modified src/main.py",
        "category": "code",
        "session_id": "session-1",
        "confidence": 0.9,
        "source": "hook",
        "scope_files": "[]",
        "scope_modules": "[]",
        "scope_entities": "[]",
        "context_json": "{}",
        "scope": "project",
    }
    base.update(overrides)
    return base


# ── _jaccard_similarity ──


class TestJaccardSimilarity:
    def test_identical(self):
        assert _jaccard_similarity("hello world", "hello world") == 1.0

    def test_no_overlap(self):
        assert _jaccard_similarity("hello world", "foo bar") == 0.0

    def test_partial_overlap(self):
        sim = _jaccard_similarity(
            "Fixed auth bug in login",
            "Fixed auth bug in login flow",
        )
        # 停用词 "in" 被过滤
        # tokens: {fixed, auth, bug, login} vs {fixed, auth, bug, login, flow}
        # intersection=4, union=5 → 4/5 = 0.8
        assert 0.75 <= sim <= 0.85

    def test_empty_a(self):
        assert _jaccard_similarity("", "hello") == 0.0

    def test_empty_b(self):
        assert _jaccard_similarity("hello", "") == 0.0

    def test_both_empty(self):
        assert _jaccard_similarity("", "") == 0.0

    def test_case_insensitive(self):
        assert _jaccard_similarity("Hello World", "hello world") == 1.0

    def test_cjk_tokens(self):
        # CJK 按空格分词（粗糙但够用）
        sim = _jaccard_similarity("修改 数据库 连接", "修改 数据库 配置")
        # {修改, 数据库, 连接} vs {修改, 数据库, 配置} → 2/4 = 0.5
        assert 0.4 < sim < 0.6

    def test_threshold_constants(self):
        assert JACCARD_DUP_THRESHOLD == 0.70  # #128: 0.75→0.70, 碰撞测试确认安全
        assert JACCARD_SIMILAR_THRESHOLD == 0.4
        assert COSINE_DUP_THRESHOLD == 0.85


# ── check_duplicate ──


class TestCheckDuplicate:
    def test_exact_match(self, cog_db: CogDatabase):
        """精确匹配 → is_dup=True, method=exact"""
        cog_db.insert_fact(_make_fact(assertion="SQLite needs WAL mode"))
        result = check_duplicate(
            assertion="SQLite needs WAL mode",
            collection="test-proj",
            session_id="session-1",
            db_cog=cog_db,
        )
        assert result.is_dup is True
        assert result.method == "exact"
        assert result.similarity == 1.0

    def test_no_match(self, cog_db: CogDatabase):
        """完全不同 → is_dup=False"""
        cog_db.insert_fact(_make_fact(assertion="SQLite needs WAL mode"))
        result = check_duplicate(
            assertion="Python asyncio event loop management",
            collection="test-proj",
            session_id="session-1",
            db_cog=cog_db,
        )
        assert result.is_dup is False
        assert result.method == "none"

    def test_jaccard_high_similarity(self, cog_db: CogDatabase):
        """高 Jaccard → is_dup=True, method=jaccard"""
        cog_db.insert_fact(_make_fact(
            assertion="Fixed authentication bug in login handler",
        ))
        result = check_duplicate(
            assertion="Fixed authentication bug in login handler code",
            collection="test-proj",
            session_id="session-1",
            db_cog=cog_db,
        )
        assert result.is_dup is True
        assert result.method == "jaccard"
        assert result.similarity >= JACCARD_DUP_THRESHOLD

    def test_jaccard_medium_similarity_not_dup(self, cog_db: CogDatabase):
        """中等 Jaccard → 不判重，但有 similar_facts"""
        cog_db.insert_fact(_make_fact(
            assertion="Fixed authentication bug in login handler",
        ))
        result = check_duplicate(
            assertion="authentication login issue needs investigation",
            collection="test-proj",
            session_id="session-1",
            db_cog=cog_db,
        )
        assert result.is_dup is False
        # similar_facts 可能有也可能没有，取决于 FTS5 是否命中

    def test_cosine_layer_skipped_no_embedder(self, cog_db: CogDatabase):
        """embedder=None → 跳过向量层"""
        cog_db.insert_fact(_make_fact(assertion="test fact"))
        result = check_duplicate(
            assertion="completely different statement here",
            collection="test-proj",
            session_id="session-1",
            db_cog=cog_db,
            embedder=None,
        )
        assert result.is_dup is False
        # 确认没有走到 cosine 层
        assert result.method in ("none", "exact", "jaccard")

    def test_cosine_layer_skipped_unavailable(self, cog_db: CogDatabase):
        """embedder.available=False → 跳过向量层"""
        mock_embedder = MagicMock()
        mock_embedder.available = False
        result = check_duplicate(
            assertion="test assertion",
            collection="test-proj",
            session_id="session-1",
            db_cog=cog_db,
            embedder=mock_embedder,
        )
        assert result.is_dup is False

    def test_different_collection_not_dup(self, cog_db: CogDatabase):
        """不同 collection → 不判重"""
        cog_db.insert_fact(_make_fact(
            assertion="SQLite needs WAL mode",
            collection="project-a",
        ))
        result = check_duplicate(
            assertion="SQLite needs WAL mode",
            collection="project-b",
            session_id="session-1",
            db_cog=cog_db,
        )
        # fact_exists 按 collection 过滤，不同集合不算精确重复
        assert result.is_dup is False

    def test_dedup_result_dataclass(self):
        """验证 DedupResult 字段"""
        r = DedupResult(is_dup=False)
        assert r.is_dup is False
        assert r.similar_facts == []
        assert r.method == "none"
        assert r.similarity == 0.0

    def test_similar_facts_limited(self, cog_db: CogDatabase):
        """similar_facts 最多返回 5 个"""
        for i in range(10):
            cog_db.insert_fact(_make_fact(
                assertion=f"database query optimization pattern number {i}",
            ))
        result = check_duplicate(
            assertion="totally new insight about caching",
            collection="test-proj",
            session_id="session-1",
            db_cog=cog_db,
        )
        assert len(result.similar_facts) <= 5
