"""记忆分层衰减测试 — LAYER_CONFIG + Ebbinghaus 公式

Issue #106: 记忆分层优化
Phase A: 分层衰减 + Ebbinghaus 公式
"""

from __future__ import annotations

import math
from pathlib import Path

import pytest

from index1.cognitive.db_cog import (
    LAYER_CONFIG,
    VALID_CATEGORIES,
    CogDatabase,
    get_default_decay_rate,
    get_layer,
)


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


def _make_fact(**overrides) -> dict:
    base = {
        "collection": "test-proj",
        "assertion": "Test assertion",
        "category": "code",
        "session_id": "session-1",
        "confidence": 0.9,
        "source": "hook",
    }
    base.update(overrides)
    return base


# ── LAYER_CONFIG 结构测试 ──


class TestLayerConfig:
    def test_l3b_categories_exist(self):
        """L3b 层包含 constraint 和 preference"""
        assert get_layer("constraint") == "L3b"
        assert get_layer("preference") == "L3b"

    def test_l3a_categories_exist(self):
        """L3a 层包含 pattern"""
        assert get_layer("pattern") == "L3a"

    def test_l2_categories_exist(self):
        """L2 层包含所有战术记忆 category"""
        l2_cats = {"decision", "root_cause", "tradeoff", "revert", "code", "architecture", "config"}
        for cat in l2_cats:
            assert get_layer(cat) == "L2", f"{cat} should be L2"

    def test_unknown_category_defaults_l2(self):
        """未知 category 默认 L2"""
        assert get_layer("unknown_cat") == "L2"

    def test_l3b_decay_rate_zero(self):
        """L3b 层衰减率为 0"""
        assert get_default_decay_rate("constraint") == 0.0
        assert get_default_decay_rate("preference") == 0.0

    def test_l3a_decay_rate_very_small(self):
        """L3a 层衰减率极小"""
        rate = get_default_decay_rate("pattern")
        assert 0 < rate < 0.005

    def test_l2_decay_rate_reasonable(self):
        """L2 层衰减率在合理范围"""
        for cat in ("decision", "root_cause", "code"):
            rate = get_default_decay_rate(cat)
            assert 0.001 <= rate <= 0.02, f"{cat} decay rate {rate} out of range"

    def test_decision_decays_slower_than_code(self):
        """decision 默认衰减率低于 code"""
        assert get_default_decay_rate("decision") < get_default_decay_rate("code")

    def test_valid_categories_matches_config(self):
        """VALID_CATEGORIES 与 LAYER_CONFIG keys 一致"""
        assert VALID_CATEGORIES == frozenset(LAYER_CONFIG.keys())

    def test_all_config_entries_have_required_keys(self):
        """每个 LAYER_CONFIG 条目都有 decay_rate 和 layer"""
        for cat, cfg in LAYER_CONFIG.items():
            assert "decay_rate" in cfg, f"{cat} missing decay_rate"
            assert "layer" in cfg, f"{cat} missing layer"
            assert cfg["layer"] in ("L3b", "L3a", "L2"), f"{cat} invalid layer: {cfg['layer']}"


# ── insert_fact 自动推导 decay_rate ──


class TestInsertDecayRate:
    def test_code_gets_default_decay_rate(self, cog_db: CogDatabase):
        """code category 自动使用 LAYER_CONFIG 的 decay_rate"""
        fid = cog_db.insert_fact(_make_fact(category="code"))
        fact = cog_db.get_fact(fid)
        assert fact["decay_rate"] == get_default_decay_rate("code")

    def test_constraint_gets_zero_decay_rate(self, cog_db: CogDatabase):
        """constraint category 自动使用 0.0 衰减率"""
        fid = cog_db.insert_fact(_make_fact(category="constraint"))
        fact = cog_db.get_fact(fid)
        assert fact["decay_rate"] == 0.0

    def test_explicit_decay_rate_overrides(self, cog_db: CogDatabase):
        """显式提供 decay_rate 时优先使用"""
        fid = cog_db.insert_fact(_make_fact(category="code", decay_rate=0.05))
        fact = cog_db.get_fact(fid)
        assert fact["decay_rate"] == 0.05

    def test_pattern_gets_small_decay_rate(self, cog_db: CogDatabase):
        """pattern category 自动使用极小衰减率"""
        fid = cog_db.insert_fact(_make_fact(category="pattern"))
        fact = cog_db.get_fact(fid)
        assert fact["decay_rate"] == get_default_decay_rate("pattern")


