"""Tests for knowledge validation wiring and freshness field activation.

Covers acceptance criteria:
1. gm_knowledge(action="validate") runs all three validators and returns a report
2. After validation, entries have last_validated_at populated (not NULL)
3. Invalid entries have validation_status='stale' and stale_flagged_at set
4. Valid entries have validation_status='valid' and last_confirmed_at set
5. SessionStart hook triggers weekly validation (once per 7 days)
6. Stale/invalid entries are excluded from context injection to agents
7. Search results show [STALE] prefix for flagged entries
8. Validation completes within 10s (hook timeout constraint)
9. confirm/unflag/apply clear validation_status so entries re-enter context
10. search() excludes invalid entries by default, include_invalid overrides
"""
import time
import pytest

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


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


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


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


# ---------------------------------------------------------------------------
# AC1 + AC2: Validator writes freshness fields on validation
# ---------------------------------------------------------------------------


class TestValidatorWritesFreshnessFields:
    """Verify _mark_valid and _mark_stale populate freshness columns."""

    def test_valid_entry_gets_last_validated_at(self, knowledge, validator):
        """After validating a path-valid entry, last_validated_at is populated."""
        entry = knowledge.create(
            entry_type="reference",
            title="Temp directory",
            content="The temp directory is /tmp for scratch files",
        )
        validator.validate_paths()
        updated = knowledge.get(entry["id"])
        assert updated["last_validated_at"] is not None

    def test_valid_entry_gets_validation_status(self, knowledge, validator):
        """Valid entries get validation_status='valid'."""
        entry = knowledge.create(
            entry_type="reference",
            title="Temp directory ref",
            content="Scratch space at /tmp is always available",
        )
        validator.validate_paths()
        updated = knowledge.get(entry["id"])
        assert updated["validation_status"] == "valid"

    def test_valid_entry_gets_last_confirmed_at(self, knowledge, validator):
        """Valid entries get last_confirmed_at set."""
        entry = knowledge.create(
            entry_type="reference",
            title="Temp scratch dir",
            content="Use /tmp for temporary files",
        )
        validator.validate_paths()
        updated = knowledge.get(entry["id"])
        assert updated["last_confirmed_at"] is not None


# ---------------------------------------------------------------------------
# AC3: Invalid entries get stale status
# ---------------------------------------------------------------------------


class TestInvalidEntriesMarkedStale:
    """Verify that entries referencing missing paths are flagged stale."""

    def test_missing_path_sets_validation_status_stale(self, knowledge, validator):
        """Entry with nonexistent path gets validation_status='stale'."""
        entry = knowledge.create(
            entry_type="reference",
            title="Missing config ref",
            content="Config at /nonexistent/greymatter/config.yaml",
        )
        validator.validate_paths()
        updated = knowledge.get(entry["id"])
        assert updated["validation_status"] == "stale"

    def test_missing_path_sets_stale_flagged_at(self, knowledge, validator):
        """Entry with nonexistent path gets stale_flagged_at set."""
        entry = knowledge.create(
            entry_type="reference",
            title="Missing file ref",
            content="Data at /nonexistent/data/file.txt",
        )
        validator.validate_paths()
        updated = knowledge.get(entry["id"])
        assert updated["stale_flagged_at"] is not None

    def test_missing_path_sets_last_validated_at(self, knowledge, validator):
        """Even invalid entries get last_validated_at populated."""
        entry = knowledge.create(
            entry_type="reference",
            title="Bad path ref",
            content="File at /nonexistent/path/file.py",
        )
        validator.validate_paths()
        updated = knowledge.get(entry["id"])
        assert updated["last_validated_at"] is not None

    def test_stale_overrides_valid_status(self, knowledge, validator):
        """If both path valid and staleness check fail, stale wins."""
        entry = knowledge.create(
            entry_type="reference",
            title="Old valid-path entry",
            content="Config at /tmp is fine",
        )
        # Set low confidence and old date to trigger staleness check
        knowledge.db.execute(
            """UPDATE knowledge_entries
               SET confidence = 0.3,
                   created_at = '2024-01-01 00:00:00',
                   updated_at = '2024-01-01 00:00:00'
               WHERE id = ?""",
            (entry["id"],),
        )
        report = validator.validate_all()
        updated = knowledge.get(entry["id"])
        # Stale wins: validate_paths marks valid, validate_stale overrides to stale
        assert updated["validation_status"] == "stale"
        assert updated["stale_flagged_at"] is not None


# ---------------------------------------------------------------------------
# AC4: Valid entries get confirmed status
# ---------------------------------------------------------------------------


