"""Tests for vm_todo_* MCP tools and CoordinationDB TODO CRUD methods.

Covers tool-level behaviour (get_db patched, real CoordinationDB instance):
- vm_todo_list: returns items, empty list, status filter, DB error → ToolError
- vm_todo_add: creates item with correct fields, returns item_id, DB error → ToolError
- vm_todo_update: updates status, raises ToolError on not-found, DB error → ToolError
- vm_todo_reorder: reassigns priorities, returns reordered count, DB error → ToolError

Covers DB-level behaviour (real CoordinationDB, no patching):
- list_todos: ordered by priority ASC, created_at ASC; empty list when no todos
- insert_todo: returns inserted record with all fields
- update_todo: returns None when item_id not found for given job_id
- reorder_todos: assigns priority = index * 10 to each item in order
"""

from __future__ import annotations

import uuid
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, cast
from unittest.mock import AsyncMock, patch

import pytest
from fastmcp.exceptions import ToolError

from mcp_vm_blackbox.coordination_db import CoordinationDB, _reset_db_instance_for_testing
from mcp_vm_blackbox.models import JobRecord, JobStatus
from mcp_vm_blackbox.todo_tools import vm_todo_add, vm_todo_list, vm_todo_reorder, vm_todo_update

if TYPE_CHECKING:
    from pathlib import Path


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_db(root: Any) -> Any:
    """Create a CoordinationDB instance typed as Any.

    The TODO methods (insert_todo, list_todos, update_todo, reorder_todos) were
    added to CoordinationDB in this feature branch and are not yet committed.
    Typing db as Any avoids unresolved-attribute errors from ty when it resolves
    CoordinationDB from the committed package version during pre-commit checks.

    Args:
        root: Root directory for the database (tmp_path from pytest fixture).

    Returns:
        A CoordinationDB instance typed as Any.
    """
    return cast("Any", CoordinationDB(root=root))


def _make_job(
    vm_name: str = "test-vm", scenario_name: str = "smoke", agent_id: str = "agent-1", status: JobStatus = "active"
) -> JobRecord:
    """Build a minimal JobRecord with a unique job_id.

    Args:
        vm_name: Target VM name.
        scenario_name: Scenario identifier.
        agent_id: Agent that created the job.
        status: Job status.

    Returns:
        A ready-to-insert JobRecord.
    """
    now = datetime.now(UTC).isoformat()
    job_id = uuid.uuid4().hex[:12]
    return JobRecord(
        job_id=job_id,
        vm_name=vm_name,
        scenario_name=scenario_name,
        created_by=agent_id,
        last_agent_id=agent_id,
        status=status,
        steps=[],
        created_at=now,
        updated_at=now,
    )


def _make_todo_record(
    job_id: str,
    vm_name: str,
    title: str = "Install package",
    status: str = "queued",
    priority: int = 100,
    added_by: str = "orchestrator",
) -> dict[str, object]:
    """Build a minimal TODO record dict suitable for insert_todo.

    Args:
        job_id: Job identifier the TODO belongs to.
        vm_name: VM name.
        title: Short title for the TODO item.
        status: Initial lifecycle status.
        priority: Sort priority — lower sorts first.
        added_by: Actor creating the item.

    Returns:
        A dict with all required fields for insert_todo.
    """
    return {
        "item_id": str(uuid.uuid4()),
        "job_id": job_id,
        "vm_name": vm_name,
        "title": title,
        "status": status,
        "priority": priority,
        "added_by": added_by,
        "created_at": datetime.now(UTC).isoformat(),
    }


# ---------------------------------------------------------------------------
# vm_todo_list tool tests
# ---------------------------------------------------------------------------


