"""Tests for knowledge freshness categories and TTL-based staleness.

Covers:
- Knowledge categorizer (keyword-based classification)
- TTL assignment per category
- TTL-based staleness detection
- Freshness report (GREEN/YELLOW/RED)
- Confirm resets TTL clock
- Manual category override
- Edge cases (multi-category, title-only, etc.)
"""
import pytest
from datetime import datetime, timezone, timedelta

from greymatter_core.db import SQLiteDatabase
from greymatter_core.knowledge import KnowledgeStore, _compute_freshness_status
from greymatter_core.knowledge_categorizer import (
    classify_entry,
    classify_batch,
    get_ttl_for_category,
    CATEGORIES,
    CATEGORY_MAP,
    VALID_CATEGORIES,
    DEFAULT_CATEGORY,
    DEFAULT_TTL,
)


# --- Fixtures ---


@pytest.fixture
def db():
    return SQLiteDatabase(":memory:")


@pytest.fixture
def knowledge(db):
    return KnowledgeStore(db)


# --- Categorizer unit tests ---


class TestKnowledgeCategorizer:
    """Test the keyword-based knowledge categorizer."""

    def test_infrastructure_by_keywords(self):
        entry = {
            "title": "Coordinator runs on port 8000",
            "content": "The coordinator service listens on port 8000 at IP 100.81.247.14",
            "entry_type": "reference",
            "domain": "gmnet",
        }
        cat, ttl = classify_entry(entry)
        assert cat == "infrastructure"
        assert ttl == 7

    def test_infrastructure_by_domain(self):
        entry = {
            "title": "CockroachDB node topology",
            "content": "Three-node cluster across availability zones",
            "entry_type": "reference",
            "domain": "infrastructure",
        }
        cat, ttl = classify_entry(entry)
        assert cat == "infrastructure"
        assert ttl == 7

    def test_architecture_by_keywords(self):
        entry = {
            "title": "Event-driven data pipeline design",
            "content": "Components communicate via message queue using pub/sub protocol",
            "entry_type": "expertise",
            "domain": "gmnet",
        }
        cat, ttl = classify_entry(entry)
        assert cat == "architecture"
        assert ttl == 14

    def test_configuration_by_keywords(self):
        entry = {
            "title": "Rate limit settings for API gateway",
            "content": "Rate limit threshold is 1000 req/s with timeout of 30s",
            "entry_type": "reference",
            "domain": None,
        }
        cat, ttl = classify_entry(entry)
        assert cat == "configuration"
        assert ttl == 14

    def test_correction_by_entry_type(self):
        entry = {
            "title": "Fix pagination bug in sync endpoint",
            "content": "The cursor was using non-unique sync_version column",
            "entry_type": "correction",
            "domain": "database",
        }
        cat, ttl = classify_entry(entry)
        assert cat == "correction"
        assert ttl == 30

    def test_gotcha_maps_to_correction(self):
        entry = {
            "title": "SQLite WAL gotcha",
            "content": "WAL mode doesn't work across network mounts",
            "entry_type": "gotcha",
            "domain": "database",
        }
        cat, ttl = classify_entry(entry)
        assert cat == "correction"
        assert ttl == 30

    def test_process_by_keywords(self):
        entry = {
            "title": "How to upgrade the coordinator",
            "content": "Step 1: Install dependencies. Step 2: Run migration. Step 3: Follow the runbook procedure",
            "entry_type": "reference",
            "domain": None,
        }
        cat, ttl = classify_entry(entry)
        assert cat == "process"
        assert ttl == 30

    def test_pattern_by_entry_type(self):
        entry = {
            "title": "Repository pattern for data access",
            "content": "Use repository pattern to abstract database calls",
            "entry_type": "pattern",
            "domain": "database",
        }
        cat, ttl = classify_entry(entry)
        # Pattern entry_type matches pattern category (60d)
        assert cat == "pattern"
        assert ttl == 60

    def test_domain_expertise_by_keywords(self):
        entry = {
            "title": "FIPS 140-3 cryptographic requirements",
            "content": "FIPS requires NIST-approved algorithms for all encryption operations",
            "entry_type": "expertise",
            "domain": "security",
        }
        cat, ttl = classify_entry(entry)
        assert cat == "domain_expertise"
        assert ttl == 90

    def test_domain_expertise_by_entry_type(self):
        entry = {
            "title": "Python asyncio concurrency model",
            "content": "Use asyncio.gather for concurrent coroutines",
            "entry_type": "expertise",
            "domain": "python",
        }
        cat, ttl = classify_entry(entry)
        # "expertise" entry_type matches domain_expertise
        assert cat == "domain_expertise"
        assert ttl == 90

    def test_no_match_defaults_to_domain_expertise(self):
        entry = {
            "title": "Random thought",
            "content": "This is just a random observation with no keywords",
            "entry_type": "observation",
            "domain": None,
        }
        cat, ttl = classify_entry(entry)
        assert cat == DEFAULT_CATEGORY
        assert ttl == DEFAULT_TTL

    def test_manual_override(self):
        entry = {
            "title": "Random entry about servers",
            "content": "Something about port numbers and services",
            "entry_type": "expertise",
            "domain": None,
        }
        # Without override: matches infrastructure (port, service keywords)
        cat_auto, _ = classify_entry(entry)
        assert cat_auto == "infrastructure"

        # With override: respects manual category
        cat, ttl = classify_entry(entry, manual_override="domain_expertise")
        assert cat == "domain_expertise"
        assert ttl == 90

    def test_invalid_manual_override_falls_through(self):
        entry = {
            "title": "Server entry",
            "content": "Port 8000 service",
            "entry_type": "reference",
            "domain": None,
        }
        cat, ttl = classify_entry(entry, manual_override="nonexistent_category")
        # Invalid override ignored, auto-classification used
        assert cat == "infrastructure"
        assert ttl == 7

    def test_multi_category_shortest_ttl_wins(self):
        """Entry matching both infrastructure and configuration keywords."""
        entry = {
            "title": "Service configuration settings",
            "content": "The service on port 8000 has rate limit config set to 500",
            "entry_type": "reference",
            "domain": None,
        }
        cat, ttl = classify_entry(entry)
        # infrastructure (7d) and configuration (14d) both match
        # Shortest TTL wins
        assert ttl == 7
        assert cat == "infrastructure"

    def test_title_only_classification(self):
        """Entries with empty content should classify based on title."""
        entry = {
            "title": "Upgrade procedure runbook for the app",
            "content": "",
            "entry_type": "reference",
            "domain": None,
        }
        cat, ttl = classify_entry(entry)
        assert cat == "process"
        assert ttl == 30

    def test_classify_batch(self):
        entries = [
            {"id": "1", "title": "Port 8000", "content": "Service listens", "entry_type": "reference", "domain": None},
            {"id": "2", "title": "FIPS standard", "content": "NIST algorithm", "entry_type": "expertise", "domain": None},
        ]
        results = classify_batch(entries)
        assert len(results) == 2
        assert results[0]["knowledge_category"] == "infrastructure"
        assert results[0]["ttl_days"] == 7
        assert results[1]["knowledge_category"] == "domain_expertise"
        assert results[1]["ttl_days"] == 90

    def test_get_ttl_for_category(self):
        assert get_ttl_for_category("infrastructure") == 7
        assert get_ttl_for_category("architecture") == 14
        assert get_ttl_for_category("configuration") == 14
        assert get_ttl_for_category("correction") == 30
        assert get_ttl_for_category("process") == 30
        assert get_ttl_for_category("pattern") == 60
        assert get_ttl_for_category("domain_expertise") == 90
        assert get_ttl_for_category("nonexistent") == DEFAULT_TTL

    def test_all_categories_in_map(self):
        """Every category in CATEGORIES has a map entry."""
        for spec in CATEGORIES:
            assert spec.name in CATEGORY_MAP
            assert CATEGORY_MAP[spec.name] is spec

    def test_valid_categories_complete(self):
        assert VALID_CATEGORIES == frozenset(c.name for c in CATEGORIES)


