"""Integration tests for the workflow system — loader, runner, state persistence."""

from __future__ import annotations

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

import pytest

from loom.config import SkillsConfig
from loom.graph import store
from loom.graph.task import Priority, Task, TaskStatus
from loom.ids import task_id as gen_task_id
from loom.workflows.loader import (
    BUILTIN_DIR,
    WorkflowDefinition,
    WorkflowStep,
    discover_workflows,
    load_workflow,
    parse_workflow_file,
)
from loom.workflows.runner import run_workflow, resume_workflow


# -- Helpers --


def _mock_anthropic(output: dict | None = None):
    """Create a mock AsyncAnthropic that returns the given output as JSON."""
    if output is None:
        output = {"result": "mock output"}

    mock_response = MagicMock()
    mock_response.content = [MagicMock(text=json.dumps(output))]
    mock_response.usage = MagicMock(input_tokens=100, output_tokens=200)

    mock_client = AsyncMock()
    mock_client.messages.create = AsyncMock(return_value=mock_response)

    return MagicMock(return_value=mock_client)


# -- Loader tests --


def test_discover_builtin_workflows():
    """Both built-in workflows (ship_feature, debug_and_fix) are discovered."""
    workflows = discover_workflows()
    assert "ship_feature" in workflows
    assert "debug_and_fix" in workflows
    assert len(workflows) >= 2


def test_parse_workflow_file():
    """YAML parsing produces correct steps with proper types."""
    path = BUILTIN_DIR / "ship_feature.yaml"
    wf = parse_workflow_file(path)
    assert wf.name == "ship_feature"
    assert wf.version == "1.0"
    assert "goal" in wf.inputs
    assert "context" in wf.inputs
    assert len(wf.steps) == 4

    # Check step types
    step_types = [(s.name, s.type) for s in wf.steps]
    assert ("write_spec", "skill") in step_types
    assert ("decompose", "skill") in step_types
    assert ("confirm", "confirm") in step_types
    assert ("write_tasks", "action") in step_types

    # Verify the confirm step has a message
    confirm_step = next(s for s in wf.steps if s.name == "confirm")
    assert confirm_step.message is not None

    # Verify skill steps reference skills
    write_spec_step = next(s for s in wf.steps if s.name == "write_spec")
    assert write_spec_step.skill == "write_spec"
    assert write_spec_step.outputs_key == "spec"


def test_load_workflow_not_found():
    """LookupError raised for unknown workflow name."""
    with pytest.raises(LookupError, match="Workflow not found"):
        load_workflow("nonexistent_workflow_xyz")


# -- Runner tests --


@pytest.mark.asyncio
async def test_workflow_skill_step(pool, redis_conn, project):
    """A workflow with a single skill step executes and stores output."""
    test_wf = WorkflowDefinition(
        name="test_skill_wf",
        inputs=["goal"],
        steps=[
            WorkflowStep(
                name="step1",
                skill="write_spec",
                inputs={"goal": "{{ goal }}", "context": "", "constraints": ""},
                outputs_key="result",
            ),
        ],
    )
    config = SkillsConfig(api_key="test-key")
    mock_output = {"spec": {"overview": "A test spec", "requirements": ["R1"]}}

    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic(mock_output)):
        result = await run_workflow(
            test_wf,
            inputs={"goal": "Build something"},
            config=config,
            pool=pool,
            redis=redis_conn,
            project_id=project,
        )

    assert result["status"] == "completed"
    assert result["outputs"]["result"] == mock_output


