"""Tests for the sovereignty loop orchestrator.

Refs: #953
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest


@pytest.mark.unit
@pytest.mark.asyncio
class TestSovereignPerceive:
    """Tests for sovereign_perceive (perception layer)."""

    async def test_cache_hit_skips_vlm(self):
        """When cache has high-confidence match, VLM is not called."""
        from timmy.sovereignty.perception_cache import CacheResult
        from timmy.sovereignty.sovereignty_loop import sovereign_perceive

        cache = MagicMock()
        cache.match.return_value = CacheResult(
            confidence=0.95, state={"template_name": "health_bar"}
        )

        vlm = AsyncMock()
        screenshot = MagicMock()

        with patch(
            "timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
            new_callable=AsyncMock,
        ) as mock_emit:
            result = await sovereign_perceive(screenshot, cache, vlm)

        assert result == {"template_name": "health_bar"}
        vlm.analyze.assert_not_called()
        mock_emit.assert_called_once_with("perception_cache_hit", session_id="")

    async def test_cache_miss_calls_vlm_and_crystallizes(self):
        """On cache miss, VLM is called and output is crystallized."""
        from timmy.sovereignty.perception_cache import CacheResult
        from timmy.sovereignty.sovereignty_loop import sovereign_perceive

        cache = MagicMock()
        cache.match.return_value = CacheResult(confidence=0.3, state=None)

        vlm = AsyncMock()
        vlm.analyze.return_value = {"items": []}

        screenshot = MagicMock()
        crystallize_fn = MagicMock(return_value=[])

        with patch(
            "timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
            new_callable=AsyncMock,
        ):
            await sovereign_perceive(screenshot, cache, vlm, crystallize_fn=crystallize_fn)

        vlm.analyze.assert_called_once_with(screenshot)
        crystallize_fn.assert_called_once()


@pytest.mark.unit
@pytest.mark.asyncio
class TestSovereignDecide:
    """Tests for sovereign_decide (decision layer)."""

    async def test_rule_hit_skips_llm(self, tmp_path):
        """Reliable rule match bypasses the LLM."""
        from timmy.sovereignty.auto_crystallizer import Rule, RuleStore
        from timmy.sovereignty.sovereignty_loop import sovereign_decide

        store = RuleStore(path=tmp_path / "strategy.json")
        store.add(
            Rule(
                id="r1",
                condition="health low",
                action="heal",
                confidence=0.9,
                times_applied=5,
                times_succeeded=4,
            )
        )

        llm = AsyncMock()
        context = {"health": "low", "mana": 50}

        with patch(
            "timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
            new_callable=AsyncMock,
        ):
            result = await sovereign_decide(context, llm, rule_store=store)

        assert result["action"] == "heal"
        assert result["source"] == "crystallized_rule"
        llm.reason.assert_not_called()

    async def test_no_rule_calls_llm_and_crystallizes(self, tmp_path):
        """Without matching rules, LLM is called and reasoning is crystallized."""
        from timmy.sovereignty.auto_crystallizer import RuleStore
        from timmy.sovereignty.sovereignty_loop import sovereign_decide

        store = RuleStore(path=tmp_path / "strategy.json")

        llm = AsyncMock()
        llm.reason.return_value = {
            "action": "attack",
            "reasoning": "I chose attack because enemy_health was below 50%.",
        }

        context = {"enemy_health": 45}

        with patch(
            "timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
            new_callable=AsyncMock,
        ):
            result = await sovereign_decide(context, llm, rule_store=store)

        assert result["action"] == "attack"
        llm.reason.assert_called_once_with(context)
        # The reasoning should have been crystallized (threshold pattern detected)
        assert len(store) > 0


@pytest.mark.unit
@pytest.mark.asyncio
class TestSovereignNarrate:
    """Tests for sovereign_narrate (narration layer)."""

    async def test_template_hit_skips_llm(self):
        """Known event type uses template without LLM."""
        from timmy.sovereignty.sovereignty_loop import sovereign_narrate

        template_store = {
            "combat_start": "Battle begins against {enemy}!",
        }

        llm = AsyncMock()

        with patch(
            "timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
            new_callable=AsyncMock,
        ) as mock_emit:
            result = await sovereign_narrate(
                {"type": "combat_start", "enemy": "Cliff Racer"},
                llm=llm,
                template_store=template_store,
            )

        assert result == "Battle begins against Cliff Racer!"
        llm.narrate.assert_not_called()
        mock_emit.assert_called_once_with("narration_template", session_id="")

    async def test_unknown_event_calls_llm(self):
        """Unknown event type falls through to LLM and crystallizes template."""
        from timmy.sovereignty.sovereignty_loop import sovereign_narrate

        template_store = {}

        llm = AsyncMock()
        llm.narrate.return_value = "You discovered a hidden cave in the mountains."

        with patch(
            "timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
            new_callable=AsyncMock,
        ):
            with patch(
                "timmy.sovereignty.sovereignty_loop._crystallize_narration_template"
            ) as mock_cryst:
                result = await sovereign_narrate(
                    {"type": "discovery", "location": "mountains"},
                    llm=llm,
                    template_store=template_store,
                )

        assert result == "You discovered a hidden cave in the mountains."
        llm.narrate.assert_called_once()
        mock_cryst.assert_called_once()

    async def test_no_llm_returns_default(self):
        """Without LLM and no template, returns a default narration."""
        from timmy.sovereignty.sovereignty_loop import sovereign_narrate

        with patch(
            "timmy.sovereignty.sovereignty_loop.emit_sovereignty_event",
            new_callable=AsyncMock,
        ):
            result = await sovereign_narrate(
                {"type": "unknown_event"},
                llm=None,
                template_store={},
            )

        assert "[unknown_event]" in result


@pytest.mark.unit
@pytest.mark.asyncio
class TestSovereigntyEnforcedDecorator:
    """Tests for the @sovereignty_enforced decorator."""

    async def test_cache_hit_skips_function(self):
        """Decorator returns cached value without calling the wrapped function."""
        from timmy.sovereignty.sovereignty_loop import sovereignty_enforced

        call_count = 0

        @sovereignty_enforced(
            layer="decision",
            cache_check=lambda a, kw: "cached_result",
        )
        async def expensive_fn():
            nonlocal call_count
            call_count += 1
            return "expensive_result"

        with patch("timmy.sovereignty.sovereignty_loop.get_metrics_store") as mock_store:
            mock_store.return_value = MagicMock()
            result = await expensive_fn()

        assert result == "cached_result"
        assert call_count == 0

    async def test_cache_miss_runs_function(self):
        """Decorator calls function when cache returns None."""
        from timmy.sovereignty.sovereignty_loop import sovereignty_enforced

        @sovereignty_enforced(
            layer="decision",
            cache_check=lambda a, kw: None,
        )
        async def expensive_fn():
            return "computed_result"

        with patch("timmy.sovereignty.sovereignty_loop.get_metrics_store") as mock_store:
            mock_store.return_value = MagicMock()
            result = await expensive_fn()

        assert result == "computed_result"
