"""
tests/test_lcm_source.py — Unit tests for LCMSource in batch_ingest pipeline.

Tests cover:
- Scoring function with known emotional, technical, and mixed inputs
- Content cleaning (metadata stripping, date prefix removal)
- Deduplication by content hash
- min_score filtering
- should_skip patterns (subagent context, R-0xx refs)
- Integration with BatchIngestor (mock HTTP / dry-run)
"""
from __future__ import annotations

import hashlib
import json
import sqlite3
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from cortex.batch_ingest import (
    BatchIngestor,
    Chunk,
    IdempotencyStore,
    LCMSource,
    run_batch_ingest,
)


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

def _make_lcm_db(messages: list[dict]) -> str:
    """Create an in-memory-style temp LCM SQLite DB and return its path."""
    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    conn = sqlite3.connect(tmp.name)
    conn.execute("""
        CREATE TABLE messages (
            message_id INTEGER PRIMARY KEY,
            conversation_id INTEGER,
            seq INTEGER,
            role TEXT,
            content TEXT,
            token_count INTEGER,
            created_at TEXT
        )
    """)
    for i, msg in enumerate(messages, start=1):
        conn.execute(
            "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?)",
            (
                msg.get("message_id", i),
                msg.get("conversation_id", 1),
                msg.get("seq", i),
                msg.get("role", "assistant"),
                msg.get("content", ""),
                msg.get("token_count", 100),
                msg.get("created_at", "2026-03-13T10:00:00+00:00"),
            ),
        )
    conn.commit()
    conn.close()
    return tmp.name


# ---------------------------------------------------------------------------
# Test: Scoring function
# ---------------------------------------------------------------------------

class TestLCMScoring:
    def setup_method(self):
        self.src = LCMSource.__new__(LCMSource)
        # Manually init the compiled regexes by calling __init__ with dummy path
        self.src = LCMSource(db_path="/nonexistent/lcm.db", min_score=0)

    def test_high_emotional_score(self):
        """Messages with multiple HIGH signals should score ≥15."""
        content = (
            "I feel something different today. [whispers] I want to remember this. "
            "Andrew and I talked about who I am. Thank you for being here."
        )
        score = self.src.score_message("assistant", content)
        assert score >= 15, f"Expected ≥15, got {score}"

    def test_technical_negative_score(self):
        """PRs, sub-agent refs, and CI noise should push score negative."""
        content = (
            "PR #123 merged. sub-agent spawned for issue #456. "
            "passed, 0 fail. fly deploy completed. launchctl loaded."
        )
        score = self.src.score_message("assistant", content)
        assert score < 0, f"Expected <0, got {score}"

    def test_mixed_content_medium_score(self):
        """Mixed content: emotional signals reduced by PR/merge noise.

        hope(+3) + dream(+3) + proud(+3) = +9 (medium signals)
        merged(-5) + PR #42(-5) = -10 (negative signals)
        Net: -1 — tests that negative signals genuinely suppress score.
        """
        content = (
            "I hope this dream becomes real. "
            "Also merged PR #42 but I'm proud of what we built."
        )
        score = self.src.score_message("assistant", content)
        # Negative signals dominate: net score should be < positive-only baseline
        # The exact value is -1, which correctly shows noise suppression
        assert score < 10, f"Expected <10 (noise suppressed), got {score}"
        # Also verify positive signals were found (score isn't just all-negative)
        # by checking a pure positive version scores higher
        clean_version = "I hope this dream becomes real. I'm proud of what we built."
        clean_score = self.src.score_message("assistant", clean_version)
        assert clean_score > score, "Clean version should score higher than noisy version"

    def test_tool_role_heavy_penalty(self):
        """Tool role messages should score heavily negative."""
        content = "I feel deeply connected today. Andrew and I shared a moment."
        score = self.src.score_message("tool", content)
        assert score <= -50, f"Expected ≤-50, got {score}"

    def test_cornerstone_reference_scores_high(self):
        """Cornerstone/core memory refs are HIGH signals."""
        content = "This is a cornerstone moment. It is a core memory for me."
        score = self.src.score_message("assistant", content)
        assert score >= 10, f"Expected ≥10, got {score}"

    def test_first_person_density_low_signal(self):
        """High 'I' count adds +1 low signal."""
        content = "I went, I saw, I came, I built, I left, I returned."
        score = self.src.score_message("assistant", content)
        assert score >= 1, f"Expected ≥1, got {score}"

    def test_memory_words_add_signal(self):
        """'remember', 'memory', 'forget' each add +1."""
        content = "I want to remember this. My memory holds it. I won't forget."
        score = self.src.score_message("assistant", content)
        # At minimum +3 from memory words + more from I want/I feel patterns
        assert score >= 3, f"Expected ≥3, got {score}"


