"""Tests for daemon lifecycle manager: TTLCaches, checkpoint_tick tool tracking,
store session tracking, storage.get_by_id, and context_monitor action.

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.calibration import Tier
from claude_engram.daemon import MemoryDaemon


@pytest_asyncio.fixture()
async def daemon(tmp_path: Path) -> MemoryDaemon:
    """Create a MemoryDaemon with a temp database."""
    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()


# ==============================================================================
# TTLCache initialization tests
# ==============================================================================


class TestTTLCacheInit:
    """Verify 4 new TTLCaches are initialized in daemon.__init__."""

    def test_context_history_exists(self, daemon: MemoryDaemon) -> None:
        assert hasattr(daemon, "_context_history")
        assert daemon._context_history.maxsize == 200

    def test_session_memories_exists(self, daemon: MemoryDaemon) -> None:
        assert hasattr(daemon, "_session_memories")
        assert daemon._session_memories.maxsize == 200

    def test_session_tool_counts_exists(self, daemon: MemoryDaemon) -> None:
        assert hasattr(daemon, "_session_tool_counts")
        assert daemon._session_tool_counts.maxsize == 200

    def test_handoff_state_exists(self, daemon: MemoryDaemon) -> None:
        assert hasattr(daemon, "_handoff_state")
        assert daemon._handoff_state.maxsize == 200

    def test_calibration_config_loaded(self, daemon: MemoryDaemon) -> None:
        from claude_engram.calibration import CalibrationConfig

        assert hasattr(daemon, "_calibration")
        assert isinstance(daemon._calibration, CalibrationConfig)

    def test_caches_empty_on_init(self, daemon: MemoryDaemon) -> None:
        """All in-memory state empty on init (SC#11: safe restart)."""
        assert len(daemon._context_history) == 0
        assert len(daemon._session_memories) == 0
        assert len(daemon._session_tool_counts) == 0
        assert len(daemon._handoff_state) == 0


# ==============================================================================
# checkpoint_tick tool_name tracking tests
# ==============================================================================


class TestCheckpointTickToolName:
    """Test checkpoint_tick with tool_name parameter."""

    @pytest.mark.asyncio()
    async def test_tool_name_tracked(self, daemon: MemoryDaemon) -> None:
        """checkpoint_tick with tool_name increments per-session tool count."""
        result = await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "s1",
                "profile": "balanced",
                "tool_name": "Write",
            }
        )
        assert result["checkpoint_fired"] is False
        assert daemon._session_tool_counts["s1"]["Write"] == 1

    @pytest.mark.asyncio()
    async def test_tool_name_multiple_tools(self, daemon: MemoryDaemon) -> None:
        """Multiple different tools tracked separately."""
        await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "s1",
                "profile": "balanced",
                "tool_name": "Write",
            }
        )
        await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "s1",
                "profile": "balanced",
                "tool_name": "Edit",
            }
        )
        await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "s1",
                "profile": "balanced",
                "tool_name": "Write",
            }
        )
        assert daemon._session_tool_counts["s1"]["Write"] == 2
        assert daemon._session_tool_counts["s1"]["Edit"] == 1

    @pytest.mark.asyncio()
    async def test_tool_name_backward_compatible(self, daemon: MemoryDaemon) -> None:
        """checkpoint_tick without tool_name still works (backward compatible)."""
        result = await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "s2",
                "profile": "balanced",
            }
        )
        assert result["checkpoint_fired"] is False
        assert result["counter"] == 1
        # No tool count should be created for this session
        assert "s2" not in daemon._session_tool_counts

    @pytest.mark.asyncio()
    async def test_tool_name_across_sessions(self, daemon: MemoryDaemon) -> None:
        """Tool counts are per-session, not global."""
        await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "s1",
                "profile": "balanced",
                "tool_name": "Write",
            }
        )
        await daemon._process_request(
            {
                "action": "checkpoint_tick",
                "session_id": "s2",
                "profile": "balanced",
                "tool_name": "Write",
            }
        )
        assert daemon._session_tool_counts["s1"]["Write"] == 1
        assert daemon._session_tool_counts["s2"]["Write"] == 1


# ==============================================================================
# store with session_id tracking tests
# ==============================================================================


