"""Tests for the escalation toggle (enable_escalation config + env var).

Covers:
  - OrchestrationConfig.enable_escalation defaults to True
  - LOOM_ENABLE_ESCALATION env var override
  - loom_fail skips publish_escalation when disabled
  - loom_fail still calls process_failed_task when disabled
  - _process_escalations drains queue without handling when disabled
"""

from __future__ import annotations

import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from loom.config import LoomConfig, OrchestrationConfig, load_config


# ── Tests: Config defaults ────────────────────────────────────────────


class TestEscalationConfigDefault:
    """enable_escalation defaults to True."""

    def test_default_is_true(self) -> None:
        config = OrchestrationConfig()
        assert config.enable_escalation is True

    def test_loom_config_default(self) -> None:
        config = LoomConfig()
        assert config.orchestration.enable_escalation is True

    def test_explicit_false(self) -> None:
        config = OrchestrationConfig(enable_escalation=False)
        assert config.enable_escalation is False


# ── Tests: Env var override ───────────────────────────────────────────


class TestEscalationEnvVar:
    """LOOM_ENABLE_ESCALATION env var disables escalation."""

    def test_false_disables(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("LOOM_ENABLE_ESCALATION", "false")
        config = load_config()
        assert config.orchestration.enable_escalation is False

    def test_zero_disables(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("LOOM_ENABLE_ESCALATION", "0")
        config = load_config()
        assert config.orchestration.enable_escalation is False

    def test_true_enables(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("LOOM_ENABLE_ESCALATION", "true")
        config = load_config()
        assert config.orchestration.enable_escalation is True

    def test_one_enables(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("LOOM_ENABLE_ESCALATION", "1")
        config = load_config()
        assert config.orchestration.enable_escalation is True

    def test_yes_enables(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("LOOM_ENABLE_ESCALATION", "yes")
        config = load_config()
        assert config.orchestration.enable_escalation is True

    def test_unset_defaults_true(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("LOOM_ENABLE_ESCALATION", raising=False)
        config = load_config()
        assert config.orchestration.enable_escalation is True


# ── Tests: loom_fail guard ────────────────────────────────────────────


class TestLoomFailEscalationGuard:
    """loom_fail skips publish_escalation when escalation is disabled."""

    @pytest.fixture()
    def app(self) -> MagicMock:
        app = MagicMock()
        app.project_id = "test-project"
        app.config = LoomConfig()
        app.pool = AsyncMock()
        app.redis = AsyncMock()
        return app

    @patch("loom.mcp.tools.publish_event", new_callable=AsyncMock)
    @patch("loom.mcp.tools.publish_escalation", new_callable=AsyncMock)
    @patch("loom.mcp.tools.store")
    @patch("loom.mcp.tools.cache")
    @patch("loom.mcp.tools._ctx")
    async def test_escalation_disabled_skips_publish(
        self, mock_ctx, mock_cache, mock_store, mock_pub_esc, mock_pub_evt, app
    ) -> None:
        app.config.orchestration.enable_escalation = False
        mock_ctx.return_value = app
        mock_task = MagicMock()
        mock_task.model_dump.return_value = {"id": "t1", "status": "failed"}
        mock_store.fail_task = AsyncMock(return_value=mock_task)
        mock_store.record_event = AsyncMock()
        mock_cache.sync_task = AsyncMock()
        mock_cache.remove_from_ready_queue = AsyncMock()

        with patch("loom.orchestration.retry.process_failed_task", new_callable=AsyncMock, return_value=mock_task):
            from loom.mcp.tools import loom_fail
            await loom_fail(MagicMock(), "t1", "some reason")

        mock_pub_esc.assert_not_called()

    @patch("loom.mcp.tools.publish_event", new_callable=AsyncMock)
    @patch("loom.mcp.tools.publish_escalation", new_callable=AsyncMock)
    @patch("loom.mcp.tools.store")
    @patch("loom.mcp.tools.cache")
    @patch("loom.mcp.tools._ctx")
    async def test_escalation_enabled_publishes(
        self, mock_ctx, mock_cache, mock_store, mock_pub_esc, mock_pub_evt, app
    ) -> None:
        app.config.orchestration.enable_escalation = True
        mock_ctx.return_value = app
        mock_task = MagicMock()
        mock_task.model_dump.return_value = {"id": "t1", "status": "failed"}
        mock_store.fail_task = AsyncMock(return_value=mock_task)
        mock_store.record_event = AsyncMock()
        mock_cache.sync_task = AsyncMock()
        mock_cache.remove_from_ready_queue = AsyncMock()

        with patch("loom.orchestration.retry.process_failed_task", new_callable=AsyncMock, return_value=mock_task):
            from loom.mcp.tools import loom_fail
            await loom_fail(MagicMock(), "t1", "some reason")

        mock_pub_esc.assert_called_once()

    @patch("loom.mcp.tools.publish_event", new_callable=AsyncMock)
    @patch("loom.mcp.tools.publish_escalation", new_callable=AsyncMock)
    @patch("loom.mcp.tools.store")
    @patch("loom.mcp.tools.cache")
    @patch("loom.mcp.tools._ctx")
    async def test_process_failed_task_still_called_when_disabled(
        self, mock_ctx, mock_cache, mock_store, mock_pub_esc, mock_pub_evt, app
    ) -> None:
        """process_failed_task (retries/dead-letter) runs regardless of escalation toggle."""
        app.config.orchestration.enable_escalation = False
        mock_ctx.return_value = app
        mock_task = MagicMock()
        mock_task.model_dump.return_value = {"id": "t1", "status": "failed"}
        mock_store.fail_task = AsyncMock(return_value=mock_task)
        mock_store.record_event = AsyncMock()
        mock_cache.sync_task = AsyncMock()
        mock_cache.remove_from_ready_queue = AsyncMock()

        with patch("loom.orchestration.retry.process_failed_task", new_callable=AsyncMock, return_value=mock_task) as mock_pft:
            from loom.mcp.tools import loom_fail
            await loom_fail(MagicMock(), "t1", "some reason")

        mock_pft.assert_called_once()


# ── Tests: _process_escalations drain ──────────────────────────────────


class TestProcessEscalationsDrain:
    """_process_escalations drains queue without handling when disabled."""

    async def test_drain_without_handling(self) -> None:
        from loom.orchestration.loop import _process_escalations

        config = LoomConfig()
        config.orchestration.enable_escalation = False

        escalation = json.dumps({"task_id": "t1", "message": "boom"}).encode()
        redis = AsyncMock()
        # Return one item, then None to end the loop
        redis.brpop = AsyncMock(side_effect=[
            (b"queue", escalation),
            None,
        ])

        with patch("loom.orchestration.loop.handle_escalation", new_callable=AsyncMock) as mock_handle:
            handled = await _process_escalations(
                AsyncMock(), redis, "proj-1", config, None,
            )

        assert handled == 0
        mock_handle.assert_not_called()

    async def test_handles_when_enabled(self) -> None:
        from loom.orchestration.loop import _process_escalations

        config = LoomConfig()
        config.orchestration.enable_escalation = True

        escalation = json.dumps({"task_id": "t1", "message": "boom"}).encode()
        redis = AsyncMock()
        redis.brpop = AsyncMock(side_effect=[
            (b"queue", escalation),
            None,
        ])

        with patch("loom.orchestration.loop.handle_escalation", new_callable=AsyncMock) as mock_handle:
            handled = await _process_escalations(
                AsyncMock(), redis, "proj-1", config, None,
            )

        assert handled == 1
        mock_handle.assert_called_once()
