"""Tests for three PULSE fixes: cross-session dedup, prediction fuzzy match,
contradiction confidence penalty.
"""
import json
import pytest
from pathlib import Path


# ---------------------------------------------------------------------------
# Test 1: Cross-session fingerprint dedup
# ---------------------------------------------------------------------------

class TestCrossSessionFingerprint:
    """Dedup must reject duplicates via SQLite INSERT OR IGNORE (v10.37.45 SQLite-only)."""

    def test_duplicate_rejected_after_cache_clear(self, tmp_path, monkeypatch):
        from pulse.brain.save_writers.jsonl_store import (
            append_knowledge_jsonl, _JSONL_DEDUP_CACHE, _make_fingerprint,
        )
        jsonl_file = tmp_path / "auto_lessons.jsonl"
        entry = {
            "id": "L-test0001",
            "fingerprint": _make_fingerprint("filelock prevents corruption"),
            "type": "lesson", "content": "filelock prevents corruption",
            "confidence": 0.8, "salience": 1.0,
        }
        assert append_knowledge_jsonl(jsonl_file, dict(entry)) is True
        key = str(jsonl_file)
        if key in _JSONL_DEDUP_CACHE:
            del _JSONL_DEDUP_CACHE[key]
        # SQLite INSERT OR IGNORE silently deduplicates — no duplicate row created
        # Function may return True (no error) but SQLite won't add a second row
        append_knowledge_jsonl(jsonl_file, dict(entry))
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        count = conn.execute("SELECT COUNT(*) FROM lessons WHERE fingerprint=?",
                             (entry["fingerprint"],)).fetchone()[0]
        assert count <= 1, f"Duplicate row created: {count}"
        conn.execute("DELETE FROM lessons WHERE fingerprint=?", (entry["fingerprint"],))
        conn.execute("DELETE FROM knowledge_fts WHERE item_id=?", (entry["id"],))
        conn.commit()

    def test_distinct_entry_accepted_after_cache_clear(self, tmp_path):
        """A genuinely different entry must still be accepted after cache clear."""
        from pulse.brain.save_writers.jsonl_store import (
            append_knowledge_jsonl, _JSONL_DEDUP_CACHE, _make_fingerprint,
        )
        jsonl_file = tmp_path / "auto_lessons.jsonl"
        entry_a = {
            "id": "L-aaa00001",
            "fingerprint": _make_fingerprint("filelock prevents corruption"),
            "type": "lesson", "content": "filelock prevents corruption",
            "confidence": 0.8, "salience": 1.0,
        }
        entry_b = {
            "id": "L-bbb00002",
            "fingerprint": _make_fingerprint("always run brain query before coding"),
            "type": "lesson", "content": "always run brain query before coding",
            "confidence": 0.7, "salience": 1.5,
        }
        assert append_knowledge_jsonl(jsonl_file, dict(entry_a)) is True
        key = str(jsonl_file)
        if key in _JSONL_DEDUP_CACHE:
            del _JSONL_DEDUP_CACHE[key]
        assert append_knowledge_jsonl(jsonl_file, dict(entry_b)) is True

    def test_fingerprint_loaded_for_legacy_entries_without_fingerprint_field(self, tmp_path):
        """Entries on disk missing a 'fingerprint' field should still be
        deduplicated via content-based fingerprint computation."""
        from pulse.brain.save_writers.jsonl_store import (
            append_knowledge_jsonl,
            _JSONL_DEDUP_CACHE,
            _make_fingerprint,
        )
        jsonl_file = tmp_path / "auto_lessons.jsonl"

        # Write a legacy entry directly (no fingerprint field)
        legacy = {"id": "L-legacy01", "type": "lesson",
                  "content": "subprocess encoding must be utf8"}
        jsonl_file.write_text(
            json.dumps(legacy, ensure_ascii=False) + "\n", encoding="utf-8"
        )

        # Ensure cache is cold
        key = str(jsonl_file)
        if key in _JSONL_DEDUP_CACHE:
            del _JSONL_DEDUP_CACHE[key]

        # Now attempt to append the same content with a proper fingerprint
        new_entry = {
            "id": "L-new00001",
            "fingerprint": _make_fingerprint("subprocess encoding must be utf8"),
            "type": "lesson",
            "content": "subprocess encoding must be utf8",
            "confidence": 0.6,
            "salience": 1.0,
        }
        assert append_knowledge_jsonl(jsonl_file, dict(new_entry)) is False