@pytest.mark.asyncio
async def test_workflow_confirm_pauses(pool, redis_conn, project):
    """A workflow with a confirm step pauses, workflow run status is 'paused' in DB."""
    test_wf = WorkflowDefinition(
        name="test_confirm_wf",
        inputs=["goal"],
        steps=[
            WorkflowStep(
                name="do_work",
                skill="write_spec",
                inputs={"goal": "{{ goal }}", "context": "", "constraints": ""},
                outputs_key="result",
            ),
            WorkflowStep(
                name="review",
                type="confirm",
                message="Please review before proceeding.",
            ),
            WorkflowStep(
                name="final_step",
                skill="write_spec",
                inputs={"goal": "{{ goal }}", "context": "", "constraints": ""},
                outputs_key="final",
            ),
        ],
    )
    config = SkillsConfig(api_key="test-key")
    mock_output = {"spec": {"overview": "Test"}}

    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic(mock_output)):
        result = await run_workflow(
            test_wf,
            inputs={"goal": "Test goal"},
            config=config,
            pool=pool,
            redis=redis_conn,
            project_id=project,
        )

    assert result["status"] == "paused"
    assert result["current_step"] == "review"

    # Verify in DB
    run = await store.get_workflow_run(pool, result["id"])
    assert run["status"] == "paused"
    assert run["current_step"] == "review"


@pytest.mark.asyncio
async def test_workflow_resume_after_confirm(pool, redis_conn, project):
    """Paused workflow can be resumed and completes remaining steps."""
    test_wf = WorkflowDefinition(
        name="test_resume_wf",
        inputs=["goal"],
        steps=[
            WorkflowStep(
                name="first_step",
                skill="write_spec",
                inputs={"goal": "{{ goal }}", "context": "", "constraints": ""},
                outputs_key="step1_out",
            ),
            WorkflowStep(
                name="confirm_step",
                type="confirm",
                message="Review results.",
            ),
            WorkflowStep(
                name="second_step",
                skill="write_spec",
                inputs={"goal": "{{ goal }}", "context": "", "constraints": ""},
                outputs_key="step2_out",
            ),
        ],
    )
    config = SkillsConfig(api_key="test-key")
    mock_output = {"spec": {"overview": "Test output"}}

    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic(mock_output)):
        # Run until paused
        paused = await run_workflow(
            test_wf,
            inputs={"goal": "Test resume"},
            config=config,
            pool=pool,
            redis=redis_conn,
            project_id=project,
        )

    assert paused["status"] == "paused"
    run_id = paused["id"]

    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic(mock_output)):
        # Resume the workflow (pass workflow def since it's an inline test definition)
        completed = await resume_workflow(
            run_id,
            pool=pool,
            redis=redis_conn,
            config=config,
            project_id=project,
            workflow=test_wf,
        )

    assert completed["status"] == "completed"
    assert "step2_out" in completed["outputs"]
    assert completed["outputs"]["step2_out"] == mock_output


@pytest.mark.asyncio
async def test_workflow_state_persisted(pool, redis_conn, project):
    """Workflow state (outputs from steps) is persisted to the workflow_runs table."""
    test_wf = WorkflowDefinition(
        name="test_state_wf",
        inputs=["val"],
        steps=[
            WorkflowStep(
                name="step_a",
                skill="write_spec",
                inputs={"goal": "{{ val }}", "context": "", "constraints": ""},
                outputs_key="a_out",
            ),
            WorkflowStep(
                name="step_b",
                skill="write_spec",
                inputs={"goal": "{{ val }}", "context": "", "constraints": ""},
                outputs_key="b_out",
            ),
        ],
    )
    config = SkillsConfig(api_key="test-key")
    mock_a = {"spec": {"from_step": "a"}}
    mock_b = {"spec": {"from_step": "b"}}

    # Both steps will get the same mock (we just verify state structure)
    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic(mock_a)):
        result = await run_workflow(
            test_wf,
            inputs={"val": "state test"},
            config=config,
            pool=pool,
            redis=redis_conn,
            project_id=project,
        )

    # Verify state is persisted in DB
    run = await store.get_workflow_run(pool, result["id"])
    assert run["status"] == "completed"
    state = run["state"]
    assert "outputs" in state
    assert "a_out" in state["outputs"]
    assert "b_out" in state["outputs"]
    # The workflow inputs should also be in state
    assert state.get("val") == "state test"