# --- KnowledgeStore integration tests ---


class TestKnowledgeStoreFreshness:
    """Test KnowledgeStore category assignment and TTL checking."""

    def test_create_auto_classifies(self, knowledge):
        """New entries get auto-classified with category and TTL."""
        entry = knowledge.create(
            entry_type="reference",
            title="Coordinator runs on port 8000",
            content="Service listens on port 8000 at the Mac Mini",
            domain="infrastructure",
        )
        assert entry.get("knowledge_category") == "infrastructure"
        assert entry.get("ttl_days") == 7

    def test_create_with_manual_category(self, knowledge):
        """Manual category override is respected."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Some general knowledge",
            content="This could be anything",
            category="configuration",
        )
        assert entry.get("knowledge_category") == "configuration"
        assert entry.get("ttl_days") == 14

    def test_create_domain_expertise_default(self, knowledge):
        """Entries with no matching signals default to domain_expertise."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Abstract algebra fundamentals",
            content="Group theory and ring theory basics",
        )
        assert entry.get("knowledge_category") == "domain_expertise"
        assert entry.get("ttl_days") == 90

    def test_create_correction_type(self, knowledge):
        """Correction entry type maps to correction category."""
        entry = knowledge.create(
            entry_type="correction",
            title="Fixed the sync pagination bug",
            content="Used id-based cursor instead of sync_version",
            domain="database",
        )
        assert entry.get("knowledge_category") == "correction"
        assert entry.get("ttl_days") == 30

    def test_check_ttl_flags_expired(self, knowledge, db):
        """Entries past their TTL get flagged as stale."""
        entry = knowledge.create(
            entry_type="reference",
            title="Mac Mini IP address",
            content="The service runs at 100.81.247.14",
            domain="infrastructure",
        )
        assert entry.get("ttl_days") == 7

        # Backdate created_at to 8 days ago
        eight_days_ago = (datetime.now(timezone.utc) - timedelta(days=8)).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        db.execute(
            "UPDATE knowledge_entries SET created_at = ?, last_confirmed_at = NULL WHERE id = ?",
            (eight_days_ago, entry["id"]),
        )

        flagged = knowledge.check_ttl()
        assert len(flagged) == 1
        assert flagged[0]["id"] == entry["id"]

        # Verify it's now marked stale
        updated = knowledge.get(entry["id"])
        assert updated["stale_flagged_at"] is not None

    def test_check_ttl_skips_recent(self, knowledge):
        """Entries within their TTL are not flagged."""
        entry = knowledge.create(
            entry_type="expertise",
            title="FIPS 140-3 requirements",
            content="NIST approved algorithms only",
        )
        # Just created — well within 90-day TTL
        flagged = knowledge.check_ttl()
        assert len(flagged) == 0

    def test_check_ttl_skips_already_stale(self, knowledge, db):
        """Already-stale entries are not re-flagged."""
        entry = knowledge.create(
            entry_type="reference",
            title="Service port info",
            content="Running on port 9000",
            domain="infrastructure",
        )
        # Backdate and manually flag stale
        old_date = (datetime.now(timezone.utc) - timedelta(days=15)).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        db.execute(
            "UPDATE knowledge_entries SET created_at = ?, stale_flagged_at = ?, last_confirmed_at = NULL WHERE id = ?",
            (old_date, old_date, entry["id"]),
        )
        flagged = knowledge.check_ttl()
        assert len(flagged) == 0  # Already stale, not re-flagged

    def test_confirm_resets_ttl(self, knowledge, db):
        """Confirming an entry resets its TTL clock."""
        entry = knowledge.create(
            entry_type="reference",
            title="Mac Mini port info",
            content="Service on port 8000",
            domain="infrastructure",
        )
        # Backdate to 8 days ago (past 7-day TTL)
        old_date = (datetime.now(timezone.utc) - timedelta(days=8)).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        db.execute(
            "UPDATE knowledge_entries SET created_at = ?, last_confirmed_at = NULL WHERE id = ?",
            (old_date, entry["id"]),
        )

        # Flag it stale
        flagged = knowledge.check_ttl()
        assert len(flagged) == 1

        # Confirm it — should reset TTL
        knowledge.confirm(entry["id"])
        updated = knowledge.get(entry["id"])
        assert updated["stale_flagged_at"] is None
        assert updated["last_confirmed_at"] is not None

        # Now check_ttl should find nothing
        flagged2 = knowledge.check_ttl()
        assert len(flagged2) == 0

    def test_infrastructure_vs_expertise_ttl(self, knowledge, db):
        """Infrastructure entries (7d) flag faster than expertise (90d)."""
        infra = knowledge.create(
            entry_type="reference",
            title="Node IP address info",
            content="Server at 10.0.0.1 on port 3000",
            domain="infrastructure",
        )
        expertise = knowledge.create(
            entry_type="expertise",
            title="RSA key length recommendations",
            content="NIST recommends 2048-bit RSA minimum",
        )
        assert infra.get("ttl_days") == 7
        assert expertise.get("ttl_days") == 90

        # Backdate both to 10 days ago
        ten_days_ago = (datetime.now(timezone.utc) - timedelta(days=10)).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        for eid in [infra["id"], expertise["id"]]:
            db.execute(
                "UPDATE knowledge_entries SET created_at = ?, last_confirmed_at = NULL WHERE id = ?",
                (ten_days_ago, eid),
            )

        flagged = knowledge.check_ttl()
        flagged_ids = {e["id"] for e in flagged}

        # Infrastructure (7d TTL) should be flagged, expertise (90d) should not
        assert infra["id"] in flagged_ids
        assert expertise["id"] not in flagged_ids

    def test_classify_all(self, knowledge, db):
        """classify_all re-classifies unclassified entries."""
        # Create entries without category (simulate pre-migration state)
        knowledge.create(
            entry_type="reference",
            title="Port 8000 service",
            content="Listening on port 8000",
            domain="infrastructure",
        )
        knowledge.create(
            entry_type="expertise",
            title="TLS handshake protocol",
            content="The TLS protocol specification requires certificate validation",
        )

        # Reset categories to default (simulating pre-migration)
        db.execute(
            "UPDATE knowledge_entries SET knowledge_category = 'domain_expertise', ttl_days = 90"
        )

        counts = knowledge.classify_all()
        assert "infrastructure" in counts
        assert counts["infrastructure"] >= 1


