"""Tests for agent loop guard (Phase 14.3)."""

from __future__ import annotations

from openjarvis.core.events import EventBus, EventType


class TestLoopGuard:
    def _make_guard(self, **kwargs):
        from openjarvis.agents.loop_guard import LoopGuard, LoopGuardConfig

        kwargs.setdefault("warn_before_block", False)
        config = LoopGuardConfig(**kwargs)
        bus = EventBus(record_history=True)
        return LoopGuard(config, bus=bus), bus

    def test_identical_calls_blocked(self):
        guard, bus = self._make_guard(max_identical_calls=2)
        v1 = guard.check_call("calc", '{"x": 1}')
        assert not v1.blocked
        # Rust backend uses a HashSet — blocks on the second identical call
        v2 = guard.check_call("calc", '{"x": 1}')
        assert v2.blocked
        assert "identical" in v2.reason.lower()

    def test_different_args_not_blocked(self):
        guard, _ = self._make_guard(max_identical_calls=2)
        guard.check_call("calc", '{"x": 1}')
        guard.check_call("calc", '{"x": 1}')
        v = guard.check_call("calc", '{"x": 2}')
        assert not v.blocked

    def test_ping_pong_detection(self):
        guard, _ = self._make_guard(ping_pong_window=4, poll_tool_budget=100)
        guard.check_call("A", "{}")
        guard.check_call("B", "{}")
        guard.check_call("A", '{"x": 1}')
        guard.check_call("B", '{"x": 1}')
        guard.check_call("A", '{"x": 2}')
        # After A-B-A-B pattern, next A should be blocked
        # Note: exact blocking depends on the window + detection logic
        # The sequence [A, B, A, B, A] with window=4 should detect A-B-A-B
        # But detection happens after 4+ calls in sequence

    def test_poll_budget_exceeded(self):
        guard, _ = self._make_guard(poll_tool_budget=3, max_identical_calls=100)
        guard.check_call("poll", '{"a": 1}')
        guard.check_call("poll", '{"a": 2}')
        guard.check_call("poll", '{"a": 3}')
        v = guard.check_call("poll", '{"a": 4}')
        assert v.blocked
        assert "poll budget" in v.reason.lower()

    def test_event_emitted(self):
        guard, bus = self._make_guard(max_identical_calls=1)
        guard.check_call("x", '{"a": 1}')
        guard.check_call("x", '{"a": 1}')
        events = [
            e for e in bus.history if e.event_type == EventType.LOOP_GUARD_TRIGGERED
        ]
        assert len(events) == 1

    def test_reset(self):
        guard, _ = self._make_guard(max_identical_calls=2)
        guard.check_call("x", '{"a": 1}')
        guard.check_call("x", '{"a": 1}')
        guard.reset()
        v = guard.check_call("x", '{"a": 1}')
        assert not v.blocked

    def test_context_compression_no_overflow(self):
        from openjarvis.core.types import Message, Role

        guard, _ = self._make_guard(max_context_messages=100)
        messages = [Message(role=Role.USER, content=f"msg {i}") for i in range(10)]
        result = guard.compress_context(messages)
        assert len(result) == 10

    def test_context_compression_with_overflow(self):
        from openjarvis.core.types import Message, Role

        guard, _ = self._make_guard(max_context_messages=10)
        messages = (
            [
                Message(role=Role.SYSTEM, content="sys"),
            ]
            + [Message(role=Role.USER, content=f"msg {i}") for i in range(50)]
            + [
                Message(role=Role.TOOL, content=f"result {i}", tool_call_id=f"t{i}")
                for i in range(50)
            ]
        )
        result = guard.compress_context(messages)
        assert len(result) <= 10

    def test_context_compression_stage4_uses_current_state(self):
        """Stage 4 should derive from compressed state."""
        from openjarvis.core.types import Message, Role

        guard, _ = self._make_guard(max_context_messages=5)
        messages = (
            [
                Message(role=Role.SYSTEM, content="sys"),
            ]
            + [Message(role=Role.USER, content=f"msg {i}") for i in range(100)]
            + [
                Message(
                    role=Role.TOOL,
                    content=f"result {i}",
                    tool_call_id=f"t{i}",
                )
                for i in range(100)
            ]
        )
        result = guard.compress_context(messages)
        assert len(result) == 5
        system_count = sum(1 for m in result if getattr(m, "role", None) == "system")
        assert system_count == 1

    def test_check_response_returns_unblocked(self):
        guard, _ = self._make_guard()
        v = guard.check_response("some content")
        assert not v.blocked

    def test_disabled_loop_guard(self):
        from openjarvis.agents.loop_guard import LoopGuard, LoopGuardConfig

        config = LoopGuardConfig(enabled=False)
        guard = LoopGuard(config)
        # Even though we'd normally block, disabled guard shouldn't
        for _ in range(10):
            guard.check_call("x", '{"a": 1}')
        # Guard is still created but check_call still works
        # (the enabled flag is checked at the ToolUsingAgent level)