# ---------------------------------------------------------------------------
# Test 2: Prediction ID fuzzy matching
# ---------------------------------------------------------------------------

class TestAlertMatches:
    """_alert_matches must support exact ID, prefix ID, and title-based matching."""

    def test_exact_id_match(self):
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {"task_id": "t_123", "status": "active"}
        assert _alert_matches(alert, "t_123") is True

    def test_exact_id_no_match(self):
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {"task_id": "t_123", "status": "active"}
        assert _alert_matches(alert, "t_999") is False

    def test_prefix_match_alert_is_prefix(self):
        """Alert task_id 't_123' is a prefix of task_id 't_123_456'."""
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {"task_id": "t_123", "status": "active"}
        assert _alert_matches(alert, "t_123_456") is True

    def test_prefix_match_task_is_prefix(self):
        """Task ID 't_123' is a prefix of alert task_id 't_123_suffix'."""
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {"task_id": "t_123_suffix", "status": "active"}
        assert _alert_matches(alert, "t_123") is True

    def test_title_in_prediction_text(self):
        """Alert prediction text contains the task title."""
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {
            "task_id": "oracle_abc",
            "prediction": "The brain consolidation daemon will likely crash on large files",
            "status": "active",
        }
        assert _alert_matches(alert, "t_unrelated", "consolidation daemon") is True

    def test_title_in_alert_task_title(self):
        """Alert has a task_title field that contains the search title."""
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {
            "task_id": "oracle_xyz",
            "task_title": "Fix brain consolidation daemon crash",
            "status": "active",
        }
        assert _alert_matches(alert, "t_other", "consolidation daemon") is True

    def test_title_in_evidence_chain(self):
        """Alert evidence_chain contains a string matching the task title."""
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {
            "task_id": "oracle_ev",
            "evidence_chain": ["consolidation daemon instability detected"],
            "status": "active",
        }
        assert _alert_matches(alert, "t_other", "consolidation daemon") is True

    def test_short_title_rejected(self):
        """Titles shorter than 5 chars must not match to avoid false positives."""
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {
            "task_id": "oracle_short",
            "prediction": "fix it now",
            "status": "active",
        }
        # "fix" is only 3 chars -- should not match even though it appears in prediction
        assert _alert_matches(alert, "t_other", "fix") is False

    def test_no_match_returns_false(self):
        """Completely unrelated alert and task must not match."""
        from pulse.daemons.neural.prediction_feedback import _alert_matches
        alert = {
            "task_id": "oracle_999",
            "prediction": "something about API rate limits",
            "evidence_chain": ["api throttling"],
            "task_title": "Rate limit handler",
            "status": "active",
        }
        assert _alert_matches(alert, "t_brain_fix", "brain search latency") is False


# ---------------------------------------------------------------------------
# Test 3: Contradiction confidence penalty
# ---------------------------------------------------------------------------