# --- Freshness report tests ---


class TestFreshnessReport:
    """Test the freshness report with GREEN/YELLOW/RED statuses."""

    def test_fresh_entry_is_green(self, knowledge):
        """Newly created entry should be GREEN."""
        knowledge.create(
            entry_type="expertise",
            title="Test entry",
            content="Some content",
        )
        report = knowledge.freshness_report()
        assert report["by_status"]["green"] == 1
        assert report["by_status"]["yellow"] == 0
        assert report["by_status"]["red"] == 0

    def test_expired_entry_is_red(self, knowledge, db):
        """Entry past its TTL should be RED."""
        entry = knowledge.create(
            entry_type="reference",
            title="Service port config",
            content="Port 8000 service info",
            domain="infrastructure",
        )
        # Backdate to 10 days ago (past 7-day TTL)
        old_date = (datetime.now(timezone.utc) - timedelta(days=10)).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        db.execute(
            "UPDATE knowledge_entries SET created_at = ?, last_confirmed_at = NULL WHERE id = ?",
            (old_date, entry["id"]),
        )
        report = knowledge.freshness_report()
        assert report["by_status"]["red"] == 1

    def test_nearing_expiry_is_yellow(self, knowledge, db):
        """Entry at >75% of TTL should be YELLOW."""
        entry = knowledge.create(
            entry_type="reference",
            title="Service port config",
            content="Port 8000 service info",
            domain="infrastructure",
        )
        # 7-day TTL, set to 6 days ago (>75% = 5.25 days, but <7)
        six_days_ago = (datetime.now(timezone.utc) - timedelta(days=6)).strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        db.execute(
            "UPDATE knowledge_entries SET created_at = ?, last_confirmed_at = NULL WHERE id = ?",
            (six_days_ago, entry["id"]),
        )
        report = knowledge.freshness_report()
        assert report["by_status"]["yellow"] == 1

    def test_report_groups_by_category(self, knowledge):
        """Report should group entries by knowledge_category."""
        knowledge.create(
            entry_type="reference",
            title="Port 8000 service",
            content="Service on port 8000",
            domain="infrastructure",
        )
        knowledge.create(
            entry_type="expertise",
            title="FIPS standard info",
            content="NIST algorithm requirements",
        )
        report = knowledge.freshness_report()
        assert "infrastructure" in report["by_category"]
        assert "domain_expertise" in report["by_category"]
        assert report["by_category"]["infrastructure"]["ttl_days"] == 7
        assert report["by_category"]["domain_expertise"]["ttl_days"] == 90

    def test_stale_flagged_entry_is_red(self, knowledge, db):
        """Entry with stale_flagged_at set should always be RED."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Some expertise",
            content="Content here",
        )
        db.execute(
            "UPDATE knowledge_entries SET stale_flagged_at = datetime('now') WHERE id = ?",
            (entry["id"],),
        )
        report = knowledge.freshness_report()
        assert report["by_status"]["red"] == 1

    def test_report_includes_entry_details(self, knowledge):
        """Report entries list should include status and metadata."""
        knowledge.create(
            entry_type="reference",
            title="Test entry detail",
            content="Port 8000 service detail",
            domain="infrastructure",
        )
        report = knowledge.freshness_report()
        assert len(report["entries"]) == 1
        entry = report["entries"][0]
        assert "status" in entry
        assert "knowledge_category" in entry
        assert "ttl_days" in entry
        assert entry["status"] == "GREEN"


# --- _compute_freshness_status unit tests ---


class TestComputeFreshnessStatus:
    """Unit tests for the freshness status computation."""

    def test_stale_flagged_is_red(self):
        now = datetime.now(timezone.utc).isoformat()
        assert _compute_freshness_status(now, 90, now) == "red"

    def test_no_timestamp_is_yellow(self):
        assert _compute_freshness_status(None, 90, None) == "yellow"

    def test_fresh_is_green(self):
        now = datetime.now(timezone.utc).isoformat()
        assert _compute_freshness_status(now, 90, None) == "green"

    def test_expired_is_red(self):
        old = (datetime.now(timezone.utc) - timedelta(days=100)).isoformat()
        assert _compute_freshness_status(old, 90, None) == "red"

    def test_nearing_expiry_is_yellow(self):
        # 75% of 90 = 67.5 days. Set to 70 days ago.
        old = (datetime.now(timezone.utc) - timedelta(days=70)).isoformat()
        assert _compute_freshness_status(old, 90, None) == "yellow"

    def test_within_threshold_is_green(self):
        # 50% of 90 = 45 days. Well within.
        old = (datetime.now(timezone.utc) - timedelta(days=45)).isoformat()
        assert _compute_freshness_status(old, 90, None) == "green"

    def test_infrastructure_7_day_boundary(self):
        """Test 7-day infrastructure TTL at boundary."""
        # 5 days ago — green (< 75% of 7 = 5.25)
        five = (datetime.now(timezone.utc) - timedelta(days=5)).isoformat()
        assert _compute_freshness_status(five, 7, None) == "green"

        # 6 days ago — yellow (>= 75% of 7 = 5.25, < 7)
        six = (datetime.now(timezone.utc) - timedelta(days=6)).isoformat()
        assert _compute_freshness_status(six, 7, None) == "yellow"

        # 8 days ago — red (>= 7)
        eight = (datetime.now(timezone.utc) - timedelta(days=8)).isoformat()
        assert _compute_freshness_status(eight, 7, None) == "red"

    def test_invalid_timestamp_is_yellow(self):
        assert _compute_freshness_status("not-a-date", 90, None) == "yellow"