# ── L3b 不衰减 ──


class TestL3bNoDecay:
    def test_constraint_never_decays(self, cog_db: CogDatabase):
        """constraint 类 fact 即使很旧也不衰减"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Must always use offline-first design",
            category="constraint",
            confidence=0.95,
        ))
        # 设为 100 天前
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-100 days') WHERE id=?",
            [fid],
        )
        cog_db.conn.commit()

        cog_db.decay_facts("test-proj", stale_days=7)

        fact = cog_db.get_fact(fid)
        assert fact["confidence"] == 0.95, "L3b constraint should not decay"

    def test_preference_never_decays(self, cog_db: CogDatabase):
        """preference 类 fact 不衰减"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Prefer pytest over unittest",
            category="preference",
            confidence=0.9,
        ))
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-100 days') WHERE id=?",
            [fid],
        )
        cog_db.conn.commit()

        cog_db.decay_facts("test-proj", stale_days=7)

        fact = cog_db.get_fact(fid)
        assert fact["confidence"] == 0.9, "L3b preference should not decay"


# ── L3a 极慢衰减 ──


class TestL3aSlowDecay:
    def test_pattern_requires_long_stale_period(self, cog_db: CogDatabase):
        """pattern 需要 stale_days×4 才触发衰减"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Every time we skip tests, bugs appear",
            category="pattern",
            confidence=0.9,
        ))
        # 设为 20 天前（stale_days=7 × 4 = 28，20 < 28 不该衰减）
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-20 days') WHERE id=?",
            [fid],
        )
        cog_db.conn.commit()

        cog_db.decay_facts("test-proj", stale_days=7)

        fact = cog_db.get_fact(fid)
        assert fact["confidence"] == 0.9, "Pattern should not decay within stale_days*4"

    def test_pattern_decays_after_long_stale(self, cog_db: CogDatabase):
        """pattern 超过 stale_days×4 后衰减"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Recurring pattern of config drift",
            category="pattern",
            confidence=0.9,
        ))
        # 设为 60 天前（stale_days=7 × 4 = 28, 60 > 28 应衰减）
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-60 days') WHERE id=?",
            [fid],
        )
        cog_db.conn.commit()

        cog_db.decay_facts("test-proj", stale_days=7)

        fact = cog_db.get_fact(fid)
        # 应该衰减但很慢（decay_rate=0.002, factor=0.1）
        assert fact["confidence"] < 0.9
        assert fact["confidence"] > 0.85, "Pattern should decay very slowly"


# ── L2 Ebbinghaus 衰减（访问强化）──