class TestValidEntriesConfirmed:
    """Verify valid entries get proper confirmation."""

    def test_validate_all_report_format(self, knowledge, validator):
        """validate_all returns a well-formed report."""
        knowledge.create(
            entry_type="reference",
            title="Valid entry",
            content="Scratch at /tmp works fine",
        )
        knowledge.create(
            entry_type="reference",
            title="Invalid entry",
            content="Missing at /nonexistent/missing/file.txt",
        )
        report = validator.validate_all()
        d = report.to_dict()
        assert "valid" in d
        assert "invalid" in d
        assert "stale" in d
        assert "total_checked" in d
        assert "issues" in d
        assert d["total_checked"] == d["valid"] + d["invalid"] + d["stale"]


# ---------------------------------------------------------------------------
# AC5: Weekly hook fires only once per 7 days
# ---------------------------------------------------------------------------


class TestWeeklyHookConstraint:
    """Verify weekly validation fires once per 7 days."""

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

    def test_second_run_returns_none(self, validator):
        """Second call within 7 days returns None."""
        validator.run_weekly_if_due()
        report2 = validator.run_weekly_if_due()
        assert report2 is None

    def test_record_run_prevents_rerun(self, validator):
        """After record_run, should_run_weekly is False."""
        validator.record_run()
        assert validator.should_run_weekly() is False


# ---------------------------------------------------------------------------
# AC6: Stale/invalid entries excluded from context injection
# ---------------------------------------------------------------------------


class TestContextInjectionFiltering:
    """Verify _freshness_filter() SQL excludes stale/invalid entries."""

    def test_freshness_filter_sql(self):
        """_freshness_filter returns correct SQL fragment."""
        # Import from session_loader
        import sys
        import os
        hook_dir = os.path.expanduser(
            "~/Development/greymatter/plugin/hooks"
        )
        if hook_dir not in sys.path:
            sys.path.insert(0, hook_dir)
        from session_loader import _freshness_filter
        sql = _freshness_filter()
        assert "pruned_at IS NULL" in sql
        assert "validation_status" in sql
        assert "'stale'" in sql
        assert "'invalid'" in sql
        assert "'pruned'" in sql

    def test_stale_entries_excluded_by_filter(self, db):
        """Entries with validation_status='stale' are excluded by the filter."""
        knowledge = KnowledgeStore(db)
        entry = knowledge.create(
            entry_type="gotcha",
            title="Stale gotcha",
            content="This gotcha is outdated",
            domain="database",
        )
        # Mark stale via validator
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale' WHERE id = ?",
            (entry["id"],),
        )
        # Query with freshness filter (same SQL as session_loader)
        results = db.execute(
            """SELECT * FROM knowledge_entries
               WHERE 1=1
                 AND pruned_at IS NULL
                 AND (validation_status IS NULL
                      OR validation_status NOT IN ('stale', 'invalid', 'pruned'))"""
        )
        result_ids = {r["id"] for r in results}
        assert entry["id"] not in result_ids

    def test_valid_entries_pass_filter(self, db):
        """Entries with validation_status='valid' pass the filter."""
        knowledge = KnowledgeStore(db)
        entry = knowledge.create(
            entry_type="gotcha",
            title="Valid gotcha",
            content="This gotcha is current",
            domain="database",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'valid' WHERE id = ?",
            (entry["id"],),
        )
        results = db.execute(
            """SELECT * FROM knowledge_entries
               WHERE 1=1
                 AND pruned_at IS NULL
                 AND (validation_status IS NULL
                      OR validation_status NOT IN ('stale', 'invalid', 'pruned'))"""
        )
        result_ids = {r["id"] for r in results}
        assert entry["id"] in result_ids

    def test_null_status_passes_filter(self, db):
        """Entries with NULL validation_status pass the filter."""
        knowledge = KnowledgeStore(db)
        entry = knowledge.create(
            entry_type="expertise",
            title="New entry",
            content="Unvalidated entry",
        )
        results = db.execute(
            """SELECT * FROM knowledge_entries
               WHERE 1=1
                 AND pruned_at IS NULL
                 AND (validation_status IS NULL
                      OR validation_status NOT IN ('stale', 'invalid', 'pruned'))"""
        )
        result_ids = {r["id"] for r in results}
        assert entry["id"] in result_ids


# ---------------------------------------------------------------------------
# AC7: Search results show [STALE] prefix for flagged entries
# ---------------------------------------------------------------------------


