"""Tests for brynq.librarian — Phase 68 knowledge graph maintenance."""

import json
import pytest
from datetime import datetime, timezone, timedelta
from pathlib import Path


@pytest.fixture(autouse=True)
def _isolate_librarian(tmp_path, monkeypatch):
    """Redirect librarian storage to temp directory."""
    lib_dir = tmp_path / "librarian"
    reports_dir = lib_dir / "reports"
    lib_dir.mkdir()
    reports_dir.mkdir()
    monkeypatch.setattr("brynq.librarian.LIBRARIAN_DIR", lib_dir)
    monkeypatch.setattr("brynq.librarian.REPORTS_DIR", reports_dir)
    monkeypatch.setattr("brynq.librarian.MERGE_LOG_PATH", lib_dir / "merge_log.json")


@pytest.fixture
def knowledge_dir(tmp_path, monkeypatch):
    """Create a mock knowledge directory with entries."""
    kd = tmp_path / "knowledge"
    kd.mkdir()

    old_date = (datetime.now(timezone.utc) - timedelta(days=60)).isoformat()
    recent_date = datetime.now(timezone.utc).isoformat()

    entries = [
        {"knowledge_id": "k1", "topic": "Python best practices",
         "content": "Use virtual environments and type hints for python code",
         "category": "codebase", "confidence": 0.9, "tags": ["python"],
         "created_at": old_date, "updated_at": old_date, "archived": False},
        {"knowledge_id": "k2", "topic": "Python coding standards",
         "content": "Use virtual environments and type hints for python development",
         "category": "codebase", "confidence": 0.7, "tags": ["python", "standards"],
         "created_at": recent_date, "updated_at": recent_date, "archived": False},
        {"knowledge_id": "k3", "topic": "Database design patterns",
         "content": "Normalize tables and use indexes for query performance",
         "category": "architecture", "confidence": 0.85, "tags": ["database"],
         "created_at": old_date, "updated_at": old_date, "archived": False},
        {"knowledge_id": "k4", "topic": "Uncategorized entry",
         "content": "How to deploy the code and review the release pipeline",
         "category": "general", "confidence": 0.5, "tags": [],
         "created_at": recent_date, "updated_at": recent_date, "archived": False},
        {"knowledge_id": "k5", "topic": "Archived entry",
         "content": "Old obsolete information",
         "category": "general", "confidence": 0.3, "tags": [],
         "created_at": old_date, "updated_at": old_date, "archived": True},
    ]

    for entry in entries:
        (kd / f"{entry['knowledge_id']}.json").write_text(
            json.dumps(entry), encoding="utf-8")
    (kd / "index.jsonl").write_text("")
    monkeypatch.setattr("brynq.knowledge_mcp.KNOWLEDGE_DIR", kd)
    return kd


# ── scan_for_stale ────────────────────────────────────────────────────────


class TestScanForStale:
    def test_finds_stale_entries(self, knowledge_dir):
        from brynq.librarian import scan_for_stale
        stale = scan_for_stale(max_age_days=30)
        ids = [s["knowledge_id"] for s in stale]
        assert "k1" in ids
        assert "k3" in ids

    def test_excludes_recent(self, knowledge_dir):
        from brynq.librarian import scan_for_stale
        stale = scan_for_stale(max_age_days=30)
        ids = [s["knowledge_id"] for s in stale]
        assert "k2" not in ids
        assert "k4" not in ids

    def test_excludes_archived(self, knowledge_dir):
        from brynq.librarian import scan_for_stale
        stale = scan_for_stale(max_age_days=1)
        ids = [s["knowledge_id"] for s in stale]
        assert "k5" not in ids

    def test_custom_age(self, knowledge_dir):
        from brynq.librarian import scan_for_stale
        stale = scan_for_stale(max_age_days=365)
        # Even old entries are within a year
        assert len(stale) == 0 or all(s["knowledge_id"] in ("k1", "k3") for s in stale)

    def test_empty_knowledge(self, monkeypatch):
        from brynq.librarian import scan_for_stale
        monkeypatch.setattr("brynq.librarian._load_all_knowledge", lambda: [])
        assert scan_for_stale() == []


# ── scan_for_duplicates ───────────────────────────────────────────────────


class TestScanForDuplicates:
    def test_finds_near_duplicates(self, knowledge_dir):
        from brynq.librarian import scan_for_duplicates
        dupes = scan_for_duplicates(similarity_threshold=0.5)
        # k1 and k2 should be similar (python + virtual environments + type hints)
        assert len(dupes) >= 1
        pair_ids = set()
        for d in dupes:
            pair_ids.add(d["entry_a"])
            pair_ids.add(d["entry_b"])
        assert "k1" in pair_ids or "k2" in pair_ids

    def test_high_threshold_fewer_results(self, knowledge_dir):
        from brynq.librarian import scan_for_duplicates
        dupes_low = scan_for_duplicates(similarity_threshold=0.3)
        dupes_high = scan_for_duplicates(similarity_threshold=0.9)
        assert len(dupes_high) <= len(dupes_low)

    def test_similarity_score_present(self, knowledge_dir):
        from brynq.librarian import scan_for_duplicates
        dupes = scan_for_duplicates(similarity_threshold=0.3)
        if dupes:
            assert "similarity" in dupes[0]
            assert 0 < dupes[0]["similarity"] <= 1.0

    def test_sorted_by_similarity(self, knowledge_dir):
        from brynq.librarian import scan_for_duplicates
        dupes = scan_for_duplicates(similarity_threshold=0.3)
        if len(dupes) > 1:
            assert dupes[0]["similarity"] >= dupes[1]["similarity"]

    def test_empty_knowledge(self, monkeypatch):
        from brynq.librarian import scan_for_duplicates
        monkeypatch.setattr("brynq.librarian._load_all_knowledge", lambda: [])
        assert scan_for_duplicates() == []


