"""
Tests for pulse.brain.hot_cache — latency and correctness.

Verifies that search_hot returns results within 50ms for a populated cache,
and that write_hot / trim_hot work correctly.
"""
from __future__ import annotations

import json
import sys
import time
import tempfile
from pathlib import Path
from datetime import datetime, timezone
from unittest.mock import patch

import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_entry(content: str, salience: float = 5.0, age_s: float = 0.0) -> dict:
    """Build a hot-cache JSONL entry dict."""
    return {
        "content": content,
        "title": content[:80],
        "type": "lesson",
        "salience": salience,
        "confidence": 0.7,
        "source": "test_agent",
        "domain": "testing",
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "_hot_written": time.time() - age_s,
    }


def _write_entries(path: Path, entries: list[dict]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        for e in entries:
            f.write(json.dumps(e, ensure_ascii=False, separators=(",", ":")) + "\n")


# ---------------------------------------------------------------------------
# Latency: search_hot must return within 50ms for a full cache (200 entries)
# ---------------------------------------------------------------------------

class TestHotCacheLatency:

    def test_search_hot_returns_within_50ms_empty_cache(self, tmp_path):
        """Empty / missing cache returns in well under 50ms."""
        from pulse.brain.memory.hot_cache import search_hot
        hot_file = tmp_path / "hot_cache.jsonl"
        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            start = time.perf_counter()
            results = search_hot("brain search latency", {})
            elapsed_ms = (time.perf_counter() - start) * 1000
        assert elapsed_ms < 50, f"Empty cache took {elapsed_ms:.1f}ms (limit 50ms)"

    def test_search_hot_returns_within_50ms_full_cache(self, tmp_path):
        """200-entry cache (max size) is read and searched within 50ms.

        Imports are pre-warmed outside the timed window so we measure only
        the I/O + scoring path, not Python cold-import overhead.
        """
        from pulse.brain.memory.hot_cache import search_hot, HOT_MAX_ENTRIES
        # Pre-warm: one search on a tiny file to trigger all lazy imports
        warm_file = tmp_path / "warm.jsonl"
        _write_entries(warm_file, [_make_entry("warmup entry for import preloading", salience=5.0)])
        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", warm_file):
            search_hot("warmup", {})

        entries = [
            _make_entry(
                f"brain search lesson about hot cache performance entry number {i} "
                "filelock atomic write salience boost",
                salience=5.0 + (i % 3),
            )
            for i in range(HOT_MAX_ENTRIES)
        ]
        hot_file = tmp_path / "hot_cache.jsonl"
        _write_entries(hot_file, entries)

        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            start = time.perf_counter()
            results = search_hot("brain search performance", {})
            elapsed_ms = (time.perf_counter() - start) * 1000

        assert elapsed_ms < 50, f"Full cache ({HOT_MAX_ENTRIES} entries) took {elapsed_ms:.1f}ms (limit 50ms)"
        assert isinstance(results, list)

    def test_search_hot_returns_matching_results(self, tmp_path):
        """search_hot returns entries that match the query."""
        from pulse.brain.memory.hot_cache import search_hot

        entries = [
            _make_entry("filelock prevents race condition on task claiming", salience=5.0),
            _make_entry("temporal decay reduces confidence for stale knowledge", salience=5.5),
            _make_entry("hot cache improves brain search latency dramatically", salience=6.0),
        ]
        hot_file = tmp_path / "hot_cache.jsonl"
        _write_entries(hot_file, entries)

        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            results = search_hot("brain search latency hot cache", {})

        assert len(results) >= 1, "Expected at least one matching result"
        assert all(r.get("source") == "hot_cache" for r in results)

    def test_search_hot_excludes_expired_entries(self, tmp_path):
        """Entries older than HOT_MAX_AGE_S are skipped during search."""
        from pulse.brain.memory.hot_cache import search_hot, HOT_MAX_AGE_S

        entries = [
            _make_entry("fresh entry about hot cache search speed", age_s=0),
            _make_entry("stale entry about hot cache search speed", age_s=HOT_MAX_AGE_S + 10),
        ]
        hot_file = tmp_path / "hot_cache.jsonl"
        _write_entries(hot_file, entries)

        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            results = search_hot("hot cache search speed", {})

        texts = [r.get("text", "") for r in results]
        assert any("fresh" in t for t in texts), "Fresh entry should appear"
        assert not any("stale" in t for t in texts), "Stale entry should be filtered"

    def test_search_hot_sorted_by_salience_descending(self, tmp_path):
        """Results are sorted highest salience first."""
        from pulse.brain.memory.hot_cache import search_hot

        entries = [
            _make_entry("hot cache latency low salience entry testing", salience=4.5),
            _make_entry("hot cache latency high salience entry testing", salience=9.0),
            _make_entry("hot cache latency medium salience entry testing", salience=6.5),
        ]
        hot_file = tmp_path / "hot_cache.jsonl"
        _write_entries(hot_file, entries)

        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            results = search_hot("hot cache latency testing", {})

        assert len(results) >= 2
        for i in range(len(results) - 1):
            assert results[i]["salience"] >= results[i + 1]["salience"], (
                "Results must be sorted by salience descending"
            )


# ---------------------------------------------------------------------------
# write_hot: salience gate + file write
# ---------------------------------------------------------------------------

class TestWriteHot:

    def test_write_hot_rejects_low_salience(self, tmp_path):
        from pulse.brain.memory.hot_cache import write_hot, HOT_SALIENCE_THRESHOLD

        entry = _make_entry("low salience content here", salience=HOT_SALIENCE_THRESHOLD - 0.1)
        hot_file = tmp_path / "hot_cache.jsonl"
        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            result = write_hot(entry)
        assert result is False
        assert not hot_file.exists()

    def test_write_hot_accepts_high_salience(self, tmp_path):
        from pulse.brain.memory.hot_cache import write_hot, HOT_SALIENCE_THRESHOLD

        entry = _make_entry("actionable high salience content here", salience=HOT_SALIENCE_THRESHOLD + 1.0)
        hot_file = tmp_path / "hot_cache.jsonl"
        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            result = write_hot(entry)
        assert result is True
        assert hot_file.exists()
        lines = hot_file.read_text(encoding="utf-8").strip().splitlines()
        assert len(lines) == 1
        stored = json.loads(lines[0])
        assert "actionable high salience content" in stored["content"]

    def test_write_hot_rejects_short_content(self, tmp_path):
        from pulse.brain.memory.hot_cache import write_hot, HOT_SALIENCE_THRESHOLD

        entry = _make_entry("short", salience=HOT_SALIENCE_THRESHOLD + 1.0)
        hot_file = tmp_path / "hot_cache.jsonl"
        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            result = write_hot(entry)
        assert result is False


# ---------------------------------------------------------------------------
# trim_hot: removes expired + excess entries
# ---------------------------------------------------------------------------

class TestTrimHot:

    def test_trim_hot_removes_expired(self, tmp_path):
        from pulse.brain.memory.hot_cache import trim_hot, HOT_MAX_AGE_S

        entries = [
            _make_entry(f"fresh entry number {i} testing trim operation", age_s=0)
            for i in range(3)
        ] + [
            _make_entry(f"expired entry number {i} testing trim operation", age_s=HOT_MAX_AGE_S + 60)
            for i in range(2)
        ]
        hot_file = tmp_path / "hot_cache.jsonl"
        _write_entries(hot_file, entries)

        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            removed = trim_hot()

        assert removed == 2
        lines = hot_file.read_text(encoding="utf-8").strip().splitlines()
        assert len(lines) == 3

    def test_trim_hot_no_file_returns_zero(self, tmp_path):
        from pulse.brain.memory.hot_cache import trim_hot

        hot_file = tmp_path / "nonexistent_hot_cache.jsonl"
        with patch("pulse.brain.hot_cache.HOT_CACHE_FILE", hot_file):
            result = trim_hot()
        assert result == 0
