"""Tests for direct observation-to-knowledge promotion.

Tests the auto-consolidation path where individual observations with
confidence >0.8 and review_count >=3 are promoted directly to knowledge
entries without requiring cluster-based consolidation.
"""
import os
import sqlite3
import tempfile

import pytest

from greymatter_core.recall import ProactiveRecall


@pytest.fixture
def db_path(tmp_path):
    """Create a temporary database with both observations and knowledge_entries tables."""
    path = str(tmp_path / "test.db")
    conn = sqlite3.connect(path)
    conn.executescript("""
        CREATE TABLE IF NOT EXISTS observations (
            id TEXT PRIMARY KEY,
            session_id TEXT NOT NULL DEFAULT '',
            trigger TEXT NOT NULL DEFAULT 'manual',
            category TEXT NOT NULL DEFAULT 'insight',
            title TEXT NOT NULL DEFAULT '',
            content TEXT NOT NULL DEFAULT '',
            raw_context TEXT DEFAULT '',
            confidence REAL DEFAULT 0.5,
            stability_days REAL DEFAULT 45,
            last_confirmed_at TEXT,
            token_count INTEGER DEFAULT 0,
            context_usage_pct REAL DEFAULT 0.0,
            knowledge_id TEXT,
            pattern_id TEXT,
            promoted_at TEXT,
            review_count INTEGER DEFAULT 0,
            last_reviewed_at TEXT,
            review_source TEXT DEFAULT '',
            source_tool TEXT DEFAULT '',
            domains TEXT DEFAULT '',
            invalidated_by TEXT,
            pruned INTEGER DEFAULT 0,
            created_at TEXT DEFAULT (datetime('now')),
            updated_at TEXT DEFAULT (datetime('now'))
        );

        CREATE TABLE IF NOT EXISTS knowledge_entries (
            id TEXT PRIMARY KEY,
            entry_type TEXT NOT NULL,
            scope TEXT DEFAULT 'cross-cutting',
            domain TEXT,
            title TEXT NOT NULL,
            content TEXT NOT NULL,
            valence TEXT,
            confidence REAL DEFAULT 0.5,
            applied_count INTEGER DEFAULT 0,
            tags TEXT,
            created_at TEXT DEFAULT (datetime('now')),
            updated_at TEXT DEFAULT (datetime('now')),
            pruned_at TEXT
        );

        CREATE TABLE IF NOT EXISTS patterns (
            id TEXT PRIMARY KEY,
            type TEXT NOT NULL DEFAULT 'process',
            domain TEXT NOT NULL DEFAULT 'general',
            title TEXT NOT NULL DEFAULT '',
            observation TEXT NOT NULL DEFAULT '',
            confidence TEXT DEFAULT 'low',
            confirmations INTEGER DEFAULT 0,
            last_applied_at TEXT,
            created_at TEXT DEFAULT (datetime('now')),
            updated_at TEXT DEFAULT (datetime('now'))
        );
    """)
    conn.commit()
    conn.close()
    return path


@pytest.fixture
def recall(db_path):
    r = ProactiveRecall(db_path=db_path)
    yield r
    r.close()


