"""
Tests for cortex/hippo/tools/recursion_guard.py — Expansion recursion guard.

Issue: #1118
"""

from __future__ import annotations

import pytest

from cortex.hippo.tools.recursion_guard import (
    EXPANSION_DELEGATION_DEPTH_CAP,
    EXPANSION_RECURSION_ERROR_CODE,
    clear_delegated_expansion_context,
    create_expansion_request_id,
    evaluate_expansion_recursion_guard,
    get_delegated_context_for_tests,
    get_telemetry_snapshot_for_tests,
    record_expansion_delegation_telemetry,
    reset_for_tests,
    resolve_expansion_request_id,
    resolve_next_expansion_depth,
    stamp_delegated_expansion_context,
)


@pytest.fixture(autouse=True)
def clean_state():
    """Reset all in-memory state before each test."""
    reset_for_tests()
    yield
    reset_for_tests()


class TestCreateExpansionRequestId:
    def test_returns_uuid_string(self):
        rid = create_expansion_request_id()
        assert isinstance(rid, str)
        assert len(rid) == 36  # UUID format
        assert "-" in rid

    def test_unique_ids(self):
        ids = {create_expansion_request_id() for _ in range(100)}
        assert len(ids) == 100


class TestResolveExpansionRequestId:
    def test_no_context_returns_new_id(self):
        rid = resolve_expansion_request_id("session-a")
        assert isinstance(rid, str)
        assert len(rid) == 36

    def test_with_stamped_context_returns_stamped_id(self):
        stamp_delegated_expansion_context(
            session_key="session-a",
            request_id="req-123",
            expansion_depth=0,
            origin_session_key="main",
            stamped_by="test",
        )
        assert resolve_expansion_request_id("session-a") == "req-123"


class TestResolveNextExpansionDepth:
    def test_no_context_returns_1(self):
        assert resolve_next_expansion_depth("session-a") == 1

    def test_empty_key_returns_1(self):
        assert resolve_next_expansion_depth("") == 1
        assert resolve_next_expansion_depth(None) == 1

    def test_with_stamped_context_increments(self):
        stamp_delegated_expansion_context(
            session_key="session-a",
            request_id="req-1",
            expansion_depth=0,
            origin_session_key="main",
            stamped_by="test",
        )
        assert resolve_next_expansion_depth("session-a") == 1


class TestStampAndEvaluate:
    def test_allowed_at_depth_0(self):
        """A session stamped at depth 0 (below cap) is allowed to delegate."""
        stamp_delegated_expansion_context(
            session_key="child-1",
            request_id="req-1",
            expansion_depth=0,
            origin_session_key="main",
            stamped_by="test",
        )

        decision = evaluate_expansion_recursion_guard(
            session_key="child-1",
            request_id="req-1",
        )

        assert decision.blocked is False
        assert decision.expansion_depth == 0
        assert decision.origin_session_key == "main"

    def test_blocked_at_depth_cap(self):
        """A session stamped at depth >= cap is blocked."""
        stamp_delegated_expansion_context(
            session_key="child-1",
            request_id="req-1",
            expansion_depth=EXPANSION_DELEGATION_DEPTH_CAP,
            origin_session_key="main",
            stamped_by="test",
        )

        decision = evaluate_expansion_recursion_guard(
            session_key="child-1",
            request_id="req-new",
        )

        assert decision.blocked is True
        assert decision.code == EXPANSION_RECURSION_ERROR_CODE
        assert decision.reason == "depth_cap"
        assert "blocked at depth" in decision.message

    def test_idempotent_reentry_detection(self):
        """Repeated calls with same requestId return idempotent_reentry reason."""
        stamp_delegated_expansion_context(
            session_key="child-1",
            request_id="req-1",
            expansion_depth=EXPANSION_DELEGATION_DEPTH_CAP,
            origin_session_key="main",
            stamped_by="test",
        )

        # First block — depth_cap
        d1 = evaluate_expansion_recursion_guard(
            session_key="child-1",
            request_id="req-repeat",
        )
        assert d1.blocked is True
        assert d1.reason == "depth_cap"

        # Second block — idempotent_reentry
        d2 = evaluate_expansion_recursion_guard(
            session_key="child-1",
            request_id="req-repeat",
        )
        assert d2.blocked is True
        assert d2.reason == "idempotent_reentry"

    def test_no_context_allows(self):
        """Without any stamped context, evaluate allows."""
        decision = evaluate_expansion_recursion_guard(
            session_key="session-fresh",
            request_id="req-1",
        )
        assert decision.blocked is False
        assert decision.expansion_depth == 0


class TestClearContext:
    def test_clear_removes_context(self):
        stamp_delegated_expansion_context(
            session_key="child-1",
            request_id="req-1",
            expansion_depth=1,
            origin_session_key="main",
            stamped_by="test",
        )

        assert get_delegated_context_for_tests("child-1") is not None

        clear_delegated_expansion_context("child-1")

        assert get_delegated_context_for_tests("child-1") is None

    def test_clear_empty_key_is_noop(self):
        """Clearing empty key does not raise."""
        clear_delegated_expansion_context("")
        clear_delegated_expansion_context("  ")


class TestTelemetry:
    def test_telemetry_recording(self):
        """Telemetry counters increment correctly."""
        snap0 = get_telemetry_snapshot_for_tests()
        assert snap0["start"] == 0
        assert snap0["block"] == 0

        record_expansion_delegation_telemetry(
            component="test",
            event="start",
            request_id="req-1",
            session_key="s1",
            expansion_depth=0,
            origin_session_key="main",
        )

        snap1 = get_telemetry_snapshot_for_tests()
        assert snap1["start"] == 1

        record_expansion_delegation_telemetry(
            component="test",
            event="block",
            request_id="req-1",
            session_key="s1",
            expansion_depth=1,
            origin_session_key="main",
            reason="depth_cap",
        )

        snap2 = get_telemetry_snapshot_for_tests()
        assert snap2["block"] == 1
        assert snap2["start"] == 1

    def test_reset_clears_counters(self):
        record_expansion_delegation_telemetry(
            component="test",
            event="success",
            request_id="req-1",
            expansion_depth=0,
            origin_session_key="main",
        )
        assert get_telemetry_snapshot_for_tests()["success"] == 1

        reset_for_tests()
        assert get_telemetry_snapshot_for_tests()["success"] == 0


class TestGetDelegatedContextForTests:
    def test_returns_stamped_context(self):
        stamp_delegated_expansion_context(
            session_key="s1",
            request_id="req-1",
            expansion_depth=0,
            origin_session_key="main",
            stamped_by="unit-test",
        )
        ctx = get_delegated_context_for_tests("s1")
        assert ctx is not None
        assert ctx.request_id == "req-1"
        assert ctx.stamped_by == "unit-test"

    def test_returns_none_when_not_stamped(self):
        assert get_delegated_context_for_tests("missing") is None