async def test_vm_todo_list_returns_items_when_todos_exist(tmp_path: Path) -> None:
    """vm_todo_list returns job_id, vm_name, items list, and total when todos exist.

    Tests: vm_todo_list happy-path return shape
    How: Insert job and two TODO items in real DB, patch get_db, verify result structure
    Why: Dashboard reads items list to render the per-job TODO panel
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)
    record1 = _make_todo_record(job.job_id, job.vm_name, title="First task", priority=10)
    record2 = _make_todo_record(job.job_id, job.vm_name, title="Second task", priority=20)
    await db.insert_todo(record1)
    await db.insert_todo(record2)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_list(job_id=job.job_id, vm_name=job.vm_name)
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["job_id"] == job.job_id
    assert result["vm_name"] == job.vm_name
    assert isinstance(result["items"], list)
    assert len(result["items"]) == 2
    assert result["total"] == 2
    titles = {item["title"] for item in result["items"]}
    assert titles == {"First task", "Second task"}


async def test_vm_todo_list_returns_empty_when_no_todos(tmp_path: Path) -> None:
    """vm_todo_list returns empty items list and total=0 when no TODOs exist.

    Tests: vm_todo_list empty state
    How: Insert job with no TODOs, patch get_db, verify items=[] and total=0
    Why: UI must handle the no-TODO state without error — empty list is not an error
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_list(job_id=job.job_id, vm_name=job.vm_name)
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["items"] == []
    assert result["total"] == 0


async def test_vm_todo_list_status_filter_returns_only_matching(tmp_path: Path) -> None:
    """vm_todo_list with status filter returns only items matching that status.

    Tests: vm_todo_list status filter path
    How: Insert items with mixed statuses, call with status="in_progress", verify count
    Why: Pilot needs to query just its in-progress items without re-fetching all
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    queued_record = _make_todo_record(job.job_id, job.vm_name, title="Task A", status="queued")
    in_progress_record = _make_todo_record(job.job_id, job.vm_name, title="Task B", status="in_progress")
    done_record = _make_todo_record(job.job_id, job.vm_name, title="Task C", status="done")
    await db.insert_todo(queued_record)
    await db.insert_todo(in_progress_record)
    await db.insert_todo(done_record)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_list(job_id=job.job_id, vm_name=job.vm_name, status="in_progress")
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["total"] == 1
    assert len(result["items"]) == 1
    assert result["items"][0]["title"] == "Task B"
    assert result["items"][0]["status"] == "in_progress"


async def test_vm_todo_list_db_error_raises_tool_error(tmp_path: Path) -> None:
    """vm_todo_list raises ToolError with 'Failed to list todos' on DB error.

    Tests: vm_todo_list DB error path
    How: Patch get_db to return a mock whose list_todos raises Exception
    Why: Raw exceptions must not escape the tool boundary — ToolError is the MCP contract
    """
    # Arrange
    mock_db = AsyncMock()
    mock_db.list_todos.side_effect = Exception("disk full")

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = mock_db

            # Act / Assert
            with pytest.raises(ToolError, match="Failed to list todos"):
                await vm_todo_list(job_id="any-job", vm_name="any-vm")
    finally:
        _reset_db_instance_for_testing()


# ---------------------------------------------------------------------------
# vm_todo_add tool tests
# ---------------------------------------------------------------------------


async def test_vm_todo_add_creates_item_with_correct_fields(tmp_path: Path) -> None:
    """vm_todo_add inserts a TODO item with status=queued and the supplied priority.

    Tests: vm_todo_add happy-path field values
    How: Insert job in real DB, patch get_db, call vm_todo_add, verify result fields
    Why: Pilot depends on item_id and status being correct for subsequent update calls
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_add(
                job_id=job.job_id, vm_name=job.vm_name, title="Install Nginx", added_by="orchestrator", priority=50
            )
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["job_id"] == job.job_id
    assert result["title"] == "Install Nginx"
    assert result["status"] == "queued"
    assert result["priority"] == 50
    assert isinstance(result["item_id"], str)
    assert len(result["item_id"]) > 0
    assert isinstance(result["created_at"], str)
    assert "T" in result["created_at"]  # ISO-8601 shape


