"""Integration tests for the escalation handler and orchestrator loop."""

from __future__ import annotations

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

import pytest

from loom.bus import channels
from loom.config import LoomConfig, OrchestrationConfig, SkillsConfig
from loom.graph import cache, store
from loom.graph.project import create_project
from loom.graph.task import Priority, Task, TaskStatus
from loom.ids import task_id as gen_task_id
from loom.orchestration.escalation import handle_escalation


@pytest.fixture
async def project(pool):
    """Create a test project and return its ID as a string."""
    record = await create_project(pool, "escalation-test", "Escalation tests")
    return str(record["id"])


def _make_config(**overrides) -> LoomConfig:
    """Build a LoomConfig with test defaults."""
    return LoomConfig(
        project_id="test",
        skills=SkillsConfig(api_key="test-key"),
        orchestration=OrchestrationConfig(
            claim_ttl_seconds=600,
            max_retries=3,
            sweep_interval_seconds=1,
            **overrides,
        ),
    )


def _mock_anthropic(output: dict):
    """Create a mock AsyncAnthropic that returns JSON output."""
    mock_response = MagicMock()
    mock_response.content = [MagicMock(text=json.dumps(output))]
    mock_response.usage = MagicMock(input_tokens=50, output_tokens=100)
    mock_client = AsyncMock()
    mock_client.messages.create = AsyncMock(return_value=mock_response)
    return MagicMock(return_value=mock_client)


# ── Test 1: Escalation round trip ────────────────────────────────────────


@pytest.mark.asyncio
async def test_escalation_round_trip(pool, redis_conn, project):
    """Escalation created → diagnosis run → escalation resolved."""
    task = Task(
        id=gen_task_id(), project_id=project, title="Failing task",
        priority=Priority.P0,
        context={"description": "Something complex"},
        output={"error": "segfault"},
    )
    task = await store.create_task(pool, task)
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "segfault in module X")

    config = _make_config()
    diagnosis_output = {
        "diagnosis": {
            "root_cause": "Null pointer dereference",
            "category": "code_bug",
            "confidence": "high",
        },
        "fix_steps": ["Check null before access"],
        "suggested_tasks": [],
    }

    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic(diagnosis_output)):
        result = await handle_escalation(
            pool, redis_conn, project, task.id,
            "segfault in module X", config,
        )

    assert result["task_id"] == task.id
    assert result["diagnosis"]["diagnosis"]["root_cause"] == "Null pointer dereference"
    assert result["fix_tasks_created"] == []

    # Verify escalation is marked resolved in DB
    async with pool.acquire() as conn:
        esc = await conn.fetchrow(
            "SELECT resolved FROM escalations WHERE task_id = $1 ORDER BY id DESC LIMIT 1",
            task.id,
        )
    assert esc["resolved"] is True


# ── Test 2: Escalation creates fix tasks ─────────────────────────────────


@pytest.mark.asyncio
async def test_escalation_creates_fix_tasks(pool, redis_conn, project):
    """Handler creates child tasks from suggested_tasks in diagnosis."""
    task = Task(
        id=gen_task_id(), project_id=project, title="Broken feature",
        priority=Priority.P1,
    )
    task = await store.create_task(pool, task)
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "TypeError in handler")

    config = _make_config()
    diagnosis_output = {
        "diagnosis": {
            "root_cause": "Wrong type passed to handler",
            "category": "code_bug",
            "confidence": "high",
        },
        "fix_steps": ["Add type check", "Write test"],
        "suggested_tasks": [
            {
                "title": "Add type validation",
                "context": "Validate input types in the handler",
                "priority": "p0",
                "done_when": "Handler rejects invalid types",
            },
            {
                "title": "Write regression test",
                "context": "Test that the handler works with correct types",
                "priority": "p1",
                "done_when": "Test passes in CI",
            },
        ],
    }

    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic(diagnosis_output)):
        result = await handle_escalation(
            pool, redis_conn, project, task.id,
            "TypeError in handler", config,
        )

    assert len(result["fix_tasks_created"]) == 2

    # Verify the created tasks exist in Postgres
    for fix_id in result["fix_tasks_created"]:
        fix_task = await store.get_task(pool, fix_id)
        assert fix_task.parent_id == task.id
        assert fix_task.status == TaskStatus.PENDING

    # Verify fix tasks are in the ready queue
    ready = await cache.get_ready_tasks(redis_conn, pool, project, limit=10)
    ready_ids = {r.id for r in ready}
    for fix_id in result["fix_tasks_created"]:
        assert fix_id in ready_ids