class TestContradictionPenalty:
    """A hit flagged with _contradiction_flag = True must have its confidence
    multiplied by 0.7 (Dimension A in search_engine.py enrichment loop)."""

    def test_penalty_math(self):
        """0.8 * 0.7 = 0.56 -- verify the exact penalty factor."""
        base_confidence = 0.8
        penalty_factor = 0.7
        expected = round(base_confidence * penalty_factor, 4)
        assert expected == 0.56

    def test_penalty_applied_to_hit(self):
        """Simulate the enrichment loop logic: a contradicted hit gets
        confidence *= 0.7 and the flag is set."""
        hit = {"content": "some claim", "confidence": 0.8}

        # Reproduce the inline logic from search_engine.py lines 203-206
        contradiction_detected = True  # simulates similarity >= 0.5
        if contradiction_detected:
            hit["_contradiction_flag"] = True
            hit["confidence"] = hit.get("confidence", 0.5) * 0.7

        assert hit["_contradiction_flag"] is True
        assert abs(hit["confidence"] - 0.56) < 1e-9

    def test_no_penalty_without_contradiction(self):
        """A non-contradicted hit keeps its original confidence."""
        hit = {"content": "some claim", "confidence": 0.8}

        contradiction_detected = False
        if contradiction_detected:
            hit["_contradiction_flag"] = True
            hit["confidence"] = hit.get("confidence", 0.5) * 0.7

        assert "_contradiction_flag" not in hit
        assert hit["confidence"] == 0.8

    def test_penalty_with_default_confidence(self):
        """When confidence is missing, the .get default of 0.5 applies."""
        hit = {"content": "some claim"}  # no confidence key

        contradiction_detected = True
        if contradiction_detected:
            hit["_contradiction_flag"] = True
            hit["confidence"] = hit.get("confidence", 0.5) * 0.7

        assert abs(hit["confidence"] - 0.35) < 1e-9

    def test_penalty_stacks_with_trust_modulation(self):
        """If contradiction penalty runs before trust modulation, both
        multiply into the final confidence."""
        hit = {"content": "claim", "confidence": 1.0}

        # Step 1: contradiction penalty
        hit["confidence"] = hit["confidence"] * 0.7  # -> 0.7

        # Step 2: agent trust modulation (e.g. trust = 0.8)
        trust_score = 0.8
        hit["confidence"] = hit["confidence"] * trust_score  # -> 0.56

        assert abs(hit["confidence"] - 0.56) < 1e-9


# ---------------------------------------------------------------------------
# Test 4: Dual-write (Phase 2 — JSONL + SQLite)
# ---------------------------------------------------------------------------


class TestDualWrite:
    """Verify append_knowledge_jsonl writes to both JSONL and SQLite."""

    def test_write_to_sqlite(self, tmp_path):
        """An entry must appear in SQLite (SQLite-only mode since v10.37.45)."""
        from pulse.brain.save_writers.jsonl_store import (
            append_knowledge_jsonl, build_jsonl_entry, _JSONL_DEDUP_CACHE,
        )
        _JSONL_DEDUP_CACHE.clear()
        jsonl_file = tmp_path / "auto_lessons.jsonl"
        entry = build_jsonl_entry(
            "SQLite-only write test",
            "opus_test", "2026-03-19T03:00:00Z", "testing", 0.85, 5.0,
        )
        result = append_knowledge_jsonl(jsonl_file, entry)
        assert result is True
        from pulse.core.brain_db import get_conn
        conn = get_conn()
        row = conn.execute(
            "SELECT confidence, domain FROM lessons WHERE fingerprint = ?",
            (entry["fingerprint"],),
        ).fetchone()
        if row:
            assert row[0] == 0.85
            assert row[1] == "testing"
            conn.execute("DELETE FROM lessons WHERE fingerprint = ?", (entry["fingerprint"],))
            conn.execute("DELETE FROM knowledge_fts WHERE item_id = ?", (entry["id"],))
            conn.commit()

    def test_table_mapping(self):
        """JSONL filenames must map to correct SQLite tables."""
        from pulse.brain.save_writers.jsonl_store import _jsonl_to_table
        from pathlib import Path
        assert _jsonl_to_table(Path("auto_lessons.jsonl")) == "lessons"
        assert _jsonl_to_table(Path("auto_fails.jsonl")) == "failures"
        assert _jsonl_to_table(Path("auto_patterns.jsonl")) == "patterns"
        assert _jsonl_to_table(Path("private_lessons.jsonl")) == "private"
        assert _jsonl_to_table(Path("auto_decisions.jsonl")) == "decisions"
        assert _jsonl_to_table(Path("unknown.jsonl")) == ""