class TestSearchStalePrefixing:
    """Verify [STALE] prefix appears in search output."""

    def test_stale_entry_gets_stale_prefix(self, knowledge, db):
        """Entries with validation_status='stale' show [STALE] in title."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Outdated fact",
            content="This is old info",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale' WHERE id = ?",
            (entry["id"],),
        )
        results = knowledge.search()
        assert len(results) == 1
        assert results[0]["title"].startswith("[STALE]")

    def test_valid_entry_no_stale_prefix(self, knowledge, db):
        """Entries with validation_status='valid' have no [STALE] prefix."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Current fact",
            content="This is up to date",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'valid' WHERE id = ?",
            (entry["id"],),
        )
        results = knowledge.search()
        assert len(results) == 1
        assert not results[0]["title"].startswith("[STALE]")

    def test_search_summary_shows_validation_status(self, knowledge, db):
        """search_summary output includes validation_status and last_validated_at."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Validated entry",
            content="Some content",
        )
        db.execute(
            """UPDATE knowledge_entries
               SET validation_status = 'valid',
                   last_validated_at = datetime('now')
               WHERE id = ?""",
            (entry["id"],),
        )
        summary = knowledge.search_summary()
        assert len(summary["entries"]) == 1
        assert "validation_status" in summary["entries"][0]
        assert summary["entries"][0]["validation_status"] == "valid"
        assert "last_validated_at" in summary["entries"][0]
        assert summary["entries"][0]["last_validated_at"] is not None

    def test_stale_prefix_not_doubled(self, knowledge, db):
        """Calling search twice doesn't double the [STALE] prefix."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Old fact",
            content="Outdated",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale' WHERE id = ?",
            (entry["id"],),
        )
        results1 = knowledge.search()
        # The prefix is added to the dict in memory, not persisted
        # A second search should also have a single [STALE] prefix
        results2 = knowledge.search()
        assert results2[0]["title"] == "[STALE] Old fact"
        assert not results2[0]["title"].startswith("[STALE] [STALE]")


# ---------------------------------------------------------------------------
# AC10: search() excludes invalid by default, include_invalid overrides
# ---------------------------------------------------------------------------


class TestSearchInvalidFiltering:
    """Verify include_invalid parameter controls invalid entry visibility."""

    def test_invalid_entries_excluded_by_default(self, knowledge, db):
        """Entries with validation_status='invalid' excluded from default search."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Invalid entry",
            content="This is invalid",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'invalid' WHERE id = ?",
            (entry["id"],),
        )
        results = knowledge.search()
        assert len(results) == 0

    def test_invalid_entries_included_with_flag(self, knowledge, db):
        """include_invalid=True returns invalid entries."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Invalid but requested",
            content="This is invalid but explicitly requested",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'invalid' WHERE id = ?",
            (entry["id"],),
        )
        results = knowledge.search(include_invalid=True)
        assert len(results) == 1
        assert results[0]["id"] == entry["id"]

    def test_stale_entries_shown_in_search(self, knowledge, db):
        """Entries with validation_status='stale' still appear in search."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Stale entry",
            content="This is stale but still visible",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale' WHERE id = ?",
            (entry["id"],),
        )
        results = knowledge.search()
        assert len(results) == 1

    def test_search_summary_excludes_invalid(self, knowledge, db):
        """search_summary excludes invalid entries by default."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Invalid summary entry",
            content="This is invalid",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'invalid' WHERE id = ?",
            (entry["id"],),
        )
        summary = knowledge.search_summary()
        assert summary["returned"] == 0

    def test_search_summary_includes_invalid_with_flag(self, knowledge, db):
        """search_summary with include_invalid=True returns invalid entries."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Invalid summary requested",
            content="Explicitly requested",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'invalid' WHERE id = ?",
            (entry["id"],),
        )
        summary = knowledge.search_summary(include_invalid=True)
        assert summary["returned"] == 1


# ---------------------------------------------------------------------------
# AC9: confirm/unflag/apply clear validation_status
# ---------------------------------------------------------------------------


class TestStatusClearingOnReset:
    """Verify that confirm, unflag_stale, and record_application clear
    validation_status so entries re-enter context injection."""

    def test_confirm_clears_validation_status(self, knowledge, db):
        """confirm() resets validation_status to NULL."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Stale then confirmed",
            content="Content",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale', "
            "stale_flagged_at = datetime('now') WHERE id = ?",
            (entry["id"],),
        )
        knowledge.confirm(entry["id"])
        updated = knowledge.get(entry["id"])
        assert updated["validation_status"] is None
        assert updated["stale_flagged_at"] is None
        assert updated["last_confirmed_at"] is not None

    def test_unflag_stale_clears_validation_status(self, knowledge, db):
        """unflag_stale() resets validation_status to NULL."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Stale then unflagged",
            content="Content",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale', "
            "stale_flagged_at = datetime('now') WHERE id = ?",
            (entry["id"],),
        )
        knowledge.unflag_stale(entry["id"])
        updated = knowledge.get(entry["id"])
        assert updated["validation_status"] is None
        assert updated["stale_flagged_at"] is None

    def test_record_application_clears_validation_status(self, knowledge, db):
        """record_application() resets validation_status to NULL."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Stale then applied",
            content="Content",
        )
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale', "
            "stale_flagged_at = datetime('now') WHERE id = ?",
            (entry["id"],),
        )
        knowledge.record_application(entry["id"])
        updated = knowledge.get(entry["id"])
        assert updated["validation_status"] is None
        assert updated["stale_flagged_at"] is None
        assert updated["applied_count"] >= 1

    def test_confirm_reenables_context_injection(self, knowledge, db):
        """After confirm, entry passes the freshness filter again."""
        entry = knowledge.create(
            entry_type="gotcha",
            title="Gotcha back in action",
            content="This gotcha was stale but is confirmed",
            domain="database",
        )
        # Mark stale
        db.execute(
            "UPDATE knowledge_entries SET validation_status = 'stale', "
            "stale_flagged_at = datetime('now') WHERE id = ?",
            (entry["id"],),
        )
        # Verify excluded
        excluded = db.execute(
            """SELECT * FROM knowledge_entries
               WHERE id = ?
                 AND pruned_at IS NULL
                 AND (validation_status IS NULL
                      OR validation_status NOT IN ('stale', 'invalid', 'pruned'))""",
            (entry["id"],),
        )
        assert len(excluded) == 0

        # Confirm it
        knowledge.confirm(entry["id"])

        # Now it passes the filter
        included = db.execute(
            """SELECT * FROM knowledge_entries
               WHERE id = ?
                 AND pruned_at IS NULL
                 AND (validation_status IS NULL
                      OR validation_status NOT IN ('stale', 'invalid', 'pruned'))""",
            (entry["id"],),
        )
        assert len(included) == 1