def _insert_observation(
    db_path: str,
    obs_id: str = "obs-test1",
    title: str = "SQLite WAL mode improves concurrency",
    content: str = "WAL mode allows concurrent reads during writes in SQLite",
    category: str = "insight",
    confidence: float = 0.85,
    review_count: int = 3,
    domains: str = "database",
    session_id: str = "sess-1",
    pruned: int = 0,
    knowledge_id: str | None = None,
    pattern_id: str | None = None,
) -> None:
    """Insert a test observation directly into the database."""
    conn = sqlite3.connect(db_path)
    conn.execute(
        """INSERT INTO observations
           (id, session_id, trigger, category, title, content,
            confidence, review_count, domains, pruned, knowledge_id, pattern_id)
           VALUES (?, ?, 'manual', ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
        (obs_id, session_id, category, title, content,
         confidence, review_count, domains, pruned, knowledge_id, pattern_id),
    )
    conn.commit()
    conn.close()


class TestDirectPromotion:
    """Tests for _try_direct_promotion()."""

    def test_promotes_high_confidence_well_reviewed(self, recall, db_path):
        """Observation with confidence >0.8 and review_count >=3 should promote."""
        _insert_observation(db_path, confidence=0.85, review_count=3)
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is True

        # Verify knowledge entry was created
        row = conn.execute(
            "SELECT * FROM knowledge_entries WHERE id = 'kn-test1'"
        ).fetchone()
        assert row is not None
        entry = dict(row)
        assert entry["title"] == "SQLite WAL mode improves concurrency"
        assert entry["domain"] == "database"
        assert entry["entry_type"] == "observation"  # insight → observation

        # Verify observation was linked
        obs = dict(conn.execute(
            "SELECT knowledge_id, promoted_at FROM observations WHERE id = 'obs-test1'"
        ).fetchone())
        assert obs["knowledge_id"] == "kn-test1"
        assert obs["promoted_at"] is not None

    def test_rejects_low_confidence(self, recall, db_path):
        """Observation with confidence <=0.8 should not be directly promoted."""
        _insert_observation(db_path, confidence=0.75, review_count=5)
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is False

    def test_rejects_insufficient_reviews(self, recall, db_path):
        """Observation with review_count <3 should not be directly promoted."""
        _insert_observation(db_path, confidence=0.9, review_count=2)
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is False

    def test_skips_already_promoted(self, recall, db_path):
        """Already promoted observations should not be re-promoted."""
        _insert_observation(db_path, confidence=0.9, review_count=5, knowledge_id="kn-existing")
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is False

    def test_skips_pruned(self, recall, db_path):
        """Pruned observations should not be promoted."""
        _insert_observation(db_path, confidence=0.9, review_count=5, pruned=1)
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is False

    def test_quality_gate_short_title(self, recall, db_path):
        """Observations with titles shorter than 5 chars should be rejected."""
        _insert_observation(db_path, title="Hi", confidence=0.9, review_count=3)
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is False

    def test_quality_gate_generic_title(self, recall, db_path):
        """Observations with generic titles should be rejected."""
        _insert_observation(db_path, title="Let me check this thing", confidence=0.9, review_count=3)
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is False

    def test_quality_gate_empty_content(self, recall, db_path):
        """Observations with empty content should be rejected."""
        _insert_observation(db_path, content="", confidence=0.9, review_count=3)
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-test1")
        assert result is False

    def test_strips_category_prefix_from_title(self, recall, db_path):
        """Category prefix [insight] should be stripped from the title."""
        _insert_observation(
            db_path,
            title="[insight] SQLite WAL mode improves concurrency",
            confidence=0.85,
            review_count=3,
        )
        conn = recall._get_conn()
        recall._try_direct_promotion(conn, "obs-test1")
        row = conn.execute(
            "SELECT title FROM knowledge_entries WHERE id = 'kn-test1'"
        ).fetchone()
        assert row is not None
        assert dict(row)["title"] == "SQLite WAL mode improves concurrency"

    def test_category_maps_to_entry_type(self, recall, db_path):
        """Each observation category should map to the correct knowledge entry_type."""
        mappings = {
            "correction": "correction",
            "gotcha": "gotcha",
            "pattern": "pattern",
            "decision": "expertise",
            "reference": "reference",
            "insight": "observation",
        }
        for i, (category, expected_type) in enumerate(mappings.items()):
            obs_id = f"obs-cat{i}"
            _insert_observation(
                db_path,
                obs_id=obs_id,
                title=f"Test category mapping for {category}",
                category=category,
                confidence=0.9,
                review_count=3,
            )
            conn = recall._get_conn()
            recall._try_direct_promotion(conn, obs_id)
            row = conn.execute(
                f"SELECT entry_type FROM knowledge_entries WHERE id = 'kn-cat{i}'"
            ).fetchone()
            assert row is not None, f"No knowledge entry for category={category}"
            assert dict(row)["entry_type"] == expected_type, (
                f"category={category}: expected {expected_type}, got {dict(row)['entry_type']}"
            )

    def test_nonexistent_observation(self, recall, db_path):
        """Attempting to promote a nonexistent observation returns False."""
        conn = recall._get_conn()
        result = recall._try_direct_promotion(conn, "obs-nonexistent")
        assert result is False

    def test_uses_first_domain(self, recall, db_path):
        """Multi-domain observations should use the first domain."""
        _insert_observation(
            db_path,
            domains="python,database,testing",
            confidence=0.9,
            review_count=3,
        )
        conn = recall._get_conn()
        recall._try_direct_promotion(conn, "obs-test1")
        row = conn.execute(
            "SELECT domain FROM knowledge_entries WHERE id = 'kn-test1'"
        ).fetchone()
        assert dict(row)["domain"] == "python"

    def test_no_domains_defaults_to_general(self, recall, db_path):
        """Observations with no domains should default to 'general'."""
        _insert_observation(db_path, domains="", confidence=0.9, review_count=3)
        conn = recall._get_conn()
        recall._try_direct_promotion(conn, "obs-test1")
        row = conn.execute(
            "SELECT domain FROM knowledge_entries WHERE id = 'kn-test1'"
        ).fetchone()
        assert dict(row)["domain"] == "general"


class TestBatchPromoteHighConfidence:
    """Tests for batch_promote_high_confidence()."""

    def test_batch_promotes_eligible(self, recall, db_path):
        """Batch promotion should promote all eligible observations."""
        for i in range(5):
            _insert_observation(
                db_path,
                obs_id=f"obs-batch{i}",
                title=f"Observation number {i} about SQLite WAL mode",
                confidence=0.85 + i * 0.02,
                review_count=3 + i,
            )
        result = recall.batch_promote_high_confidence()
        assert result["promoted"] == 5
        assert result["skipped"] == 0
        assert result["scanned"] == 5

    def test_batch_skips_ineligible(self, recall, db_path):
        """Batch promotion should skip observations below thresholds."""
        # Eligible
        _insert_observation(
            db_path, obs_id="obs-good",
            title="Good observation about databases",
            confidence=0.9, review_count=4,
        )
        # Low confidence
        _insert_observation(
            db_path, obs_id="obs-lowconf",
            title="Low confidence observation about testing",
            confidence=0.6, review_count=5,
        )
        # Few reviews
        _insert_observation(
            db_path, obs_id="obs-fewrev",
            title="Few reviews observation about deployment",
            confidence=0.9, review_count=1,
        )
        result = recall.batch_promote_high_confidence()
        # Only obs-good is eligible (lowconf and fewrev don't match the query)
        assert result["promoted"] == 1

    def test_batch_respects_limit(self, recall, db_path):
        """Batch promotion should respect the limit parameter."""
        for i in range(10):
            _insert_observation(
                db_path,
                obs_id=f"obs-lim{i}",
                title=f"Observation {i} about batch limiting behavior",
                confidence=0.9,
                review_count=3,
            )
        result = recall.batch_promote_high_confidence(limit=3)
        assert result["scanned"] == 3
        assert result["promoted"] == 3

    def test_batch_skips_already_promoted(self, recall, db_path):
        """Already promoted observations should not appear in batch scan."""
        _insert_observation(
            db_path, obs_id="obs-promoted",
            title="Already promoted observation about caching",
            confidence=0.9, review_count=5,
            knowledge_id="kn-existing",
        )
        result = recall.batch_promote_high_confidence()
        assert result["scanned"] == 0

    def test_batch_empty_db(self, recall):
        """Batch on empty database should return zero counts."""
        result = recall.batch_promote_high_confidence()
        assert result == {"promoted": 0, "skipped": 0, "scanned": 0}


class TestReviewTriggersDirect:
    """Tests that record_review triggers direct promotion at the right threshold."""

    def test_review_triggers_direct_at_threshold(self, recall, db_path):
        """Review that pushes confidence >0.8 with review_count >=3 should trigger direct promotion."""
        # Insert observation just below threshold: confidence=0.76, review_count=2
        # After review: confidence=0.81, review_count=3 → triggers direct promotion
        _insert_observation(
            db_path,
            confidence=0.76,
            review_count=2,
            title="Important pattern about error handling",
            content="Always handle httpx.HTTPError at API boundaries",
            category="pattern",
        )
        result = recall.record_review("obs-test1", source_table="observations")
        assert result is True

        # Check if promoted
        conn = recall._get_conn()
        row = conn.execute(
            "SELECT knowledge_id FROM observations WHERE id = 'obs-test1'"
        ).fetchone()
        assert row is not None
        assert dict(row)["knowledge_id"] is not None

    def test_review_does_not_trigger_below_threshold(self, recall, db_path):
        """Review that stays at confidence <=0.8 should not trigger direct promotion."""
        # After review: confidence=0.55, review_count=1 → no trigger
        _insert_observation(
            db_path,
            confidence=0.5,
            review_count=0,
            title="Tentative observation about caching strategies",
            content="Redis might be useful for caching",
        )
        recall.record_review("obs-test1", source_table="observations")

        conn = recall._get_conn()
        row = conn.execute(
            "SELECT knowledge_id FROM observations WHERE id = 'obs-test1'"
        ).fetchone()
        assert dict(row)["knowledge_id"] is None