class TestStoreSessionTracking:
    """Test store action tracks session_id -> memory_id mapping."""

    @pytest.mark.asyncio()
    async def test_store_with_session_id_tracks(self, daemon: MemoryDaemon) -> None:
        """store with session_id in metadata tracks memory_id."""
        result = await daemon._process_request(
            {
                "action": "store",
                "content": "test memory for session",
                "category": "decision",
                "metadata": {"session_id": "s1"},
            }
        )
        assert result["status"] == "stored"
        memory_id = result["id"]
        assert "s1" in daemon._session_memories
        assert memory_id in daemon._session_memories["s1"]

    @pytest.mark.asyncio()
    async def test_store_without_session_id_no_tracking(self, daemon: MemoryDaemon) -> None:
        """store without session_id does NOT modify _session_memories."""
        result = await daemon._process_request(
            {
                "action": "store",
                "content": "test memory no session",
                "category": "decision",
            }
        )
        assert result["status"] == "stored"
        # _session_memories should have no entries
        assert len(daemon._session_memories) == 0

    @pytest.mark.asyncio()
    async def test_store_multiple_memories_same_session(self, daemon: MemoryDaemon) -> None:
        """Multiple stores with same session_id accumulate IDs."""
        result1 = await daemon._process_request(
            {
                "action": "store",
                "content": "first memory",
                "category": "decision",
                "metadata": {"session_id": "s1"},
            }
        )
        result2 = await daemon._process_request(
            {
                "action": "store",
                "content": "second memory",
                "category": "lesson",
                "metadata": {"session_id": "s1"},
            }
        )
        assert len(daemon._session_memories["s1"]) == 2
        assert result1["id"] in daemon._session_memories["s1"]
        assert result2["id"] in daemon._session_memories["s1"]


# ==============================================================================
# storage.get_by_id tests
# ==============================================================================


class TestStorageGetById:
    """Test MemoryStorage.get_by_id method."""

    @pytest.mark.asyncio()
    async def test_get_by_id_returns_memory(self, daemon: MemoryDaemon) -> None:
        """get_by_id with valid ID returns memory dict."""
        result = await daemon._process_request(
            {
                "action": "store",
                "content": "find me by id",
                "category": "decision",
                "metadata": {"key": "value"},
            }
        )
        memory_id = result["id"]

        found = daemon.storage.get_by_id(memory_id)
        assert found is not None
        assert found["id"] == memory_id
        assert found["content"] == "find me by id"
        assert found["category"] == "decision"
        assert found["metadata"] == {"key": "value"}
        assert "created_at" in found
        assert "proj_tag" in found

    @pytest.mark.asyncio()
    async def test_get_by_id_nonexistent_returns_none(self, daemon: MemoryDaemon) -> None:
        """get_by_id with nonexistent ID returns None."""
        result = daemon.storage.get_by_id(99999)
        assert result is None

    @pytest.mark.asyncio()
    async def test_get_by_id_metadata_parsed(self, daemon: MemoryDaemon) -> None:
        """get_by_id returns metadata as dict (parsed from JSON)."""
        result = await daemon._process_request(
            {
                "action": "store",
                "content": "metadata test",
                "category": "lesson",
                "metadata": {"nested": {"data": True}},
            }
        )
        memory_id = result["id"]
        found = daemon.storage.get_by_id(memory_id)
        assert found is not None
        assert isinstance(found["metadata"], dict)
        assert found["metadata"]["nested"]["data"] is True


# ==============================================================================
# context_monitor action tests
# ==============================================================================