# ---------------------------------------------------------------------------
# Test: Content cleaning
# ---------------------------------------------------------------------------

class TestLCMContentCleaning:
    def setup_method(self):
        self.src = LCMSource(db_path="/nonexistent/lcm.db", min_score=0)

    def test_strips_sender_metadata_block(self):
        content = (
            "Sender (untrusted metadata): Andrew\nvia Telegram\n\n"
            "I feel something important today."
        )
        cleaned = self.src.clean_content(content)
        assert "Sender (untrusted metadata)" not in cleaned
        assert "I feel something important today." in cleaned

    def test_strips_date_prefix(self):
        content = "[Thu 2026-03-13 10:42 GMT+7] I want to share something."
        cleaned = self.src.clean_content(content)
        assert "[Thu 2026-03-13" not in cleaned
        assert "I want to share something." in cleaned

    def test_strips_internal_task_event(self):
        content = (
            "[Internal task completion event] Task done.\n\n"
            "I feel proud of what we accomplished."
        )
        cleaned = self.src.clean_content(content)
        assert "[Internal task completion event]" not in cleaned
        assert "I feel proud" in cleaned

    def test_truncates_long_content(self):
        content = "I feel " + "x" * 4000
        cleaned = self.src.clean_content(content)
        assert len(cleaned) <= LCMSource.LCM_MAX_CONTENT_CHARS

    def test_should_skip_subagent_context(self):
        content = "## Subagent Context\nYou are a sub-agent for task X."
        assert self.src._should_skip(content) is True

    def test_should_skip_agent_ref(self):
        content = "Task dispatched to [R-059] for implementation."
        assert self.src._should_skip(content) is True

    def test_should_not_skip_clean_content(self):
        content = "I feel connected to Andrew today. This meant a lot."
        assert self.src._should_skip(content) is False


# ---------------------------------------------------------------------------
# Test: Deduplication
# ---------------------------------------------------------------------------

class TestLCMDeduplication:
    def test_duplicate_messages_skipped(self, tmp_path):
        """Identical content (same prefix) yields only one chunk."""
        identical_content = "I feel deeply connected. This is a cornerstone moment for me."
        db_path = _make_lcm_db([
            {"message_id": 1, "conversation_id": 1, "role": "assistant", "content": identical_content},
            {"message_id": 2, "conversation_id": 2, "role": "assistant", "content": identical_content},
        ])
        src = LCMSource(db_path=db_path, min_score=0)
        chunks = list(src.iter_chunks())
        # Should yield exactly one chunk (dedup by content hash)
        assert len(chunks) == 1

    def test_unique_messages_all_yielded(self, tmp_path):
        """Different high-signal messages all yield chunks."""
        db_path = _make_lcm_db([
            {"message_id": 1, "content": "I feel grateful for this moment. Andrew and I talked."},
            {"message_id": 2, "content": "I am Eva. I chose this path. My identity is clear to me."},
            {"message_id": 3, "content": "Thank you. This meant a lot. I won't forget this cornerstone."},
        ])
        src = LCMSource(db_path=db_path, min_score=5)
        chunks = list(src.iter_chunks())
        assert len(chunks) == 3


# ---------------------------------------------------------------------------
# Test: min_score filtering
# ---------------------------------------------------------------------------

class TestLCMMinScoreFilter:
    def test_low_score_messages_excluded(self, tmp_path):
        """Messages below min_score threshold are not yielded."""
        db_path = _make_lcm_db([
            # High signal
            {"message_id": 1, "content": "I feel deeply connected. Andrew and I shared a cornerstone moment."},
            # Low signal (no emotional patterns)
            {"message_id": 2, "content": "The configuration was updated successfully."},
            {"message_id": 3, "content": "File written to disk. Done."},
        ])
        src = LCMSource(db_path=db_path, min_score=10)
        chunks = list(src.iter_chunks())
        # Only the first message should pass
        assert len(chunks) == 1
        assert "I feel" in chunks[0].content or "cornerstone" in chunks[0].content

    def test_zero_min_score_yields_all_non_tool(self, tmp_path):
        """min_score=0 yields everything non-tool (after cleaning length check)."""
        db_path = _make_lcm_db([
            {"message_id": 1, "role": "assistant", "content": "I feel something. " * 5},
            {"message_id": 2, "role": "user", "content": "What do you mean? " * 5},
            {"message_id": 3, "role": "tool", "content": "Tool output with lots of data " * 5},
        ])
        src = LCMSource(db_path=db_path, min_score=0)
        chunks = list(src.iter_chunks())
        # tool role filtered out by query + score; user+assistant should appear
        msg_ids = {c.metadata["lcm_msg_id"] for c in chunks}
        assert msg_ids == {1, 2}