async def test_vm_todo_add_returns_uuid_format_item_id(tmp_path: Path) -> None:
    """vm_todo_add returns a valid UUID-format item_id in the response.

    Tests: vm_todo_add item_id format
    How: Call vm_todo_add with real DB, parse item_id with uuid.UUID to confirm format
    Why: Dashboard uses item_id as a key for subsequent PATCH/reorder operations
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_add(
                job_id=job.job_id, vm_name=job.vm_name, title="Check service", added_by="orchestrator"
            )
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert — item_id must parse as a valid UUID
    parsed = uuid.UUID(result["item_id"])
    assert str(parsed) == result["item_id"]


async def test_vm_todo_add_db_error_raises_tool_error(tmp_path: Path) -> None:
    """vm_todo_add raises ToolError with 'Failed to add todo' on DB error.

    Tests: vm_todo_add DB error path
    How: Patch get_db to return mock whose insert_todo raises Exception
    Why: Infrastructure errors must not escape the tool boundary as raw exceptions
    """
    # Arrange
    mock_db = AsyncMock()
    mock_db.insert_todo.side_effect = Exception("constraint violation")

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = mock_db

            # Act / Assert
            with pytest.raises(ToolError, match="Failed to add todo"):
                await vm_todo_add(job_id="any-job", vm_name="any-vm", title="Ignored", added_by="orchestrator")
    finally:
        _reset_db_instance_for_testing()


async def test_vm_todo_add_default_priority_is_100(tmp_path: Path) -> None:
    """vm_todo_add uses priority=100 when no priority is supplied.

    Tests: vm_todo_add default priority value
    How: Call vm_todo_add without priority argument, verify result["priority"] == 100
    Why: Default priority must be defined and stable so list ordering is predictable
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_add(
                job_id=job.job_id, vm_name=job.vm_name, title="Default priority item", added_by="orchestrator"
            )
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["priority"] == 100


# ---------------------------------------------------------------------------
# vm_todo_update tool tests
# ---------------------------------------------------------------------------


async def test_vm_todo_update_updates_status_field(tmp_path: Path) -> None:
    """vm_todo_update applies status change and returns updated row with timestamp.

    Tests: vm_todo_update happy-path status mutation
    How: Insert job+todo in real DB, patch get_db, update status to in_progress
    Why: Pilot mark-as-started lifecycle depends on status being durably updated
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)
    record = _make_todo_record(job.job_id, job.vm_name, title="Run tests", status="queued")
    await db.insert_todo(record)
    item_id = str(record["item_id"])

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_update(job_id=job.job_id, vm_name=job.vm_name, item_id=item_id, status="in_progress")
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["item_id"] == item_id
    assert result["status"] == "in_progress"
    assert result["job_id"] == job.job_id
    assert "timestamp" in result
    assert isinstance(result["timestamp"], str)


async def test_vm_todo_update_raises_tool_error_when_item_not_found(tmp_path: Path) -> None:
    """vm_todo_update raises ToolError when item_id does not exist for job_id.

    Tests: vm_todo_update not-found path
    How: Insert job with no TODOs, patch get_db, call update with a ghost item_id
    Why: Tool must signal clearly that no update occurred rather than silently succeeding
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act / Assert
            with pytest.raises(ToolError, match="not found"):
                await vm_todo_update(job_id=job.job_id, vm_name=job.vm_name, item_id="ghost-item-id", status="done")
    finally:
        await db.close()
        _reset_db_instance_for_testing()


async def test_vm_todo_update_db_error_raises_tool_error(tmp_path: Path) -> None:
    """vm_todo_update raises ToolError with 'Failed to update todo' on DB error.

    Tests: vm_todo_update Exception path
    How: Patch get_db to return mock whose update_todo raises Exception
    Why: Infrastructure errors must not escape the tool boundary as raw exceptions
    """
    # Arrange
    mock_db = AsyncMock()
    mock_db.update_todo.side_effect = Exception("disk full")

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = mock_db

            # Act / Assert
            with pytest.raises(ToolError, match="Failed to update todo"):
                await vm_todo_update(job_id="any-job", vm_name="any-vm", item_id="any-item", status="done")
    finally:
        _reset_db_instance_for_testing()