class TestContextMonitor:
    """Test context_monitor action for context lifecycle tracking."""

    async def _send_monitor(
        self, daemon: MemoryDaemon, session_id: str, used_pct: float, turn_number: int
    ) -> dict:
        """Helper to send a context_monitor request."""
        return await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": session_id,
                "used_pct": used_pct,
                "turn_number": turn_number,
            }
        )

    @pytest.mark.asyncio()
    async def test_first_sample_returns_none_velocity(self, daemon: MemoryDaemon) -> None:
        """First sample: velocity and eta are null (insufficient data)."""
        result = await self._send_monitor(daemon, "s1", 10.0, 1)
        assert result["status"] == "ok"
        assert result["tier"] == "none"
        assert result["velocity"] is None
        assert result["eta"] is None
        assert result["samples"] == 1

    @pytest.mark.asyncio()
    async def test_two_samples_calculates_velocity(self, daemon: MemoryDaemon) -> None:
        """With min_samples (2), velocity and eta become non-null."""
        await self._send_monitor(daemon, "s1", 10.0, 1)
        result = await self._send_monitor(daemon, "s1", 15.0, 2)
        assert result["status"] == "ok"
        assert result["velocity"] is not None
        assert result["velocity"] > 0
        assert result["eta"] is not None
        assert result["samples"] == 2

    @pytest.mark.asyncio()
    async def test_emergency_tier_at_high_pct(self, daemon: MemoryDaemon) -> None:
        """used_pct >= emergency_pct (78%) returns emergency tier."""
        # Need min_samples for tier determination based on ETA,
        # but EMERGENCY is percentage-based (bypasses ETA)
        await self._send_monitor(daemon, "s1", 70.0, 1)
        result = await self._send_monitor(daemon, "s1", 80.0, 2)
        assert result["tier"] == "emergency"

    @pytest.mark.asyncio()
    async def test_handoff_tier_low_eta(self, daemon: MemoryDaemon) -> None:
        """eta <= handoff_eta (3) returns handoff tier."""
        # Simulate rapid usage growth: 60% -> 73% in one turn
        # velocity = 13 pct/turn, target = 75, remaining = 2, eta ~= 0.15
        await self._send_monitor(daemon, "s1", 60.0, 1)
        result = await self._send_monitor(daemon, "s1", 73.0, 2)
        # With high velocity, eta should be very small -> handoff or emergency
        assert result["tier"] in ("handoff", "emergency")

    @pytest.mark.asyncio()
    async def test_advisory_tier_medium_eta(self, daemon: MemoryDaemon) -> None:
        """eta <= advisory_eta (8) but > handoff_eta (3) returns advisory."""
        # Simulate moderate growth: 40% -> 45% = 5 pct/turn
        # target = 75, remaining = 30, eta = 6 -> advisory
        await self._send_monitor(daemon, "s1", 40.0, 1)
        result = await self._send_monitor(daemon, "s1", 45.0, 2)
        assert result["tier"] == "advisory"

    @pytest.mark.asyncio()
    async def test_none_tier_high_eta(self, daemon: MemoryDaemon) -> None:
        """eta > advisory_eta (8) returns none tier."""
        # Simulate slow growth: 10% -> 12% = 2 pct/turn
        # target = 75, remaining = 63, eta = 31.5 -> none
        await self._send_monitor(daemon, "s1", 10.0, 1)
        result = await self._send_monitor(daemon, "s1", 12.0, 2)
        assert result["tier"] == "none"

    @pytest.mark.asyncio()
    async def test_samples_capped_at_20(self, daemon: MemoryDaemon) -> None:
        """context_history per session capped at 20 samples."""
        for i in range(25):
            await self._send_monitor(daemon, "s1", 10.0 + i * 0.5, i + 1)

        result = await self._send_monitor(daemon, "s1", 25.0, 26)
        # samples in history should be capped at 20 (keeps last 20)
        assert result["samples"] <= 20

    @pytest.mark.asyncio()
    async def test_handoff_ready_set_on_handoff_tier(self, daemon: MemoryDaemon) -> None:
        """When tier >= HANDOFF, _handoff_state gets created."""
        # Force high velocity -> handoff tier
        await self._send_monitor(daemon, "s1", 60.0, 1)
        result = await self._send_monitor(daemon, "s1", 73.0, 2)
        if result["tier"] in ("handoff", "emergency"):
            assert result["handoff_ready"] is True
            assert "s1" in daemon._handoff_state

    @pytest.mark.asyncio()
    async def test_handoff_not_regenerated(self, daemon: MemoryDaemon) -> None:
        """Once handoff is ready, subsequent calls don't regenerate."""
        await self._send_monitor(daemon, "s1", 60.0, 1)
        await self._send_monitor(daemon, "s1", 73.0, 2)

        # Set up a mock handoff state
        from claude_engram.handoff import HandoffState, SessionType

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

        # Next monitor call should see existing handoff, not recreate
        result = await self._send_monitor(daemon, "s1", 74.0, 3)
        assert result["handoff_ready"] is True

    @pytest.mark.asyncio()
    async def test_snooze_suppresses_tier(self, daemon: MemoryDaemon) -> None:
        """After snooze, tier returns 'none' until snooze_until_pct exceeded."""
        from claude_engram.handoff import HandoffState, SessionType

        # Manually set a snoozed handoff state
        daemon._handoff_state["s1"] = HandoffState(
            created_at="2026-02-02T00:00:00Z",
            session_type=SessionType.CODING,
            ready=True,
            tier=Tier.HANDOFF,
            snooze_until_pct=85.0,  # Snoozed until 85%
            snooze_count=1,
        )

        # Send samples that would normally trigger handoff
        await self._send_monitor(daemon, "s1", 60.0, 1)
        result = await self._send_monitor(daemon, "s1", 73.0, 2)
        # Should be suppressed by snooze (73% < 85%)
        assert result["tier"] == "none"

    @pytest.mark.asyncio()
    async def test_snooze_expires_at_threshold(self, daemon: MemoryDaemon) -> None:
        """Snooze expires when used_pct exceeds snooze_until_pct."""
        from claude_engram.handoff import HandoffState, SessionType

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

        # First sample
        await self._send_monitor(daemon, "s1", 70.0, 1)
        # Second sample at 76% -> exceeds snooze_until_pct
        result = await self._send_monitor(daemon, "s1", 76.0, 2)
        # Snooze expired, tier should NOT be "none" (velocity-dependent)
        # At 76%, with target 75%, we're past target -> handoff or emergency
        assert result["tier"] != "none"

    @pytest.mark.asyncio()
    async def test_used_pct_in_response(self, daemon: MemoryDaemon) -> None:
        """Response includes used_pct echo."""
        result = await self._send_monitor(daemon, "s1", 42.5, 1)
        assert result["used_pct"] == 42.5

    @pytest.mark.asyncio()
    async def test_separate_sessions_independent(self, daemon: MemoryDaemon) -> None:
        """Different sessions have independent context histories."""
        await self._send_monitor(daemon, "s1", 10.0, 1)
        await self._send_monitor(daemon, "s1", 15.0, 2)
        await self._send_monitor(daemon, "s2", 50.0, 1)

        result_s2 = await self._send_monitor(daemon, "s2", 55.0, 2)
        assert result_s2["samples"] == 2

        # s1 should have its own count
        assert len(daemon._context_history["s1"]) == 2
        assert len(daemon._context_history["s2"]) == 2


