"""Tests for knowledge freshness validation — field writes, filtering, and session hook wiring.

Covers:
1. Validation writes freshness fields (last_validated_at, validation_status, etc.)
2. Valid entries get confirmed, invalid entries get flagged stale
3. Stale entries excluded from context injection
4. Search results show [STALE] prefix
5. Weekly hook fires only once per 7 days
6. Edge cases: no paths, entries on remote machines, timeout safety
"""
import os
import sqlite3
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch

import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.knowledge import KnowledgeStore
from greymatter_core.validate import (
    KnowledgeValidator,
    ValidationReport,
    _extract_paths,
)


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


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


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


@pytest.fixture
def validator(db):
    return KnowledgeValidator(db)


def _insert_entry(
    db,
    *,
    entry_id: str | None = None,
    entry_type: str = "reference",
    title: str = "Test entry",
    content: str = "Test content",
    confidence: float = 0.8,
    created_at: str | None = None,
    updated_at: str | None = None,
    validation_status: str | None = None,
) -> str:
    """Insert a knowledge entry with explicit fields for testing."""
    eid = entry_id or f"kn-{uuid.uuid4().hex[:8]}"
    now = datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%d %H:%M:%S")
    db.execute(
        """INSERT INTO knowledge_entries
           (id, entry_type, title, content, confidence, created_at, updated_at, validation_status)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
        (eid, entry_type, title, content, confidence,
         created_at or now, updated_at or now, validation_status),
    )
    return eid


# ===========================================================================
# 1. Freshness field writes during validation
# ===========================================================================


class TestFreshnessFieldWrites:
    """Verify that validation writes last_validated_at, validation_status, etc."""

    def test_valid_path_sets_confirmed(self, db, knowledge, validator):
        """Entry referencing /tmp (always exists) should get validation_status='valid'."""
        entry = knowledge.create(
            entry_type="reference",
            title="Temp dir",
            content="The temp directory is /tmp for scratch files",
        )
        validator.validate_paths()

        row = db.execute_one(
            "SELECT validation_status, last_validated_at, last_confirmed_at "
            "FROM knowledge_entries WHERE id = ?",
            (entry["id"],),
        )
        assert row["validation_status"] == "valid"
        assert row["last_validated_at"] is not None
        assert row["last_confirmed_at"] is not None

    def test_invalid_path_sets_stale(self, db, knowledge, validator):
        """Entry referencing nonexistent path should get validation_status='stale'."""
        entry = knowledge.create(
            entry_type="reference",
            title="Missing config",
            content="Config at /nonexistent/greymatter/config.yaml",
        )
        validator.validate_paths()

        row = db.execute_one(
            "SELECT validation_status, last_validated_at, stale_flagged_at "
            "FROM knowledge_entries WHERE id = ?",
            (entry["id"],),
        )
        assert row["validation_status"] == "stale"
        assert row["last_validated_at"] is not None
        assert row["stale_flagged_at"] is not None

    def test_no_paths_entry_not_touched(self, db, knowledge, validator):
        """Entries without file paths should NOT have freshness fields set."""
        entry = knowledge.create(
            entry_type="expertise",
            title="SQLite tips",
            content="WAL mode is good for concurrent reads",
        )
        validator.validate_paths()

        row = db.execute_one(
            "SELECT validation_status, last_validated_at "
            "FROM knowledge_entries WHERE id = ?",
            (entry["id"],),
        )
        # No paths extracted → validator skips → fields remain NULL
        assert row["validation_status"] is None
        assert row["last_validated_at"] is None

    def test_stale_validator_sets_stale_status(self, db, validator):
        """validate_stale() should write validation_status='stale' on flagged entries."""
        eid = _insert_entry(
            db,
            entry_id="kn-old-low",
            confidence=0.3,
            created_at="2024-01-01 00:00:00",
            updated_at="2024-01-01 00:00:00",
        )
        validator.validate_stale()

        row = db.execute_one(
            "SELECT validation_status, stale_flagged_at, last_validated_at "
            "FROM knowledge_entries WHERE id = ?",
            (eid,),
        )
        assert row["validation_status"] == "stale"
        assert row["stale_flagged_at"] is not None
        assert row["last_validated_at"] is not None

    def test_stale_overrides_valid(self, db, knowledge, validator):
        """If path validator marks 'valid' but stale validator flags it, 'stale' should win."""
        # Entry with a valid path AND low confidence + old date
        db.execute(
            """INSERT INTO knowledge_entries
               (id, entry_type, title, content, confidence, created_at, updated_at)
               VALUES (?, ?, ?, ?, ?, ?, ?)""",
            ("kn-overlap", "reference", "Old temp ref",
             "Check /tmp for files",
             0.2, "2024-01-01 00:00:00", "2024-01-01 00:00:00"),
        )

        # validate_all runs: paths → versions → stale
        # paths marks it valid (path exists), stale marks it stale (old + low conf)
        report = validator.validate_all()

        row = db.execute_one(
            "SELECT validation_status FROM knowledge_entries WHERE id = ?",
            ("kn-overlap",),
        )
        # stale runs after paths, so stale status wins
        assert row["validation_status"] == "stale"

    def test_validate_all_populates_last_validated_at(self, db, knowledge, validator):
        """After validate_all(), entries that were checked should have last_validated_at set."""
        knowledge.create(
            entry_type="reference",
            title="Existing path",
            content="File at /tmp always exists",
        )
        knowledge.create(
            entry_type="reference",
            title="Bad path",
            content="File at /nonexistent/data/file.txt is gone",
        )

        validator.validate_all()

        rows = db.execute(
            "SELECT id, last_validated_at FROM knowledge_entries "
            "WHERE last_validated_at IS NOT NULL"
        )
        assert len(rows) >= 2  # Both entries were checked

    def test_mark_valid_does_not_downgrade_stale(self, db, validator):
        """_mark_valid should not overwrite an existing 'stale' status."""
        eid = _insert_entry(db, entry_id="kn-already-stale", validation_status="stale")

        validator._mark_valid(eid)

        row = db.execute_one(
            "SELECT validation_status FROM knowledge_entries WHERE id = ?", (eid,)
        )
        assert row["validation_status"] == "stale"


# ===========================================================================
# 2. Search output with freshness fields
# ===========================================================================


class TestSearchFreshness:
    """Verify search/list output includes freshness fields and [STALE] prefix."""

    def test_search_summary_includes_validation_fields(self, db, knowledge, validator):
        """search_summary should return validation_status and last_validated_at."""
        entry = knowledge.create(
            entry_type="reference",
            title="Valid path entry",
            content="Found at /tmp for testing",
        )
        validator.validate_paths()

        result = knowledge.search_summary(text="Valid path")
        assert result["returned"] >= 1
        found = result["entries"][0]
        assert "validation_status" in found
        assert "last_validated_at" in found
        assert found["validation_status"] == "valid"

    def test_stale_entry_gets_prefix_in_search(self, db, knowledge, validator):
        """Stale entries should have [STALE] prefix in title when searched."""
        entry = knowledge.create(
            entry_type="reference",
            title="Old config",
            content="Config at /nonexistent/app/config.yaml",
        )
        validator.validate_paths()

        result = knowledge.search_summary(text="Old config")
        assert result["returned"] >= 1
        found = result["entries"][0]
        assert found["title"].startswith("[STALE]")

    def test_stale_entry_gets_prefix_in_full_search(self, db, knowledge, validator):
        """Full search results should also have [STALE] prefix."""
        entry = knowledge.create(
            entry_type="reference",
            title="Missing ref",
            content="At /nonexistent/data/report.csv",
        )
        validator.validate_paths()

        results = knowledge.search(text="Missing ref")
        assert len(results) >= 1
        assert results[0]["title"].startswith("[STALE]")

    def test_valid_entry_no_prefix(self, db, knowledge, validator):
        """Valid entries should NOT have [STALE] prefix."""
        entry = knowledge.create(
            entry_type="reference",
            title="Good entry",
            content="Temp files in /tmp are fine",
        )
        validator.validate_paths()

        results = knowledge.search(text="Good entry")
        assert len(results) >= 1
        assert not results[0]["title"].startswith("[STALE]")

    def test_annotate_freshness_idempotent(self):
        """Calling _annotate_freshness twice should not double-prefix."""
        entries = [{"title": "Test", "validation_status": "stale"}]
        KnowledgeStore._annotate_freshness(entries)
        KnowledgeStore._annotate_freshness(entries)
        assert entries[0]["title"] == "[STALE] Test"
        assert not entries[0]["title"].startswith("[STALE] [STALE]")


# ===========================================================================
# 3. Context injection filtering
# ===========================================================================


class TestContextInjectionFiltering:
    """Verify stale/invalid entries are excluded from session context."""

    def test_freshness_filter_sql(self):
        """_freshness_filter should produce valid SQL that excludes stale entries."""
        # Import from session_loader
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "session_loader",
            os.path.expanduser("~/Development/greymatter/plugin/hooks/session_loader.py"),
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)

        filt = mod._freshness_filter()
        assert "validation_status" in filt
        assert "stale" in filt
        assert "invalid" in filt
        assert "pruned" in filt

    def test_stale_entries_excluded_from_tiered_query(self, db):
        """Entries with validation_status='stale' should not appear in tiered queries."""
        conn = sqlite3.connect(":memory:")
        conn.execute("""CREATE TABLE knowledge_entries (
            id TEXT PRIMARY KEY, entry_type TEXT, title TEXT, content TEXT,
            domain TEXT, created_at TEXT, pruned_at TEXT,
            validation_status TEXT, last_validated_at TEXT
        )""")
        now = datetime.now().isoformat()

        # Insert a valid correction
        conn.execute(
            "INSERT INTO knowledge_entries VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
            ("kn-valid", "gotcha", "Valid gotcha", "This is correct",
             "gotcha", now, None, "valid", now),
        )
        # Insert a stale correction
        conn.execute(
            "INSERT INTO knowledge_entries VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
            ("kn-stale", "gotcha", "Stale gotcha", "This is outdated",
             "gotcha", now, None, "stale", now),
        )
        conn.commit()

        # Query with freshness filter
        freshness = (
            " AND pruned_at IS NULL"
            " AND (validation_status IS NULL OR validation_status NOT IN ('stale', 'invalid', 'pruned'))"
        )
        results = conn.execute(f"""
            SELECT title FROM knowledge_entries
            WHERE title LIKE '%gotcha%' {freshness}
        """).fetchall()

        titles = [r[0] for r in results]
        assert "Valid gotcha" in titles
        assert "Stale gotcha" not in titles
        conn.close()

    def test_null_validation_status_not_excluded(self, db):
        """Entries with NULL validation_status should still be included."""
        conn = sqlite3.connect(":memory:")
        conn.execute("""CREATE TABLE knowledge_entries (
            id TEXT PRIMARY KEY, title TEXT, content TEXT, domain TEXT,
            created_at TEXT, pruned_at TEXT, validation_status TEXT
        )""")
        now = datetime.now().isoformat()
        conn.execute(
            "INSERT INTO knowledge_entries VALUES (?, ?, ?, ?, ?, ?, ?)",
            ("kn-null", "Unvalidated entry", "Content", "test", now, None, None),
        )
        conn.commit()

        freshness = (
            " AND pruned_at IS NULL"
            " AND (validation_status IS NULL OR validation_status NOT IN ('stale', 'invalid', 'pruned'))"
        )
        results = conn.execute(f"SELECT title FROM knowledge_entries WHERE 1=1 {freshness}").fetchall()
        assert len(results) == 1
        assert results[0][0] == "Unvalidated entry"
        conn.close()


# ===========================================================================
# 4. Weekly hook behavior
# ===========================================================================


class TestWeeklyHook:
    """Verify weekly validation fires correctly and records the run."""

    def test_first_run_always_fires(self, validator):
        """First call to run_weekly_if_due should always run."""
        report = validator.run_weekly_if_due()
        assert report is not None
        assert isinstance(report, ValidationReport)

    def test_second_run_blocked(self, validator):
        """Immediately after running, should_run_weekly returns False."""
        validator.run_weekly_if_due()
        assert validator.should_run_weekly() is False
        report = validator.run_weekly_if_due()
        assert report is None

    def test_record_run_creates_metadata(self, db, validator):
        """record_run should write to _schema_metadata table."""
        validator.record_run()
        row = db.execute_one(
            "SELECT value FROM _schema_metadata WHERE key = ?",
            (validator.META_KEY,),
        )
        assert row is not None
        # Value should be a parseable ISO datetime
        dt = datetime.fromisoformat(row["value"])
        assert dt.year >= 2026

    def test_weekly_validation_writes_freshness(self, db, knowledge, validator):
        """run_weekly_if_due should trigger freshness field writes."""
        knowledge.create(
            entry_type="reference",
            title="Weekly check",
            content="File at /tmp is checked weekly",
        )
        report = validator.run_weekly_if_due()
        assert report is not None

        row = db.execute_one(
            "SELECT validation_status, last_validated_at "
            "FROM knowledge_entries WHERE title = 'Weekly check'"
        )
        assert row["validation_status"] == "valid"
        assert row["last_validated_at"] is not None


# ===========================================================================
# 5. Validate action in MCP tool (unit-testable parts)
# ===========================================================================


class TestValidateAllReport:
    """Verify validate_all returns a complete report with freshness writes."""

    def test_report_includes_issue_counts(self, db, knowledge, validator):
        """validate_all report should have valid, invalid, stale, and issues."""
        knowledge.create(
            entry_type="reference",
            title="Good path",
            content="Valid at /tmp",
        )
        knowledge.create(
            entry_type="reference",
            title="Bad path",
            content="Missing at /nonexistent/data.txt",
        )
        db.execute(
            """INSERT INTO knowledge_entries
               (id, entry_type, title, content, confidence, created_at, updated_at)
               VALUES (?, ?, ?, ?, ?, ?, ?)""",
            ("kn-stale-rpt", "observation", "Old fact", "Fading",
             0.2, "2023-01-01 00:00:00", "2023-01-01 00:00:00"),
        )

        report = validator.validate_all()
        d = report.to_dict()

        assert d["valid"] >= 1
        assert d["invalid"] >= 1
        assert d["stale"] >= 1
        assert d["total_checked"] == d["valid"] + d["invalid"] + d["stale"]
        assert len(d["issues"]) >= 2

    def test_validate_all_empty_db(self, validator):
        """validate_all on empty DB should return clean report."""
        report = validator.validate_all()
        assert report.valid == 0
        assert report.invalid == 0
        assert report.stale == 0


# ===========================================================================
# 6. Edge cases
# ===========================================================================


class TestEdgeCases:
    """Edge cases from the work item specification."""

    def test_entries_without_paths_not_marked_stale(self, db, knowledge, validator):
        """Entries with no file paths should not be marked stale just for lacking paths."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Pure knowledge",
            content="No file paths here, just wisdom about SQLite WAL mode",
        )
        validator.validate_paths()

        row = db.execute_one(
            "SELECT validation_status FROM knowledge_entries WHERE id = ?",
            (entry["id"],),
        )
        assert row["validation_status"] is None  # Untouched

    def test_home_directory_expansion(self, db, knowledge, validator):
        """~/Development should expand to actual home directory."""
        home = os.path.expanduser("~")
        entry = knowledge.create(
            entry_type="reference",
            title="Home path",
            content=f"Project at {home}",
        )
        # /Users/username always exists
        validator.validate_paths()

        row = db.execute_one(
            "SELECT validation_status FROM knowledge_entries WHERE id = ?",
            (entry["id"],),
        )
        assert row["validation_status"] == "valid"

    def test_validation_status_column_exists(self, db):
        """The migration should create validation_status and last_validated_at columns."""
        row = db.execute_one(
            "SELECT validation_status, last_validated_at FROM knowledge_entries LIMIT 1"
        )
        # No entries, but the query should not error (columns exist)

    def test_multiple_entries_validated_efficiently(self, db, validator):
        """Validate 100 entries to verify it doesn't take excessive time."""
        import time

        for i in range(100):
            _insert_entry(
                db,
                entry_id=f"kn-perf-{i:03d}",
                title=f"Entry {i}",
                content=f"Content for entry {i} without paths",
                confidence=0.8,
            )

        t0 = time.monotonic()
        report = validator.validate_all()
        elapsed = time.monotonic() - t0

        # Should complete well within 10s timeout
        assert elapsed < 5.0

    def test_validation_on_already_valid_entry(self, db, knowledge, validator):
        """Re-validating a valid entry should keep it valid."""
        entry = knowledge.create(
            entry_type="reference",
            title="Stable ref",
            content="Always at /tmp",
        )
        validator.validate_paths()
        validator.validate_paths()  # Second run

        row = db.execute_one(
            "SELECT validation_status FROM knowledge_entries WHERE id = ?",
            (entry["id"],),
        )
        assert row["validation_status"] == "valid"

    def test_pruned_entries_not_validated(self, db, validator):
        """Pruned entries should be excluded from validation entirely."""
        db.execute(
            """INSERT INTO knowledge_entries
               (id, entry_type, title, content, pruned_at)
               VALUES (?, ?, ?, ?, ?)""",
            ("kn-pruned", "reference", "Pruned entry",
             "At /nonexistent/pruned.txt", "2025-01-01 00:00:00"),
        )
        validator.validate_paths()

        row = db.execute_one(
            "SELECT validation_status FROM knowledge_entries WHERE id = ?",
            ("kn-pruned",),
        )
        assert row["validation_status"] is None  # Not touched


# ===========================================================================
# 7. Schema migration
# ===========================================================================


class TestSchemaMigration:
    """Verify the new columns are properly migrated."""

    def test_fresh_db_has_validation_columns(self):
        """A fresh SQLiteDatabase should have both new columns."""
        db = SQLiteDatabase(":memory:")
        # Verify columns exist by inserting and reading
        db.execute(
            """INSERT INTO knowledge_entries
               (id, entry_type, title, content, validation_status, last_validated_at)
               VALUES (?, ?, ?, ?, ?, ?)""",
            ("kn-schema", "reference", "Schema test", "Content",
             "valid", "2026-04-01 00:00:00"),
        )
        row = db.execute_one("SELECT validation_status, last_validated_at FROM knowledge_entries WHERE id = 'kn-schema'")
        assert row["validation_status"] == "valid"
        assert row["last_validated_at"] == "2026-04-01 00:00:00"