# ── merge_duplicates ──────────────────────────────────────────────────────


class TestMergeDuplicates:
    def test_merges_entries(self, knowledge_dir):
        from brynq.librarian import merge_duplicates
        result = merge_duplicates(["k1", "k2"])
        assert result["kept"] in ("k1", "k2")
        assert len(result["archived"]) == 1

    def test_keeps_specified_id(self, knowledge_dir):
        from brynq.librarian import merge_duplicates
        result = merge_duplicates(["k1", "k2"], keep_id="k2")
        assert result["kept"] == "k2"

    def test_merges_tags(self, knowledge_dir):
        from brynq.librarian import merge_duplicates
        result = merge_duplicates(["k1", "k2"])
        assert "python" in result["merged_tags"]
        assert "standards" in result["merged_tags"]

    def test_keeps_highest_confidence(self, knowledge_dir):
        from brynq.librarian import merge_duplicates
        result = merge_duplicates(["k1", "k2"])
        assert result["confidence"] == 0.9  # k1 has 0.9

    def test_too_few_ids(self):
        from brynq.librarian import merge_duplicates
        with pytest.raises(ValueError, match="at least 2"):
            merge_duplicates(["k1"])

    def test_logs_merge(self, knowledge_dir):
        from brynq.librarian import merge_duplicates, LIBRARIAN_DIR
        merge_duplicates(["k1", "k2"])
        log_path = LIBRARIAN_DIR / "merge_log.json"
        assert log_path.exists()
        log = json.loads(log_path.read_text("utf-8"))
        assert len(log) == 1


# ── suggest_categories ────────────────────────────────────────────────────


class TestSuggestCategories:
    def test_suggests_for_entry(self, knowledge_dir):
        from brynq.librarian import suggest_categories
        suggestions = suggest_categories("k4")
        # k4 mentions "deployment" which should match "process"
        assert len(suggestions) >= 1

    def test_returns_scores(self, knowledge_dir):
        from brynq.librarian import suggest_categories
        suggestions = suggest_categories("k1")
        if suggestions:
            assert "score" in suggestions[0]
            assert "category" in suggestions[0]

    def test_sorted_by_score(self, knowledge_dir):
        from brynq.librarian import suggest_categories
        suggestions = suggest_categories("k1")
        if len(suggestions) > 1:
            assert suggestions[0]["score"] >= suggestions[1]["score"]

    def test_nonexistent_entry(self):
        from brynq.librarian import suggest_categories
        # Should return empty, not crash
        assert suggest_categories("nonexistent") == []

    def test_matching_keywords_present(self, knowledge_dir):
        from brynq.librarian import suggest_categories
        suggestions = suggest_categories("k3")
        if suggestions:
            assert "matching_keywords" in suggestions[0]
            assert len(suggestions[0]["matching_keywords"]) > 0


# ── generate_summary ──────────────────────────────────────────────────────


class TestGenerateSummary:
    def test_all_categories(self, knowledge_dir):
        from brynq.librarian import generate_summary
        summary = generate_summary()
        assert summary["entry_count"] >= 3  # excludes archived

    def test_filtered_by_category(self, knowledge_dir):
        from brynq.librarian import generate_summary
        summary = generate_summary(category="architecture")
        assert summary["entry_count"] == 1
        assert summary["category"] == "architecture"

    def test_has_topics(self, knowledge_dir):
        from brynq.librarian import generate_summary
        summary = generate_summary()
        assert len(summary["topics"]) >= 1

    def test_empty_category(self, knowledge_dir):
        from brynq.librarian import generate_summary
        summary = generate_summary(category="reference")
        assert summary["entry_count"] == 0


# ── get_maintenance_report ────────────────────────────────────────────────


class TestGetMaintenanceReport:
    def test_generates_report(self, knowledge_dir):
        from brynq.librarian import get_maintenance_report
        report = get_maintenance_report()
        assert "total_entries" in report
        assert "stale_count" in report
        assert "duplicate_pairs" in report
        assert "uncategorized_count" in report

    def test_saves_report_file(self, knowledge_dir):
        from brynq.librarian import get_maintenance_report, REPORTS_DIR
        get_maintenance_report()
        files = list(REPORTS_DIR.glob("*.json"))
        assert len(files) == 1

    def test_counts_uncategorized(self, knowledge_dir):
        from brynq.librarian import get_maintenance_report
        report = get_maintenance_report()
        assert report["uncategorized_count"] >= 1  # k4 is "general"

    def test_empty_knowledge(self, monkeypatch):
        from brynq.librarian import get_maintenance_report
        monkeypatch.setattr("brynq.librarian._load_all_knowledge", lambda: [])
        report = get_maintenance_report()
        assert report["total_entries"] == 0