# ==============================================================================
# get_handoff_status action tests
# ==============================================================================


class TestGetHandoffStatus:
    """Test get_handoff_status action for stop hook handoff readiness."""

    async def _send_monitor(
        self, daemon: MemoryDaemon, session_id: str, used_pct: float, turn_number: int
    ) -> dict:
        """Helper to send a context_monitor request."""
        return await daemon._process_request(
            {
                "action": "context_monitor",
                "session_id": session_id,
                "used_pct": used_pct,
                "turn_number": turn_number,
            }
        )

    @pytest.mark.asyncio()
    async def test_no_handoff_state_returns_not_ready(self, daemon: MemoryDaemon) -> None:
        """No handoff state: returns ready=false."""
        result = await daemon._process_request(
            {"action": "get_handoff_status", "session_id": "nonexistent"}
        )
        assert result["status"] == "ok"
        assert result["ready"] is False

    @pytest.mark.asyncio()
    async def test_ready_state_generates_document_lazily(self, daemon: MemoryDaemon) -> None:
        """Handoff state ready=True, document=None: classifies session, generates document."""
        from claude_engram.handoff import HandoffState, SessionType

        # Set up session context: tool counts + memories + context history
        daemon._session_tool_counts["s1"] = {"Write": 6, "Edit": 3}
        # Store a memory and track it
        await daemon._process_request(
            {
                "action": "store",
                "content": "test decision for handoff",
                "category": "decision",
                "metadata": {"session_id": "s1"},
            }
        )
        # Add context history samples for velocity/ETA
        await self._send_monitor(daemon, "s1", 60.0, 1)
        await self._send_monitor(daemon, "s1", 70.0, 2)

        # Manually set handoff state as ready with no document
        daemon._handoff_state["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": "s1", "git_branch": "main"}
        )
        assert result["status"] == "ok"
        assert result["ready"] is True
        assert result["tier"] == "handoff"
        assert "used_pct" in result
        assert "eta" in result
        assert "session_type" in result
        # Document should now be generated on the state
        assert daemon._handoff_state["s1"].document is not None

    @pytest.mark.asyncio()
    async def test_ready_state_existing_document_not_regenerated(
        self, daemon: MemoryDaemon
    ) -> None:
        """Handoff state ready=True, document exists: returns existing, no regeneration."""
        from claude_engram.handoff import HandoffState, SessionType

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

        result = await daemon._process_request({"action": "get_handoff_status", "session_id": "s1"})
        assert result["status"] == "ok"
        assert result["ready"] is True
        assert result["tier"] == "handoff"
        # Document should be unchanged (not regenerated)
        assert daemon._handoff_state["s1"].document is existing_doc

    @pytest.mark.asyncio()
    async def test_emergency_tier_regenerates_document(self, daemon: MemoryDaemon) -> None:
        """EMERGENCY tier with existing HANDOFF document: regenerates (Pitfall 4)."""
        from claude_engram.handoff import HandoffState, SessionType

        # Set up session data so classifier/generator work
        daemon._session_tool_counts["s1"] = {"Write": 10}
        await daemon._process_request(
            {
                "action": "store",
                "content": "emergency decision",
                "category": "decision",
                "metadata": {"session_id": "s1"},
            }
        )
        await self._send_monitor(daemon, "s1", 70.0, 1)
        await self._send_monitor(daemon, "s1", 80.0, 2)

        # Existing HANDOFF-tier document
        old_doc = {"type": "coding", "used_pct": 70.0, "eta": 2.0, "tier_marker": "handoff"}
        daemon._handoff_state["s1"] = HandoffState(
            created_at="2026-02-02T00:00:00Z",
            session_type=SessionType.CODING,
            ready=True,
            tier=Tier.EMERGENCY,  # Now EMERGENCY
            document=old_doc,
        )

        result = await daemon._process_request(
            {"action": "get_handoff_status", "session_id": "s1", "git_branch": "main"}
        )
        assert result["status"] == "ok"
        assert result["ready"] is True
        assert result["tier"] == "emergency"
        # Document MUST have been regenerated (different object)
        new_doc = daemon._handoff_state["s1"].document
        assert new_doc is not old_doc

    @pytest.mark.asyncio()
    async def test_insufficient_session_data_returns_not_ready(self, daemon: MemoryDaemon) -> None:
        """Empty session (0 tools, 0 memories): should_generate_handoff() -> ready=false."""
        from claude_engram.handoff import HandoffState, SessionType

        # No tool counts, no memories for this session
        daemon._handoff_state["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": "s1"})
        assert result["status"] == "ok"
        assert result["ready"] is False
        assert result.get("reason") == "insufficient_session_data"


