"""Integration tests for the full context lifecycle manager.

Tests the complete lifecycle chain end-to-end using the real MemoryDaemon
(in-process, no socket) with real MemoryStorage (SQLite-vec + MiniLM):

1. Full handoff lifecycle: context_monitor -> trigger -> get_handoff -> clear
2. Emergency tier on high percentage
3. Snooze mechanism (keep going)
4. Tool count tracking + classifier integration
5. Backward compatibility with existing daemon actions

Uses pytest-asyncio to test the async _process_request method directly.
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import patch

import pytest
import pytest_asyncio

from claude_engram.daemon import MemoryDaemon


@pytest_asyncio.fixture()
async def daemon(tmp_path: Path) -> MemoryDaemon:
    """Create a MemoryDaemon with a temp database (real storage)."""
    with patch("claude_engram.daemon.MemoryStorage") as mock_cls:
        from claude_engram.storage import MemoryStorage

        real_storage = MemoryStorage(db_path=tmp_path / "test.db")
        mock_cls.return_value = real_storage
        d = MemoryDaemon()
        d.storage = real_storage
        yield d
        real_storage.close()


# ==============================================================================
# Test 1: Full handoff lifecycle (happy path)
# ==============================================================================


class TestFullHandoffLifecycle:
    """Context monitor -> trigger -> get_handoff_status -> get_handoff -> clear."""

    @pytest.mark.asyncio()
    async def test_full_lifecycle_happy_path(self, daemon: MemoryDaemon) -> None:
        """End-to-end: monitor context growth, trigger handoff, consume, clear."""
        # Simulate context growth over 5 turns (accelerating usage)
        for i, pct in enumerate([20.0, 35.0, 50.0, 60.0, 70.0]):
            result = await daemon._process_request(
                {
                    "action": "context_monitor",
                    "session_id": "lifecycle-s1",
                    "used_pct": pct,
                    "turn_number": i + 1,
                }
            )

        # After 70% with accelerating growth, velocity should predict
        # nearing 75% target soon -> ADVISORY or HANDOFF tier
        assert result["tier"] in ("advisory", "handoff", "emergency")
        assert result["velocity"] is not None
        assert result["velocity"] > 0

        # If handoff triggered, walk the full consumption path
        if result.get("handoff_ready"):
            # Store a memory with session_id for document generation
            await daemon._process_request(
                {
                    "action": "store",
                    "content": "important decision for lifecycle test",
                    "category": "decision",
                    "metadata": {"session_id": "lifecycle-s1"},
                }
            )
            # Track tool usage for the classifier
            for _ in range(6):
                await daemon._process_request(
                    {
                        "action": "checkpoint_tick",
                        "session_id": "lifecycle-s1",
                        "profile": "balanced",
                        "tool_name": "Write",
                    }
                )

            # Get handoff status (lazy document generation)
            status = await daemon._process_request(
                {
                    "action": "get_handoff_status",
                    "session_id": "lifecycle-s1",
                    "git_branch": "main",
                }
            )
            assert status["ready"] is True

            # Get the actual document (simulating inject.js on /clear)
            handoff = await daemon._process_request(
                {
                    "action": "get_handoff",
                    "session_id": "lifecycle-s1",
                }
            )
            assert handoff["document"] is not None
            assert handoff["document"]["type"] in (
                "coding",
                "debug",
                "gsd_execution",
                "voyager",
                "beacon",
                "research",
            )

            # Clear after consumption
            clear = await daemon._process_request(
                {
                    "action": "clear_handoff",
                    "session_id": "lifecycle-s1",
                }
            )
            assert clear["cleared"] is True

            # Verify cleared: second get returns null
            handoff2 = await daemon._process_request(
                {
                    "action": "get_handoff",
                    "session_id": "lifecycle-s1",
                }
            )
            assert handoff2["document"] is None

    @pytest.mark.asyncio()
    async def test_handoff_consumed_once_only(self, daemon: MemoryDaemon) -> None:
        """Handoff document consumed exactly once (get_handoff marks consumed)."""
        from claude_engram.calibration import Tier
        from claude_engram.handoff import HandoffState, SessionType

        doc = {"type": "coding", "used_pct": 70.0, "eta": 2.0}
        daemon._handoff_state["once-s1"] = HandoffState(
            created_at="2026-02-02T00:00:00Z",
            session_type=SessionType.CODING,
            ready=True,
            tier=Tier.HANDOFF,
            document=doc,
            consumed=False,
        )

        # First call returns document
        r1 = await daemon._process_request({"action": "get_handoff", "session_id": "once-s1"})
        assert r1["document"] is not None

        # Second call returns null (consumed)
        r2 = await daemon._process_request({"action": "get_handoff", "session_id": "once-s1"})
        assert r2["document"] is None


# ==============================================================================
# Test 2: Emergency tier triggers on high percentage
# ==============================================================================


class TestEmergencyTier:
    """EMERGENCY fires when used_pct >= 78%, regardless of velocity."""

    @pytest.mark.asyncio()
    async def test_emergency_at_high_pct(self, daemon: MemoryDaemon) -> None:
        """EMERGENCY tier fires on high percentage (>= 78%)."""
        # First sample — may not trigger (needs min_samples for velocity)
        result1 = await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": "emergency-s1",
                "used_pct": 79.0,
                "turn_number": 1,
            }
        )
        # Even with just 1 sample, >= emergency_pct triggers EMERGENCY
        # (emergency check bypasses velocity requirement)
        assert result1["tier"] == "emergency"

    @pytest.mark.asyncio()
    async def test_emergency_with_two_samples(self, daemon: MemoryDaemon) -> None:
        """EMERGENCY with enough samples for velocity still returns emergency."""
        await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": "emergency-s2",
                "used_pct": 70.0,
                "turn_number": 1,
            }
        )
        result2 = await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": "emergency-s2",
                "used_pct": 80.0,
                "turn_number": 2,
            }
        )
        assert result2["tier"] == "emergency"
        assert result2["handoff_ready"] is True


# ==============================================================================
# Test 3: Snooze mechanism
# ==============================================================================


class TestSnoozeMechanism:
    """After 'Keep going', snooze prevents re-trigger until pct advances."""

    @pytest.mark.asyncio()
    async def test_snooze_suppresses_then_expires(self, daemon: MemoryDaemon) -> None:
        """Snooze suppresses tier, then expires when pct exceeds threshold."""
        # Build up to handoff trigger
        for i, pct in enumerate([20, 40, 55, 65, 72]):
            await daemon._process_request(
                {
                    "action": "context_monitor",
                    "session_id": "snooze-s1",
                    "used_pct": float(pct),
                    "turn_number": i + 1,
                }
            )

        # User chooses "Keep going" -> snooze
        snooze = await daemon._process_request(
            {
                "action": "snooze_handoff",
                "session_id": "snooze-s1",
                "current_pct": 72.0,
            }
        )
        # Default snooze_pct_increment = 5.0
        assert snooze["snooze_until_pct"] == pytest.approx(77.0)

        # Next context_monitor at 73% should be suppressed
        result = await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": "snooze-s1",
                "used_pct": 73.0,
                "turn_number": 6,
            }
        )
        # Tier should be NONE (snoozed) or at least not handoff_ready
        assert result["tier"] == "none" or not result.get("handoff_ready")

    @pytest.mark.asyncio()
    async def test_progressive_snooze_increment(self, daemon: MemoryDaemon) -> None:
        """Each snooze increases the increment (progressive backoff)."""
        from claude_engram.calibration import Tier
        from claude_engram.handoff import HandoffState, SessionType

        daemon._handoff_state["prog-s1"] = HandoffState(
            created_at="2026-02-02T00:00:00Z",
            session_type=SessionType.CODING,
            ready=True,
            tier=Tier.HANDOFF,
            snooze_count=0,
        )

        # First snooze: base increment (5.0%)
        r1 = await daemon._process_request(
            {"action": "snooze_handoff", "session_id": "prog-s1", "current_pct": 60.0}
        )
        assert r1["snooze_until_pct"] == pytest.approx(65.0)
        assert r1["snooze_count"] == 1

        # Re-enable for second snooze
        daemon._handoff_state["prog-s1"].ready = True

        # Second snooze: 5.0 * (1 + 0.6 * 1) = 8.0
        r2 = await daemon._process_request(
            {"action": "snooze_handoff", "session_id": "prog-s1", "current_pct": 65.0}
        )
        assert r2["snooze_until_pct"] == pytest.approx(73.0)
        assert r2["snooze_count"] == 2


# ==============================================================================
# Test 4: Tool count tracking and classifier integration
# ==============================================================================


class TestToolCountTracking:
    """checkpoint_tick with tool_name populates _session_tool_counts."""

    @pytest.mark.asyncio()
    async def test_tool_counts_accumulate(self, daemon: MemoryDaemon) -> None:
        """Multiple tool_name calls accumulate per-session counts."""
        for _ in range(6):
            await daemon._process_request(
                {
                    "action": "checkpoint_tick",
                    "session_id": "tools-s1",
                    "profile": "balanced",
                    "tool_name": "Write",
                }
            )
        for _ in range(3):
            await daemon._process_request(
                {
                    "action": "checkpoint_tick",
                    "session_id": "tools-s1",
                    "profile": "balanced",
                    "tool_name": "Edit",
                }
            )

        counts = daemon._session_tool_counts.get("tools-s1", {})
        assert counts.get("Write") == 6
        assert counts.get("Edit") == 3

    @pytest.mark.asyncio()
    async def test_tool_counts_feed_classifier(self, daemon: MemoryDaemon) -> None:
        """Tool counts influence session classification via get_handoff_status."""
        from claude_engram.calibration import Tier
        from claude_engram.handoff import HandoffState, SessionType

        # Set up enough Write+Edit for CODING classification (>5)
        daemon._session_tool_counts["class-s1"] = {"Write": 8, "Edit": 4}

        # Store a memory to pass should_generate_handoff()
        await daemon._process_request(
            {
                "action": "store",
                "content": "classifier test memory",
                "category": "decision",
                "metadata": {"session_id": "class-s1"},
            }
        )

        # Add context history for velocity
        await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": "class-s1",
                "used_pct": 60.0,
                "turn_number": 1,
            }
        )
        await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": "class-s1",
                "used_pct": 70.0,
                "turn_number": 2,
            }
        )

        # Set handoff state ready, no document
        daemon._handoff_state["class-s1"] = HandoffState(
            created_at="2026-02-02T00:00:00Z",
            session_type=SessionType.CODING,
            ready=True,
            tier=Tier.HANDOFF,
            document=None,
        )

        result = await daemon._process_request(
            {
                "action": "get_handoff_status",
                "session_id": "class-s1",
                "git_branch": "main",
            }
        )
        assert result["ready"] is True
        # With Write(8)+Edit(4)=12 > 5 threshold, should classify as CODING
        assert result["session_type"] == "coding"


# ==============================================================================
# Test 5: No regression on existing daemon actions
# ==============================================================================


class TestExistingActionsUnchanged:
    """Verify store, search, checkpoint_tick (without tool_name) still work."""

    @pytest.mark.asyncio()
    async def test_store_still_works(self, daemon: MemoryDaemon) -> None:
        """Basic store action unchanged."""
        result = await daemon._process_request(
            {
                "action": "store",
                "content": "regression test memory",
                "category": "general",
            }
        )
        assert result["status"] == "stored"
        assert "id" in result

    @pytest.mark.asyncio()
    async def test_search_still_works(self, daemon: MemoryDaemon) -> None:
        """Basic search action unchanged."""
        await daemon._process_request(
            {
                "action": "store",
                "content": "searchable regression test",
                "category": "decision",
            }
        )
        result = await daemon._process_request(
            {
                "action": "search",
                "query": "regression test",
            }
        )
        assert result["status"] == "ok"
        assert "results" in result

    @pytest.mark.asyncio()
    async def test_checkpoint_tick_without_tool_name(self, daemon: MemoryDaemon) -> None:
        """checkpoint_tick without tool_name (backward compat) still works."""
        result = await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "compat-s1",
                "profile": "balanced",
            }
        )
        assert "checkpoint_fired" in result
        assert result["counter"] == 1

    @pytest.mark.asyncio()
    async def test_inject_still_works(self, daemon: MemoryDaemon) -> None:
        """inject action returns context and count."""
        await daemon._process_request(
            {
                "action": "store",
                "content": "inject regression test decision",
                "category": "decision",
            }
        )
        result = await daemon._process_request(
            {
                "action": "inject",
                "query": "regression test",
                "limit": 5,
            }
        )
        # Should return context (even if empty due to similarity threshold)
        assert "context" in result
        assert "count" in result

    @pytest.mark.asyncio()
    async def test_ping_still_works(self, daemon: MemoryDaemon) -> None:
        """ping action returns pong with version."""
        result = await daemon._process_request({"action": "ping"})
        assert result["status"] == "pong"
        assert "version" in result


# ==============================================================================
# Test 6: Cross-session handoff fallback
# ==============================================================================


class TestCrossSessionFallback:
    """Handoff consumption works across session_id changes (Research OQ1)."""

    @pytest.mark.asyncio()
    async def test_new_session_finds_old_handoff(self, daemon: MemoryDaemon) -> None:
        """After /clear, new session_id can find handoff from old session."""
        from claude_engram.calibration import Tier
        from claude_engram.handoff import HandoffState, SessionType

        doc = {"type": "coding", "used_pct": 72.0, "eta": 1.5}
        daemon._handoff_state["old-sess-xyz"] = HandoffState(
            created_at="2026-02-02T00:00:00Z",
            session_type=SessionType.CODING,
            ready=True,
            tier=Tier.HANDOFF,
            document=doc,
            consumed=False,
        )

        # New session after /clear has different session_id
        result = await daemon._process_request(
            {"action": "get_handoff", "session_id": "new-sess-after-clear"}
        )
        assert result["document"] is not None
        assert result["document"]["type"] == "coding"

        # Clear using new session_id (shouldn't fail)
        clear = await daemon._process_request(
            {"action": "clear_handoff", "session_id": "new-sess-after-clear"}
        )
        assert clear["cleared"] is True