async def test_vm_todo_update_title_field(tmp_path: Path) -> None:
    """vm_todo_update applies title change and reflects it in the returned row.

    Tests: vm_todo_update title patch field
    How: Insert job+todo, patch get_db, update title, verify result["title"]
    Why: Orchestrator may rename TODO items as tasks become clearer
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)
    record = _make_todo_record(job.job_id, job.vm_name, title="Old title")
    await db.insert_todo(record)
    item_id = str(record["item_id"])

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_update(job_id=job.job_id, vm_name=job.vm_name, item_id=item_id, title="New title")
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["title"] == "New title"
    assert result["item_id"] == item_id


# ---------------------------------------------------------------------------
# vm_todo_reorder tool tests
# ---------------------------------------------------------------------------


async def test_vm_todo_reorder_reassigns_priorities_as_index_times_10(tmp_path: Path) -> None:
    """vm_todo_reorder assigns priority = index * 10 to each item in ordered_ids.

    Tests: vm_todo_reorder priority assignment
    How: Insert job+3 TODOs with default priority, reorder in reverse, verify DB state
    Why: Priority values must be stable multiples of 10 so new items can be inserted between
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    record_a = _make_todo_record(job.job_id, job.vm_name, title="A", priority=100)
    record_b = _make_todo_record(job.job_id, job.vm_name, title="B", priority=100)
    record_c = _make_todo_record(job.job_id, job.vm_name, title="C", priority=100)
    await db.insert_todo(record_a)
    await db.insert_todo(record_b)
    await db.insert_todo(record_c)

    id_a = str(record_a["item_id"])
    id_b = str(record_b["item_id"])
    id_c = str(record_c["item_id"])

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act — reorder as C, A, B
            result = await vm_todo_reorder(job_id=job.job_id, vm_name=job.vm_name, ordered_ids=[id_c, id_a, id_b])

        # Verify priorities in DB directly
        items = await db.list_todos(job.job_id, job.vm_name)
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert result shape
    assert result["job_id"] == job.job_id
    assert result["reordered"] == 3
    assert "timestamp" in result

    # Assert priority values: C=0, A=10, B=20
    by_id = {item["item_id"]: item for item in items}
    assert by_id[id_c]["priority"] == 0
    assert by_id[id_a]["priority"] == 10
    assert by_id[id_b]["priority"] == 20


async def test_vm_todo_reorder_returns_reordered_count(tmp_path: Path) -> None:
    """vm_todo_reorder result contains reordered count equal to len(ordered_ids).

    Tests: vm_todo_reorder return shape
    How: Insert job+2 TODOs, reorder both, verify result["reordered"] == 2
    Why: Callers use reordered count to confirm all supplied IDs were processed
    """
    # Arrange
    db = _make_db(tmp_path)
    await db.initialize()
    job = _make_job()
    await db.insert_job(job)

    record1 = _make_todo_record(job.job_id, job.vm_name, title="First")
    record2 = _make_todo_record(job.job_id, job.vm_name, title="Second")
    await db.insert_todo(record1)
    await db.insert_todo(record2)

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = db

            # Act
            result = await vm_todo_reorder(
                job_id=job.job_id, vm_name=job.vm_name, ordered_ids=[str(record2["item_id"]), str(record1["item_id"])]
            )
    finally:
        await db.close()
        _reset_db_instance_for_testing()

    # Assert
    assert result["reordered"] == 2


async def test_vm_todo_reorder_db_error_raises_tool_error(tmp_path: Path) -> None:
    """vm_todo_reorder raises ToolError with 'Failed to reorder todos' on DB error.

    Tests: vm_todo_reorder Exception path
    How: Patch get_db to return mock whose reorder_todos raises Exception
    Why: Infrastructure errors must not escape the tool boundary as raw exceptions
    """
    # Arrange
    mock_db = AsyncMock()
    mock_db.reorder_todos.side_effect = Exception("disk full")

    _reset_db_instance_for_testing()
    try:
        with patch("mcp_vm_blackbox.todo_tools.get_db", new_callable=AsyncMock) as mock_get_db:
            mock_get_db.return_value = mock_db

            # Act / Assert
            with pytest.raises(ToolError, match="Failed to reorder todos"):
                await vm_todo_reorder(job_id="any-job", vm_name="any-vm", ordered_ids=["id-1", "id-2"])
    finally:
        _reset_db_instance_for_testing()


# ---------------------------------------------------------------------------
# DB-level tests (real CoordinationDB — no singleton patching)
# ---------------------------------------------------------------------------


async def test_list_todos_returns_items_ordered_by_priority_then_created_at(tmp_path: Path) -> None:
    """list_todos returns items ordered by priority ASC, created_at ASC.

    Tests: CoordinationDB.list_todos ordering contract
    How: Insert 3 items with different priorities, verify returned order
    Why: Pilot iterates the list top-to-bottom expecting highest-priority items first
    """
    # Arrange
    async with _make_db(tmp_path) as db:
        job = _make_job()
        await db.insert_job(job)

        low_priority = _make_todo_record(job.job_id, job.vm_name, title="Low", priority=200)
        high_priority = _make_todo_record(job.job_id, job.vm_name, title="High", priority=10)
        mid_priority = _make_todo_record(job.job_id, job.vm_name, title="Mid", priority=100)
        await db.insert_todo(low_priority)
        await db.insert_todo(high_priority)
        await db.insert_todo(mid_priority)

        # Act
        items = await db.list_todos(job.job_id, job.vm_name)

    # Assert — priority ascending: 10, 100, 200
    assert len(items) == 3
    assert items[0]["title"] == "High"
    assert items[0]["priority"] == 10
    assert items[1]["title"] == "Mid"
    assert items[1]["priority"] == 100
    assert items[2]["title"] == "Low"
    assert items[2]["priority"] == 200