# ── Test 3: Escalation with skill failure falls back ─────────────────────


@pytest.mark.asyncio
async def test_escalation_skill_failure_fallback(pool, redis_conn, project):
    """If the debug_failure skill fails, a minimal diagnosis is returned."""
    task = Task(
        id=gen_task_id(), project_id=project, title="Another failing task",
        priority=Priority.P1,
    )
    task = await store.create_task(pool, task)
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "Unknown error")

    config = _make_config()

    # Make the skill raise an exception
    mock_client = AsyncMock()
    mock_client.messages.create = AsyncMock(side_effect=RuntimeError("API down"))
    mock_cls = MagicMock(return_value=mock_client)

    with patch("loom.skills.runner.AsyncAnthropic", mock_cls):
        result = await handle_escalation(
            pool, redis_conn, project, task.id,
            "Unknown error", config,
        )

    # Should get a fallback diagnosis
    assert result["diagnosis"]["diagnosis"]["category"] == "unknown"
    assert result["diagnosis"]["diagnosis"]["confidence"] == "low"
    assert result["fix_tasks_created"] == []


# ── Test 4: Orchestrator single sweep ────────────────────────────────────


@pytest.mark.asyncio
async def test_orchestrator_single_sweep(pool, redis_conn, project):
    """One orchestrator tick processes expired claims + retryable tasks."""
    from loom.orchestration.loop import orchestrator_tick

    # Create a task, claim it, expire its TTL
    t1 = Task(id=gen_task_id(), project_id=project, title="Expired claim")
    t1 = await store.create_task(pool, t1)
    t1 = await store.claim_task(pool, t1.id, "agent-stuck", ttl_seconds=600)
    await cache.sync_task(redis_conn, t1)
    async with pool.acquire() as conn:
        await conn.execute(
            "UPDATE tasks SET claim_expires_at = NOW() - interval '1 second' WHERE id = $1",
            t1.id,
        )

    # Create a task, fail it, set retry_after in the past
    t2 = Task(id=gen_task_id(), project_id=project, title="Retryable task")
    t2 = await store.create_task(pool, t2)
    t2 = await store.claim_task(pool, t2.id, "agent-2")
    t2 = await store.fail_task(pool, t2.id, "transient error")
    async with pool.acquire() as conn:
        await conn.execute(
            "UPDATE tasks SET retry_after = NOW() - interval '1 second', "
            "status = 'failed', dead_letter = FALSE WHERE id = $1",
            t2.id,
        )

    config = _make_config()

    summary = await orchestrator_tick(
        pool, redis_conn, project, config,
    )

    assert summary["expired_claims_released"] == 1
    assert summary["tasks_retried"] == 1

    # Verify t1 is back to pending
    updated_t1 = await store.get_task(pool, t1.id)
    assert updated_t1.status == TaskStatus.PENDING

    # Verify t2 is back to pending
    updated_t2 = await store.get_task(pool, t2.id)
    assert updated_t2.status == TaskStatus.PENDING


# ── Test 5: Project completion detection ─────────────────────────────────


@pytest.mark.asyncio
async def test_project_completion_detection(pool, redis_conn, project):
    """Orchestrator detects when all tasks are done."""
    from loom.orchestration.loop import orchestrator_tick

    # Create two tasks and mark them done
    for i in range(2):
        t = Task(id=gen_task_id(), project_id=project, title=f"Task {i}")
        t = await store.create_task(pool, t)
        t = await store.claim_task(pool, t.id, f"agent-{i}")
        await store.complete_task(pool, t.id, {"result": "ok"})

    config = _make_config()
    summary = await orchestrator_tick(pool, redis_conn, project, config)

    assert summary["project_complete"] is True