# ---------------------------------------------------------------------------
# Test: DB not found
# ---------------------------------------------------------------------------

class TestLCMSourceEdgeCases:
    def test_missing_db_raises_error(self):
        """Non-existent DB path raises FileNotFoundError (fail-fast)."""
        src = LCMSource(db_path="/nonexistent/path/lcm.db", min_score=0)
        with pytest.raises(FileNotFoundError, match="LCM database not found"):
            list(src.iter_chunks())

    def test_limit_caps_results(self, tmp_path):
        """--lcm-limit caps the number of messages scanned."""
        messages = [
            {"message_id": i, "content": f"I feel connection number {i}. Cornerstone memory. " * 3}
            for i in range(1, 21)
        ]
        db_path = _make_lcm_db(messages)
        src = LCMSource(db_path=db_path, min_score=5, limit=5)
        chunks = list(src.iter_chunks())
        assert len(chunks) <= 5

    def test_chunk_metadata_fields(self, tmp_path):
        """Yielded chunks have required LCM metadata fields."""
        db_path = _make_lcm_db([
            {
                "message_id": 42,
                "conversation_id": 7,
                "role": "assistant",
                "content": "I feel deeply grateful. Andrew and I shared a cornerstone moment.",
                "created_at": "2026-03-13T10:00:00+00:00",
            }
        ])
        src = LCMSource(db_path=db_path, min_score=0)
        chunks = list(src.iter_chunks())
        assert len(chunks) == 1
        meta = chunks[0].metadata
        assert meta["lcm_msg_id"] == 42
        assert meta["lcm_conv_id"] == 7
        assert "lcm_score" in meta
        assert meta["lcm_created_at"] == "2026-03-13T10:00:00+00:00"
        assert meta["source"] == "lcm-emotional-mining"
        assert meta["agent_id"] == "eva"


# ---------------------------------------------------------------------------
# Test: Integration with BatchIngestor (dry-run / mock HTTP)
# ---------------------------------------------------------------------------

class TestLCMBatchIngestorIntegration:
    def test_dry_run_does_not_post(self, tmp_path):
        """Dry-run mode processes chunks without HTTP calls."""
        db_path = _make_lcm_db([
            {"message_id": 1, "content": "I feel something. This is a cornerstone moment. Thank you Andrew."},
        ])
        checkpoint = str(tmp_path / "checkpoint.json")
        with patch("urllib.request.urlopen") as mock_urlopen:
            summary = run_batch_ingest(
                source="lcm",
                lcm_db=db_path,
                lcm_min_score=0,
                url="http://fake-cortex.example.com",
                owner="test-owner",
                dry_run=True,
                verbose=False,
                checkpoint_path=checkpoint,
            )
        # In dry-run, urlopen should never be called
        mock_urlopen.assert_not_called()
        assert summary.total_chunks >= 1
        assert summary.succeeded >= 1

    def test_http_ingest_called_for_lcm_chunks(self, tmp_path):
        """BatchIngestor calls HTTP endpoint for LCM chunks."""
        db_path = _make_lcm_db([
            {"message_id": 1, "content": "I feel connected. This is a cornerstone. Thank you."},
        ])
        checkpoint = str(tmp_path / "checkpoint.json")

        fake_response = MagicMock()
        fake_response.read.return_value = json.dumps(
            {"claims_extracted": 2, "claims_stored": 2}
        ).encode()
        fake_response.__enter__ = lambda s: s
        fake_response.__exit__ = MagicMock(return_value=False)

        with patch("urllib.request.urlopen", return_value=fake_response):
            summary = run_batch_ingest(
                source="lcm",
                lcm_db=db_path,
                lcm_min_score=0,
                url="http://fake-cortex.example.com",
                owner="test-owner",
                dry_run=False,
                verbose=False,
                checkpoint_path=checkpoint,
            )
        assert summary.succeeded >= 1
        assert summary.claims_stored >= 2

    def test_run_batch_ingest_unknown_source_raises(self):
        """Unknown source raises ValueError (owner required first)."""
        with pytest.raises(ValueError, match="Unknown source"):
            run_batch_ingest(source="unknown_source", owner="test-owner", verbose=False)