@pytest.mark.asyncio
async def test_workflow_step_log(pool, redis_conn, project):
    """Each executed step is logged to the workflow_steps table."""
    test_wf = WorkflowDefinition(
        name="test_log_wf",
        inputs=["x"],
        steps=[
            WorkflowStep(
                name="s1",
                skill="write_spec",
                inputs={"goal": "{{ x }}", "context": "", "constraints": ""},
                outputs_key="out1",
            ),
            WorkflowStep(
                name="s2",
                skill="write_spec",
                inputs={"goal": "{{ x }}", "context": "", "constraints": ""},
                outputs_key="out2",
            ),
        ],
    )
    config = SkillsConfig(api_key="test-key")

    with patch("loom.skills.runner.AsyncAnthropic", _mock_anthropic({"result": "ok"})):
        result = await run_workflow(
            test_wf,
            inputs={"x": "log test"},
            config=config,
            pool=pool,
            redis=redis_conn,
            project_id=project,
        )

    # Check workflow_steps table
    run_id = result["id"]
    async with pool.acquire() as conn:
        rows = await conn.fetch(
            "SELECT * FROM workflow_steps WHERE run_id = $1::uuid ORDER BY id",
            run_id,
        )

    # Each step has a running + completed entry
    step_names = [r["step_name"] for r in rows]
    assert "s1" in step_names
    assert "s2" in step_names

    # Verify completed entries exist
    completed = [r for r in rows if r["status"] == "completed"]
    assert len(completed) == 2


@pytest.mark.asyncio
async def test_workflow_failure_handling(pool, redis_conn, project):
    """A step that raises an exception marks the workflow as 'failed'."""
    test_wf = WorkflowDefinition(
        name="test_fail_wf",
        inputs=["x"],
        steps=[
            WorkflowStep(
                name="failing_step",
                skill="write_spec",
                inputs={"goal": "{{ x }}", "context": "", "constraints": ""},
                outputs_key="out",
            ),
        ],
    )
    config = SkillsConfig(api_key="test-key")

    # Make the Anthropic client raise an error
    mock_client = AsyncMock()
    mock_client.messages.create = AsyncMock(side_effect=RuntimeError("LLM exploded"))
    mock_cls = MagicMock(return_value=mock_client)

    with patch("loom.skills.runner.AsyncAnthropic", mock_cls):
        with pytest.raises(RuntimeError, match="LLM exploded"):
            await run_workflow(
                test_wf,
                inputs={"x": "fail test"},
                config=config,
                pool=pool,
                redis=redis_conn,
                project_id=project,
            )

    # Verify the workflow run is marked as failed in DB
    runs = await store.list_workflow_runs(pool, project, status="failed")
    assert len(runs) == 1
    assert "LLM exploded" in runs[0]["error"]


@pytest.mark.asyncio
async def test_workflow_action_load_task(pool, redis_conn, project):
    """The load_task action retrieves a task and stores it in workflow state."""
    # First create a real task in the DB
    task = Task(
        id=gen_task_id(),
        project_id=project,
        title="Task to load",
        status=TaskStatus.FAILED,
        priority=Priority.P1,
        context={"description": "Something went wrong"},
        output={"error": "Connection refused"},
    )
    await store.create_task(pool, task)

    test_wf = WorkflowDefinition(
        name="test_load_task_wf",
        inputs=["task_id"],
        steps=[
            WorkflowStep(
                name="load",
                type="action",
                action="load_task",
                inputs={"task_id": "{{ task_id }}"},
                outputs_key="task",
            ),
        ],
    )
    config = SkillsConfig(api_key="test-key")

    result = await run_workflow(
        test_wf,
        inputs={"task_id": task.id},
        config=config,
        pool=pool,
        redis=redis_conn,
        project_id=project,
    )

    assert result["status"] == "completed"
    loaded_task = result["outputs"]["task"]
    assert loaded_task["id"] == task.id
    assert loaded_task["title"] == "Task to load"
    assert loaded_task["output"]["error"] == "Connection refused"