# ---------------------------------------------------------------------------
# AC8: Validation completes within 10s
# ---------------------------------------------------------------------------


class TestValidationPerformance:
    """Verify validation completes within hook timeout."""

    def test_validate_all_under_10_seconds(self, db, validator):
        """validate_all on 500 entries completes within 10 seconds."""
        knowledge = KnowledgeStore(db)
        # Create 500 entries (more than the 450 in production)
        for i in range(500):
            knowledge.create(
                entry_type="reference",
                title=f"Entry {i}",
                content=f"Content for entry {i} without any file paths",
            )
        start = time.monotonic()
        report = validator.validate_all()
        elapsed = time.monotonic() - start
        assert elapsed < 10.0, f"Validation took {elapsed:.1f}s, exceeds 10s limit"
        # Should have run successfully
        assert isinstance(report, ValidationReport)


# ---------------------------------------------------------------------------
# Edge cases from the work item
# ---------------------------------------------------------------------------


class TestEdgeCases:
    """Edge cases specified in the work item."""

    def test_entry_without_paths_not_marked_stale(self, knowledge, validator):
        """Entries with no file paths skip path validation entirely."""
        entry = knowledge.create(
            entry_type="expertise",
            title="Pure knowledge",
            content="WAL mode allows concurrent reads during writes",
        )
        validator.validate_paths()
        updated = knowledge.get(entry["id"])
        # Should NOT be marked stale just because no paths found
        assert updated.get("validation_status") != "stale"

    def test_home_directory_expansion(self, knowledge, validator):
        """~ in paths expands to actual home directory."""
        from greymatter_core.validate import _extract_paths
        import os

        text = "Database at ~/.greymatter/greymatter.db"
        paths = _extract_paths(text)
        home = os.path.expanduser("~")
        assert any(home in p for p in paths)

    def test_validate_all_report_includes_issue_counts(self, knowledge, validator):
        """validate_all report to_dict includes valid + invalid + stale."""
        knowledge.create(
            entry_type="reference",
            title="Good path ref",
            content="Available at /tmp for writing",
        )
        knowledge.create(
            entry_type="reference",
            title="Bad path ref",
            content="Missing at /nonexistent/file.txt",
        )
        report = validator.validate_all()
        d = report.to_dict()
        assert d["valid"] >= 1
        assert d["invalid"] >= 1
        assert len(d["issues"]) >= 1