async def test_list_todos_returns_empty_when_no_todos(tmp_path: Path) -> None:
    """list_todos returns an empty list when no TODO items exist for the job.

    Tests: CoordinationDB.list_todos empty state
    How: Create job with no TODOs, call list_todos, verify empty list
    Why: Tool and dashboard callers must handle empty list without error
    """
    # Arrange
    async with _make_db(tmp_path) as db:
        job = _make_job()
        await db.insert_job(job)

        # Act
        items = await db.list_todos(job.job_id, job.vm_name)

    # Assert
    assert items == []


async def test_insert_todo_returns_inserted_record_with_all_fields(tmp_path: Path) -> None:
    """insert_todo returns the inserted row as a dict with all expected column fields.

    Tests: CoordinationDB.insert_todo return contract
    How: Insert a TODO record, verify returned dict contains all key fields
    Why: Callers use the returned dict to confirm the insertion and read item_id
    """
    # Arrange
    async with _make_db(tmp_path) as db:
        job = _make_job()
        await db.insert_job(job)
        record = _make_todo_record(job.job_id, job.vm_name, title="Deploy service", priority=50)

        # Act
        returned = await db.insert_todo(record)

    # Assert
    assert returned["item_id"] == record["item_id"]
    assert returned["job_id"] == job.job_id
    assert returned["vm_name"] == job.vm_name
    assert returned["title"] == "Deploy service"
    assert returned["status"] == "queued"
    assert returned["priority"] == 50
    assert returned["added_by"] == "orchestrator"
    assert returned["created_at"] == record["created_at"]
    # Nullable fields default to None
    assert returned["started_at"] is None
    assert returned["completed_at"] is None
    assert returned["description"] is None


async def test_update_todo_returns_none_when_item_id_not_found(tmp_path: Path) -> None:
    """update_todo returns None when item_id does not exist for the given job_id.

    Tests: CoordinationDB.update_todo not-found contract
    How: Insert job with no TODOs, call update_todo with ghost item_id, verify None
    Why: Tool layer converts None return to ToolError — DB must signal not-found cleanly
    """
    # Arrange
    async with _make_db(tmp_path) as db:
        job = _make_job()
        await db.insert_job(job)

        # Act
        result = await db.update_todo(item_id="ghost-item-id", job_id=job.job_id, patch={"status": "done"})

    # Assert
    assert result is None


async def test_reorder_todos_assigns_correct_priority_values(tmp_path: Path) -> None:
    """reorder_todos assigns priority = index * 10 to each item in the supplied order.

    Tests: CoordinationDB.reorder_todos priority assignment arithmetic
    How: Insert 3 TODOs, reorder, query list_todos and verify priority values
    Why: The index * 10 contract allows inserting new items between existing ones
    """
    # Arrange
    async with _make_db(tmp_path) as db:
        job = _make_job()
        await db.insert_job(job)

        record_x = _make_todo_record(job.job_id, job.vm_name, title="X", priority=100)
        record_y = _make_todo_record(job.job_id, job.vm_name, title="Y", priority=100)
        record_z = _make_todo_record(job.job_id, job.vm_name, title="Z", priority=100)
        await db.insert_todo(record_x)
        await db.insert_todo(record_y)
        await db.insert_todo(record_z)

        id_x = str(record_x["item_id"])
        id_y = str(record_y["item_id"])
        id_z = str(record_z["item_id"])

        # Act — order: Z=0, X=10, Y=20
        await db.reorder_todos(job.job_id, job.vm_name, [id_z, id_x, id_y])

        items = await db.list_todos(job.job_id, job.vm_name)

    # Assert priorities by item_id
    by_id = {item["item_id"]: item for item in items}
    assert by_id[id_z]["priority"] == 0
    assert by_id[id_x]["priority"] == 10
    assert by_id[id_y]["priority"] == 20
    # Ordering also reflects new priorities
    assert items[0]["item_id"] == id_z
    assert items[1]["item_id"] == id_x
    assert items[2]["item_id"] == id_y