# ==============================================================================
# get_handoff action tests
# ==============================================================================


class TestGetHandoff:
    """Test get_handoff action for inject.js document retrieval."""

    @pytest.mark.asyncio()
    async def test_no_state_returns_null_document(self, daemon: MemoryDaemon) -> None:
        """No state: returns document=null."""
        result = await daemon._process_request(
            {"action": "get_handoff", "session_id": "nonexistent"}
        )
        assert result["status"] == "ok"
        assert result["document"] is None

    @pytest.mark.asyncio()
    async def test_consumed_returns_null_document(self, daemon: MemoryDaemon) -> None:
        """State ready but consumed: returns document=null."""
        from claude_engram.handoff import HandoffState, SessionType

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

        result = await daemon._process_request({"action": "get_handoff", "session_id": "s1"})
        assert result["status"] == "ok"
        assert result["document"] is None

    @pytest.mark.asyncio()
    async def test_ready_unconsumed_returns_document_and_marks_consumed(
        self, daemon: MemoryDaemon
    ) -> None:
        """State ready, not consumed: returns document, marks consumed=True."""
        from claude_engram.handoff import HandoffState, SessionType

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

        result = await daemon._process_request({"action": "get_handoff", "session_id": "s1"})
        assert result["status"] == "ok"
        assert result["document"] == doc
        assert result["tier"] == "handoff"
        # Should now be marked consumed
        assert daemon._handoff_state["s1"].consumed is True

    @pytest.mark.asyncio()
    async def test_second_call_returns_null_after_consumed(self, daemon: MemoryDaemon) -> None:
        """After get_handoff, second call returns null (consumed)."""
        from claude_engram.handoff import HandoffState, SessionType

        doc = {"type": "coding", "used_pct": 70.0}
        daemon._handoff_state["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
        result1 = await daemon._process_request({"action": "get_handoff", "session_id": "s1"})
        assert result1["document"] is not None

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

    @pytest.mark.asyncio()
    async def test_fallback_finds_unconsumed_across_sessions(self, daemon: MemoryDaemon) -> None:
        """Fallback: session_id doesn't match, finds latest unconsumed from other sessions."""
        from claude_engram.handoff import HandoffState, SessionType

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

        # Query with a DIFFERENT session_id (simulates session_id change after /clear)
        result = await daemon._process_request(
            {"action": "get_handoff", "session_id": "new-session-after-clear"}
        )
        assert result["status"] == "ok"
        assert result["document"] == doc
        assert result["tier"] == "handoff"
        # Should mark old session as consumed
        assert daemon._handoff_state["old-session"].consumed is True


# ==============================================================================
# clear_handoff action tests
# ==============================================================================


class TestClearHandoff:
    """Test clear_handoff action for inject.js cleanup."""

    @pytest.mark.asyncio()
    async def test_existing_session_cleared(self, daemon: MemoryDaemon) -> None:
        """Session exists: deletes entry, returns cleared=true."""
        from claude_engram.handoff import HandoffState, SessionType

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

        result = await daemon._process_request({"action": "clear_handoff", "session_id": "s1"})
        assert result["status"] == "ok"
        assert result["cleared"] is True
        assert "s1" not in daemon._handoff_state

    @pytest.mark.asyncio()
    async def test_nonexistent_session_idempotent(self, daemon: MemoryDaemon) -> None:
        """Session doesn't exist: returns cleared=true (idempotent)."""
        result = await daemon._process_request(
            {"action": "clear_handoff", "session_id": "nonexistent"}
        )
        assert result["status"] == "ok"
        assert result["cleared"] is True


# ==============================================================================
# snooze_handoff action tests
# ==============================================================================


class TestSnoozeHandoff:
    """Test snooze_handoff action for progressive backoff."""

    @pytest.mark.asyncio()
    async def test_first_snooze_sets_increment(self, daemon: MemoryDaemon) -> None:
        """First snooze: snooze_until_pct = current_pct + base_increment (5%)."""
        from claude_engram.handoff import HandoffState, SessionType

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

        result = await daemon._process_request(
            {"action": "snooze_handoff", "session_id": "s1", "current_pct": 60.0}
        )
        assert result["status"] == "ok"
        # First snooze: 5.0% increment, so snooze_until_pct = 65.0
        assert result["snooze_until_pct"] == pytest.approx(65.0)
        assert result["snooze_count"] == 1
        # Ready flag should be reset
        assert daemon._handoff_state["s1"].ready is False

    @pytest.mark.asyncio()
    async def test_progressive_snooze_increases_increment(self, daemon: MemoryDaemon) -> None:
        """Progressive: second snooze has larger increment."""
        from claude_engram.handoff import HandoffState, SessionType

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

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

    @pytest.mark.asyncio()
    async def test_snooze_capped_at_max(self, daemon: MemoryDaemon) -> None:
        """Progressive snooze capped at max_snooze_pct (12%)."""
        from claude_engram.handoff import HandoffState, SessionType

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

        result = await daemon._process_request(
            {"action": "snooze_handoff", "session_id": "s1", "current_pct": 70.0}
        )
        assert result["status"] == "ok"
        # Increment = min(5.0 * (1 + 0.6 * 5), 12.0) = min(20.0, 12.0) = 12.0
        assert result["snooze_until_pct"] == pytest.approx(82.0)
        assert result["snooze_count"] == 6

    @pytest.mark.asyncio()
    async def test_snooze_resets_ready_flag(self, daemon: MemoryDaemon) -> None:
        """Snooze resets ready flag to False."""
        from claude_engram.handoff import HandoffState, SessionType

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

        await daemon._process_request(
            {"action": "snooze_handoff", "session_id": "s1", "current_pct": 60.0}
        )
        assert daemon._handoff_state["s1"].ready is False

    @pytest.mark.asyncio()
    async def test_snooze_no_state_returns_null(self, daemon: MemoryDaemon) -> None:
        """Snooze with no state: returns snooze_until_pct=null."""
        result = await daemon._process_request(
            {"action": "snooze_handoff", "session_id": "nonexistent", "current_pct": 60.0}
        )
        assert result["status"] == "ok"
        assert result["snooze_until_pct"] is None