class TestEbbinghausDecay:
    def test_access_count_reduces_decay(self, cog_db: CogDatabase):
        """高访问次数的 fact 衰减更慢"""
        fid_low = cog_db.insert_fact(_make_fact(
            assertion="Rarely accessed fact",
            category="code",
            confidence=0.9,
        ))
        fid_high = cog_db.insert_fact(_make_fact(
            assertion="Frequently accessed fact",
            category="code",
            confidence=0.9,
        ))
        # 设为 30 天前
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-30 days') WHERE id IN (?, ?)",
            [fid_low, fid_high],
        )
        # 给高访问 fact 设置 access_count=10
        cog_db.conn.execute(
            "UPDATE facts SET access_count=10 WHERE id=?", [fid_high]
        )
        cog_db.conn.commit()

        cog_db.decay_facts("test-proj", stale_days=7)

        low_fact = cog_db.get_fact(fid_low)
        high_fact = cog_db.get_fact(fid_high)
        # 高访问 fact 置信度应该更高
        assert high_fact["confidence"] > low_fact["confidence"], (
            f"High access ({high_fact['confidence']}) should decay slower "
            f"than low access ({low_fact['confidence']})"
        )

    def test_ebbinghaus_formula_correctness(self, cog_db: CogDatabase):
        """验证对数 Ebbinghaus 公式: conf * EXP(-decay_rate / (1 + LN(1 + access_count)))"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Formula test fact",
            category="code",
            confidence=0.9,
        ))
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-30 days'), access_count=5 WHERE id=?",
            [fid],
        )
        cog_db.conn.commit()

        decay_rate = get_default_decay_rate("code")
        expected = 0.9 * math.exp(-decay_rate / (1.0 + math.log(1.0 + 5)))

        cog_db.decay_facts("test-proj", stale_days=7)

        fact = cog_db.get_fact(fid)
        assert abs(fact["confidence"] - expected) < 0.001, (
            f"Expected {expected:.6f}, got {fact['confidence']:.6f}"
        )

    def test_decay_floor_preserved(self, cog_db: CogDatabase):
        """衰减不会低于 0.05 底线"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Very low confidence fact",
            category="code",
            confidence=0.06,
        ))
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-100 days') WHERE id=?",
            [fid],
        )
        cog_db.conn.commit()

        cog_db.decay_facts("test-proj", stale_days=7)

        fact = cog_db.get_fact(fid)
        assert fact["confidence"] >= 0.05

    def test_zero_access_count_handled(self, cog_db: CogDatabase):
        """access_count=0 时公式不除零"""
        fid = cog_db.insert_fact(_make_fact(
            assertion="Never accessed fact",
            category="code",
            confidence=0.9,
        ))
        cog_db.conn.execute(
            "UPDATE facts SET created_at=datetime('now', '-30 days'), access_count=0 WHERE id=?",
            [fid],
        )
        cog_db.conn.commit()

        # 不应抛异常
        cog_db.decay_facts("test-proj", stale_days=7)

        fact = cog_db.get_fact(fid)
        assert fact["confidence"] < 0.9
        assert fact["confidence"] > 0.05


# ── auto_classify 新 category 测试 ──


class TestAutoClassifyNewCategories:
    def test_constraint_classification(self):
        from index1.cognitive.auto_classify import classify_category
        assert classify_category("Must always use parameterized queries") == "constraint"
        assert classify_category("必须使用离线优先设计") == "constraint"
        assert classify_category("Never commit .env files") == "constraint"
        assert classify_category("禁止在 orient 中调用网络") == "constraint"

    def test_preference_classification(self):
        from index1.cognitive.auto_classify import classify_category
        assert classify_category("Prefer pytest over unittest") == "preference"
        assert classify_category("推荐使用 asyncio 而非 threading") == "preference"
        assert classify_category("Recommended to use WAL mode for concurrency") == "preference"

    def test_pattern_classification(self):
        from index1.cognitive.auto_classify import classify_category
        assert classify_category("Every time we skip tests, bugs appear") == "pattern"
        assert classify_category("Recurring issue with connection timeouts") == "pattern"
        assert classify_category("总是在周一出现性能问题") == "pattern"

    def test_existing_categories_still_work(self):
        """确保新增 category 不影响原有分类"""
        from index1.cognitive.auto_classify import classify_category
        assert classify_category("Bug caused by race condition") == "root_cause"
        assert classify_category("Chose JWT for authentication") == "decision"
        assert classify_category("Reverted the last change") == "revert"
        assert classify_category("Changed PORT=8080 in configuration") == "config"
