"""
Tests for Story 1.03 — dispatch_to_swarm() Central Dispatch Function
Track B: Genesis Persistent Context Architecture

BB1: No task_id supplied → UUID4 assigned (36 chars, 4 hyphens)
BB2: Successful execution → status == "completed", task_id present
BB3: _execute_task raises → status == "error", correction payload returned
BB4: Multiple concurrent dispatches → all get independent task_ids

WB1: execute_pre called before _execute_task (verified via mock ordering)
WB2: execute_post called only on success path (not called on error)
WB3: GLOBAL_CHAIN is an InterceptorChain instance
"""
import asyncio
import importlib
import sys
import uuid
from unittest.mock import AsyncMock, patch

import pytest

# Always import from the package so we exercise the real module
import core.interceptors as interceptors_pkg
from core.interceptors import (
    GLOBAL_CHAIN,
    InterceptorChain,
    dispatch_to_swarm,
    register_interceptor,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _is_valid_uuid4(value: str) -> bool:
    """Return True if *value* is a valid UUID-4 string."""
    try:
        parsed = uuid.UUID(value, version=4)
        # uuid.UUID normalises the string; check it round-trips cleanly
        return str(parsed) == value
    except ValueError:
        return False


# ---------------------------------------------------------------------------
# BB1 — No task_id supplied → UUID4 is assigned
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_bb1_no_task_id_gets_uuid4():
    """BB1: When task_id is absent, dispatch_to_swarm assigns a UUID4."""
    payload = {"prompt": "hello world"}
    result = await dispatch_to_swarm(payload)

    # UUID4 format: 32 hex digits + 4 hyphens = 36 chars
    task_id = result["task_id"]
    assert len(task_id) == 36, f"Expected 36 chars, got {len(task_id)}: {task_id!r}"
    assert task_id.count("-") == 4, f"Expected 4 hyphens, got {task_id.count('-')}"
    assert _is_valid_uuid4(task_id), f"Not a valid UUID4: {task_id!r}"


# ---------------------------------------------------------------------------
# BB2 — Successful execution → status == "completed" and task_id present
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_bb2_successful_dispatch_returns_completed():
    """BB2: Happy-path dispatch returns status 'completed' with a task_id."""
    payload = {"prompt": "test task"}
    result = await dispatch_to_swarm(payload)

    assert result["status"] == "completed"
    assert "task_id" in result
    assert result["task_id"]  # non-empty


# ---------------------------------------------------------------------------
# BB3 — _execute_task raises → status == "error" with correction payload
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_bb3_error_path_returns_error_status():
    """BB3: When _execute_task raises, dispatch_to_swarm returns status 'error'."""
    boom = RuntimeError("executor exploded")

    with patch(
        "core.interceptors._execute_task",
        new_callable=AsyncMock,
        side_effect=boom,
    ):
        payload = {"prompt": "will fail"}
        result = await dispatch_to_swarm(payload)

    assert result["status"] == "error"
    assert "task_id" in result
    assert "correction" in result


# ---------------------------------------------------------------------------
# BB4 — Multiple concurrent dispatches → all get independent task_ids
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_bb4_concurrent_dispatches_get_unique_task_ids():
    """BB4: Concurrent calls each receive a distinct, valid UUID4 task_id."""
    payloads = [{"prompt": f"task-{i}"} for i in range(10)]
    results = await asyncio.gather(*[dispatch_to_swarm(p) for p in payloads])

    task_ids = [r["task_id"] for r in results]
    # All must be unique
    assert len(set(task_ids)) == len(task_ids), "Duplicate task_ids detected"
    # All must be valid UUID4
    for tid in task_ids:
        assert _is_valid_uuid4(tid), f"Not a valid UUID4: {tid!r}"


# ---------------------------------------------------------------------------
# WB1 — execute_pre called BEFORE _execute_task
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_wb1_execute_pre_called_before_execute_task():
    """WB1: GLOBAL_CHAIN.execute_pre must be called before _execute_task."""
    call_order = []

    async def fake_pre(payload: dict) -> dict:
        call_order.append("pre")
        return payload

    async def fake_task(payload: dict) -> dict:
        call_order.append("task")
        return {"task_id": payload.get("task_id", "x"), "output": "", "status": "completed"}

    with (
        patch.object(GLOBAL_CHAIN, "execute_pre", side_effect=fake_pre),
        patch("core.interceptors._execute_task", side_effect=fake_task),
        # Prevent execute_post from failing due to mismatched mocks
        patch.object(GLOBAL_CHAIN, "execute_post", new_callable=AsyncMock),
    ):
        await dispatch_to_swarm({"prompt": "order test"})

    assert call_order.index("pre") < call_order.index("task"), (
        f"execute_pre must come before _execute_task; got order: {call_order}"
    )


# ---------------------------------------------------------------------------
# WB2 — execute_post called ONLY on success (not on error path)
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_wb2_execute_post_not_called_on_error():
    """WB2: execute_post must NOT be called when _execute_task raises."""
    boom = ValueError("forced failure")

    mock_post = AsyncMock()

    with (
        patch.object(GLOBAL_CHAIN, "execute_post", mock_post),
        patch(
            "core.interceptors._execute_task",
            new_callable=AsyncMock,
            side_effect=boom,
        ),
    ):
        result = await dispatch_to_swarm({"prompt": "should error"})

    assert result["status"] == "error"
    mock_post.assert_not_called()


@pytest.mark.asyncio
async def test_wb2_execute_post_called_on_success():
    """WB2 (positive): execute_post IS called on the success path."""
    mock_post = AsyncMock()

    with patch.object(GLOBAL_CHAIN, "execute_post", mock_post):
        result = await dispatch_to_swarm({"prompt": "should succeed"})

    assert result["status"] == "completed"
    mock_post.assert_called_once()


# ---------------------------------------------------------------------------
# WB3 — GLOBAL_CHAIN is an InterceptorChain instance
# ---------------------------------------------------------------------------

def test_wb3_global_chain_is_interceptor_chain():
    """WB3: GLOBAL_CHAIN must be an instance of InterceptorChain."""
    assert isinstance(GLOBAL_CHAIN, InterceptorChain), (
        f"Expected InterceptorChain, got {type(GLOBAL_CHAIN)}"
    )
