"""Tests for work item status transitions.

Systematic coverage of the work item state machine:
- All valid transition paths between statuses
- Plan approval gate: pending_approval → approved_for_dispatch
- Guard conditions (approve rejects wrong status)
- Edge cases: nonexistent items, idempotent transitions, count consistency
"""

import pytest

from greymatter_core.db import SQLiteDatabase
from greymatter_core.work_items import WorkItemManager


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def db():
    database = SQLiteDatabase(":memory:")
    yield database
    database.close()


@pytest.fixture
def work(db):
    return WorkItemManager(db)


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

def _create_at_status(work: WorkItemManager, status: str, title: str = "Test item"):
    """Create a work item and advance it to the given status."""
    item = work.create(title=title)
    transitions = {
        "queued": [],
        "in_progress": ["in_progress"],
        "assigned": ["assigned"],
        "pending_review": ["in_progress", "pending_review"],
        "needs_revision": ["in_progress", "pending_review", "needs_revision"],
        "approved": ["in_progress", "pending_review", "approved"],
        "failed": ["in_progress", "failed"],
        "completed": [],  # use work.complete() instead
        "pending_approval": [],  # use mark_pending_approval()
        "approved_for_dispatch": [],  # use mark_pending_approval() + approve()
    }
    for s in transitions.get(status, []):
        work.update_status(item["id"], s)
    if status == "completed":
        work.complete(item["id"])
    elif status == "pending_approval":
        work.mark_pending_approval(item["id"])
    elif status == "approved_for_dispatch":
        work.mark_pending_approval(item["id"])
        work.approve(item["id"])
    return work.get(item["id"])


# ---------------------------------------------------------------------------
# 1. Core Status Transitions
# ---------------------------------------------------------------------------

class TestCoreTransitions:
    """Tests every transition in the core workflow path."""

    def test_queued_to_in_progress(self, work):
        item = work.create(title="Start work")
        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

    def test_in_progress_to_pending_review(self, work):
        item = _create_at_status(work, "in_progress")
        result = work.update_status(item["id"], "pending_review")
        assert result["status"] == "pending_review"

    def test_pending_review_to_approved(self, work):
        item = _create_at_status(work, "pending_review")
        result = work.update_status(item["id"], "approved")
        assert result["status"] == "approved"

    def test_pending_review_to_needs_revision(self, work):
        item = _create_at_status(work, "pending_review")
        result = work.update_status(item["id"], "needs_revision")
        assert result["status"] == "needs_revision"

    def test_needs_revision_to_in_progress(self, work):
        item = _create_at_status(work, "needs_revision")
        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

    def test_approved_to_completed(self, work):
        item = _create_at_status(work, "approved")
        result = work.complete(item["id"])
        assert result["status"] == "completed"
        assert result["completed_at"] is not None

    def test_in_progress_to_completed_direct(self, work):
        """Direct completion without review is allowed."""
        item = _create_at_status(work, "in_progress")
        result = work.complete(item["id"])
        assert result["status"] == "completed"

    def test_queued_to_completed_direct(self, work):
        """Direct completion from queued is allowed (e.g., cancelled or trivial work)."""
        item = work.create(title="Quick task")
        result = work.complete(item["id"])
        assert result["status"] == "completed"
        assert result["completed_at"] is not None


# ---------------------------------------------------------------------------
# 2. Plan Approval Gate
# ---------------------------------------------------------------------------

class TestPlanApprovalGate:
    """Tests the plan-mode approval workflow:
    queued → pending_approval → approved_for_dispatch.
    """

    # --- mark_pending_approval ---

    def test_mark_pending_approval_from_queued(self, work):
        """Queued item transitions to pending_approval."""
        item = work.create(title="Needs approval")
        result = work.mark_pending_approval(item["id"])
        assert result is not None
        assert result["status"] == "pending_approval"

    def test_mark_pending_approval_nonexistent(self, work):
        """Marking a nonexistent item returns None."""
        result = work.mark_pending_approval("wi-does-not-exist")
        assert result is None

    def test_mark_pending_approval_preserves_metadata(self, work):
        """Transitioning to pending_approval preserves all other fields."""
        item = work.create(
            title="Detailed item",
            description="Has metadata",
            priority=25,
            domain_tags="backend,api",
            project="greymatter",
            feature="auth",
        )
        result = work.mark_pending_approval(item["id"])
        assert result["title"] == "Detailed item"
        assert result["description"] == "Has metadata"
        assert result["priority"] == 25
        assert result["domain_tags"] == "backend,api"
        assert result["project"] == "greymatter"
        assert result["feature"] == "auth"

    # --- approve ---

    def test_approve_from_pending_approval(self, work):
        """Item in pending_approval transitions to approved_for_dispatch."""
        item = work.create(title="Approve me")
        work.mark_pending_approval(item["id"])
        result = work.approve(item["id"])
        assert result is not None
        assert result["status"] == "approved_for_dispatch"

    def test_approve_rejects_queued_status(self, work):
        """approve() returns None when item is still queued."""
        item = work.create(title="Still queued")
        result = work.approve(item["id"])
        assert result is None
        # Status unchanged
        assert work.get(item["id"])["status"] == "queued"

    def test_approve_rejects_in_progress_status(self, work):
        """approve() returns None when item is in_progress."""
        item = _create_at_status(work, "in_progress")
        result = work.approve(item["id"])
        assert result is None
        assert work.get(item["id"])["status"] == "in_progress"

    def test_approve_rejects_completed_status(self, work):
        """approve() returns None when item is already completed."""
        item = work.create(title="Already done")
        work.complete(item["id"])
        result = work.approve(item["id"])
        assert result is None
        assert work.get(item["id"])["status"] == "completed"

    def test_approve_rejects_approved_for_dispatch(self, work):
        """approve() returns None when item is already approved_for_dispatch."""
        item = work.create(title="Already approved")
        work.mark_pending_approval(item["id"])
        work.approve(item["id"])
        # Try to approve again
        result = work.approve(item["id"])
        assert result is None
        # Status still approved_for_dispatch (unchanged)
        assert work.get(item["id"])["status"] == "approved_for_dispatch"

    def test_approve_nonexistent(self, work):
        """approve() returns None for nonexistent item."""
        result = work.approve("wi-ghost")
        assert result is None

    # --- approve_all ---

    def test_approve_all_multiple_items(self, work):
        """approve_all() transitions all pending_approval items."""
        items = []
        for i in range(3):
            item = work.create(title=f"Pending {i}", priority=100 - i)
            work.mark_pending_approval(item["id"])
            items.append(item)

        approved = work.approve_all()
        assert len(approved) == 3
        for a in approved:
            assert a["status"] == "approved_for_dispatch"

    def test_approve_all_empty_when_none_pending(self, work):
        """approve_all() returns empty list when no items are pending_approval."""
        work.create(title="Just queued")
        item2 = work.create(title="In progress")
        work.update_status(item2["id"], "in_progress")

        approved = work.approve_all()
        assert approved == []

    def test_approve_all_ignores_other_statuses(self, work):
        """approve_all() only affects pending_approval items."""
        queued = work.create(title="Queued item")
        in_prog = work.create(title="In-progress item")
        work.update_status(in_prog["id"], "in_progress")
        pending = work.create(title="Pending approval item")
        work.mark_pending_approval(pending["id"])

        approved = work.approve_all()
        assert len(approved) == 1
        assert approved[0]["id"] == pending["id"]

        # Other items unchanged
        assert work.get(queued["id"])["status"] == "queued"
        assert work.get(in_prog["id"])["status"] == "in_progress"

    def test_approve_all_respects_priority_order(self, work):
        """approve_all() processes items in priority order."""
        low = work.create(title="Low priority", priority=200)
        high = work.create(title="High priority", priority=10)
        mid = work.create(title="Mid priority", priority=100)

        work.mark_pending_approval(low["id"])
        work.mark_pending_approval(high["id"])
        work.mark_pending_approval(mid["id"])

        approved = work.approve_all()
        assert len(approved) == 3
        assert approved[0]["title"] == "High priority"
        assert approved[1]["title"] == "Mid priority"
        assert approved[2]["title"] == "Low priority"

    # --- list_pending_approval ---

    def test_list_pending_approval(self, work):
        """list_pending_approval() returns only items with pending_approval status."""
        queued = work.create(title="Queued")
        pending1 = work.create(title="Pending 1")
        pending2 = work.create(title="Pending 2")
        dispatched = work.create(title="Already dispatched")

        work.mark_pending_approval(pending1["id"])
        work.mark_pending_approval(pending2["id"])
        work.mark_pending_approval(dispatched["id"])
        work.approve(dispatched["id"])

        result = work.list_pending_approval()
        assert len(result) == 2
        titles = {r["title"] for r in result}
        assert titles == {"Pending 1", "Pending 2"}

    def test_list_pending_approval_empty(self, work):
        """list_pending_approval() returns empty list when nothing is pending."""
        work.create(title="Just queued")
        result = work.list_pending_approval()
        assert result == []

    def test_list_pending_approval_ordered_by_priority(self, work):
        """list_pending_approval() returns items ordered by priority then creation."""
        low = work.create(title="Low", priority=200)
        high = work.create(title="High", priority=10)
        work.mark_pending_approval(low["id"])
        work.mark_pending_approval(high["id"])

        result = work.list_pending_approval()
        assert result[0]["title"] == "High"
        assert result[1]["title"] == "Low"


# ---------------------------------------------------------------------------
# 3. Full Approval Gate Lifecycle
# ---------------------------------------------------------------------------

class TestApprovalGateLifecycle:
    """End-to-end lifecycle through the plan approval gate."""

    def test_full_approval_gate_lifecycle(self, work):
        """queued → pending_approval → approved_for_dispatch → in_progress → completed"""
        item = work.create(title="Plan-gated feature")
        assert item["status"] == "queued"

        result = work.mark_pending_approval(item["id"])
        assert result["status"] == "pending_approval"

        result = work.approve(item["id"])
        assert result["status"] == "approved_for_dispatch"

        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

        result = work.update_status(item["id"], "pending_review")
        assert result["status"] == "pending_review"

        result = work.update_review(item["id"], "approved", "LGTM")
        assert result["status"] == "approved"

        result = work.complete(item["id"], result_summary="Shipped via approval gate")
        assert result["status"] == "completed"
        assert result["completed_at"] is not None
        assert result["result_summary"] == "Shipped via approval gate"

    def test_approval_gate_with_revision_loop(self, work):
        """Approval gate lifecycle with a revision cycle in the review phase."""
        item = work.create(title="Complex gated work")

        work.mark_pending_approval(item["id"])
        work.approve(item["id"])
        work.update_status(item["id"], "in_progress")
        work.update_status(item["id"], "pending_review")

        # Reviewer requests changes
        result = work.update_review(item["id"], "needs_revision", "Needs error handling")
        assert result["status"] == "needs_revision"

        # Rework
        work.update_status(item["id"], "in_progress")
        work.update_status(item["id"], "pending_review")

        # Approved on second attempt
        result = work.update_review(item["id"], "approved", "Good now")
        assert result["status"] == "approved"

        result = work.complete(item["id"])
        assert result["status"] == "completed"

    def test_approval_gate_count_consistency(self, work):
        """Counts stay consistent through the approval gate lifecycle."""
        items = [work.create(title=f"Item {i}") for i in range(4)]

        # Move items to various approval gate states
        work.mark_pending_approval(items[0]["id"])
        work.mark_pending_approval(items[1]["id"])
        work.approve(items[1]["id"])
        work.mark_pending_approval(items[2]["id"])
        work.approve(items[2]["id"])
        work.update_status(items[2]["id"], "in_progress")

        assert work.count(status="queued") == 1
        assert work.count(status="pending_approval") == 1
        assert work.count(status="approved_for_dispatch") == 1
        assert work.count(status="in_progress") == 1
        assert work.count() == 4


# ---------------------------------------------------------------------------
# 4. Assigned Status (scheduler claim flow)
# ---------------------------------------------------------------------------

class TestAssignedStatus:
    """Tests for the 'assigned' status used by the scheduler when claiming work.

    The scheduler sets status='assigned' via update_status() when an agent
    claims a queued work item (see scheduler.py CLAIM_QUERY).
    """

    def test_queued_to_assigned(self, work):
        """Scheduler claims a queued item → assigned."""
        item = work.create(title="Claimable work")
        result = work.update_status(item["id"], "assigned")
        assert result["status"] == "assigned"

    def test_assigned_to_in_progress(self, work):
        """Agent starts working on assigned item → in_progress."""
        item = _create_at_status(work, "assigned")
        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

    def test_assigned_to_completed(self, work):
        """Agent completes assigned item directly (trivial work)."""
        item = _create_at_status(work, "assigned")
        result = work.complete(item["id"], result_summary="Done immediately")
        assert result["status"] == "completed"
        assert result["completed_at"] is not None
        assert result["result_summary"] == "Done immediately"

    def test_assigned_to_failed(self, work):
        """Agent fails while working on assigned item."""
        item = _create_at_status(work, "assigned")
        result = work.update_status(item["id"], "failed")
        assert result["status"] == "failed"

    def test_assigned_to_queued_requeue(self, work):
        """Requeue an assigned item back to queued (e.g., agent died)."""
        item = _create_at_status(work, "assigned")
        result = work.update_status(item["id"], "queued")
        assert result["status"] == "queued"

    def test_assigned_preserves_metadata(self, work):
        """Transitioning to assigned preserves all other fields."""
        item = work.create(
            title="Rich metadata",
            description="Detailed desc",
            priority=30,
            domain_tags="infra,backend",
            project="greymatter",
            feature="scheduler",
        )
        result = work.update_status(item["id"], "assigned")
        assert result["title"] == "Rich metadata"
        assert result["description"] == "Detailed desc"
        assert result["priority"] == 30
        assert result["domain_tags"] == "infra,backend"
        assert result["feature"] == "scheduler"

    def test_full_assigned_lifecycle(self, work):
        """Full path: queued → assigned → in_progress → pending_review → approved → completed."""
        item = work.create(title="Full assigned lifecycle")
        assert item["status"] == "queued"

        result = work.update_status(item["id"], "assigned")
        assert result["status"] == "assigned"

        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

        result = work.update_status(item["id"], "pending_review")
        assert result["status"] == "pending_review"

        result = work.update_review(item["id"], "approved", "Ship it")
        assert result["status"] == "approved"
        assert result["review_notes"] == "Ship it"

        result = work.complete(item["id"], result_summary="3 files changed")
        assert result["status"] == "completed"
        assert result["completed_at"] is not None

    def test_assigned_count_and_list(self, work):
        """Count and list agree for assigned items."""
        for i in range(3):
            item = work.create(title=f"Assigned {i}")
            work.update_status(item["id"], "assigned")

        assert work.count(status="assigned") == 3
        assert len(work.list(status="assigned")) == 3


# ---------------------------------------------------------------------------
# 5. Failed Status (coordinator fail endpoint)
# ---------------------------------------------------------------------------

class TestFailedStatus:
    """Tests for the 'failed' status used when agent work fails.

    The coordinator sets status='failed' via update_status() when a work item
    cannot be completed (see coordinator.py fail_work endpoint).
    """

    def test_in_progress_to_failed(self, work):
        """Work item fails during execution."""
        item = _create_at_status(work, "in_progress")
        result = work.update_status(item["id"], "failed")
        assert result["status"] == "failed"

    def test_assigned_to_failed(self, work):
        """Work item fails right after assignment."""
        item = _create_at_status(work, "assigned")
        result = work.update_status(item["id"], "failed")
        assert result["status"] == "failed"

    def test_failed_to_queued_retry(self, work):
        """Re-queue a failed item for retry."""
        item = _create_at_status(work, "failed")
        result = work.update_status(item["id"], "queued")
        assert result["status"] == "queued"

    def test_failed_to_in_progress_reopen(self, work):
        """Directly reopen a failed item for rework."""
        item = _create_at_status(work, "failed")
        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

    def test_failed_to_completed(self, work):
        """Mark a failed item as completed (e.g., resolved externally)."""
        item = _create_at_status(work, "failed")
        result = work.complete(item["id"], result_summary="Resolved manually")
        assert result["status"] == "completed"
        assert result["result_summary"] == "Resolved manually"

    def test_failed_count_and_list(self, work):
        """Count and list agree for failed items."""
        for i in range(2):
            item = work.create(title=f"Failed {i}")
            work.update_status(item["id"], "in_progress")
            work.update_status(item["id"], "failed")

        assert work.count(status="failed") == 2
        assert len(work.list(status="failed")) == 2

    def test_failed_retry_full_lifecycle(self, work):
        """Full retry path: queued → assigned → failed → queued → assigned → completed."""
        item = work.create(title="Retry lifecycle")

        work.update_status(item["id"], "assigned")
        work.update_status(item["id"], "failed")
        assert work.get(item["id"])["status"] == "failed"

        # Retry
        work.update_status(item["id"], "queued")
        work.update_status(item["id"], "assigned")
        result = work.complete(item["id"], result_summary="Succeeded on retry")
        assert result["status"] == "completed"
        assert result["result_summary"] == "Succeeded on retry"

    def test_approve_rejects_failed_status(self, work):
        """approve() returns None when item has failed status."""
        item = _create_at_status(work, "failed")
        result = work.approve(item["id"])
        assert result is None
        assert work.get(item["id"])["status"] == "failed"


# ---------------------------------------------------------------------------
# 6. Update Review Edge Cases
# ---------------------------------------------------------------------------

class TestUpdateReviewEdgeCases:
    """Edge cases for the update_review method."""

    def test_update_review_nonexistent_item(self, work):
        """update_review returns None for nonexistent item."""
        result = work.update_review("wi-nonexistent", "approved", "Good work")
        assert result is None

    def test_update_review_sets_review_notes(self, work):
        """Review notes are stored when transitioning."""
        item = _create_at_status(work, "pending_review")
        result = work.update_review(item["id"], "needs_revision", "Fix error handling")
        assert result["status"] == "needs_revision"
        assert result["review_notes"] == "Fix error handling"

    def test_update_review_overwrites_previous_notes(self, work):
        """Subsequent reviews overwrite previous review notes."""
        item = _create_at_status(work, "pending_review")

        work.update_review(item["id"], "needs_revision", "First review: fix tests")
        work.update_status(item["id"], "in_progress")
        work.update_status(item["id"], "pending_review")
        result = work.update_review(item["id"], "approved", "Second review: LGTM")

        assert result["status"] == "approved"
        assert result["review_notes"] == "Second review: LGTM"

    def test_update_review_with_none_notes(self, work):
        """Review with None notes clears the field."""
        item = _create_at_status(work, "pending_review")
        work.update_review(item["id"], "needs_revision", "Some notes")
        result = work.update_review(item["id"], "approved", None)
        assert result["status"] == "approved"
        assert result["review_notes"] is None

    def test_update_review_preserves_other_fields(self, work):
        """update_review only changes status and review_notes."""
        item = work.create(
            title="Review me",
            description="Detailed task",
            priority=20,
            domain_tags="api",
        )
        work.update_status(item["id"], "in_progress")
        work.update_status(item["id"], "pending_review")
        result = work.update_review(item["id"], "approved", "Clean code")

        assert result["title"] == "Review me"
        assert result["description"] == "Detailed task"
        assert result["priority"] == 20
        assert result["domain_tags"] == "api"


# ---------------------------------------------------------------------------
# 7. Transition Edge Cases
# ---------------------------------------------------------------------------

class TestTransitionEdgeCases:
    """Edge cases and boundary conditions for status transitions."""

    def test_update_status_nonexistent_item(self, work):
        """update_status returns None for nonexistent item."""
        result = work.update_status("wi-nonexistent", "in_progress")
        assert result is None

    def test_complete_nonexistent_item(self, work):
        """complete returns None for nonexistent item."""
        result = work.complete("wi-nonexistent")
        assert result is None

    def test_idempotent_status_update(self, work):
        """Setting the same status twice is a no-op (no error)."""
        item = work.create(title="Idempotent test")
        work.update_status(item["id"], "in_progress")
        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

    def test_complete_already_completed(self, work):
        """Completing an already-completed item overwrites completed_at."""
        item = work.create(title="Double complete")
        work.complete(item["id"], result_summary="First completion")
        first = work.get(item["id"])
        assert first["result_summary"] == "First completion"

        work.complete(item["id"], result_summary="Second completion")
        second = work.get(item["id"])
        assert second["result_summary"] == "Second completion"
        assert second["status"] == "completed"

    def test_transition_from_completed_back_to_in_progress(self, work):
        """update_status allows reopening a completed item (no guard)."""
        item = work.create(title="Reopenable")
        work.complete(item["id"])
        assert work.get(item["id"])["status"] == "completed"

        result = work.update_status(item["id"], "in_progress")
        assert result["status"] == "in_progress"

    def test_all_statuses_represented_in_list(self, work):
        """Every known status value is queryable via list()."""
        statuses_to_create = [
            "queued", "in_progress", "assigned", "pending_review",
            "needs_revision", "approved", "completed", "failed",
            "pending_approval", "approved_for_dispatch",
        ]

        for status in statuses_to_create:
            item = _create_at_status(work, status, title=f"Status: {status}")

        for status in statuses_to_create:
            items = work.list(status=status)
            assert len(items) == 1, f"Expected 1 item with status '{status}', got {len(items)}"
            assert items[0]["status"] == status

    def test_count_matches_list_for_every_status(self, work):
        """count() and list() agree for every status value."""
        for status in ["queued", "in_progress", "assigned", "pending_review",
                        "needs_revision", "approved", "completed", "failed",
                        "pending_approval", "approved_for_dispatch"]:
            _create_at_status(work, status, title=f"Count check: {status}")

        for status in ["queued", "in_progress", "assigned", "pending_review",
                        "needs_revision", "approved", "completed", "failed",
                        "pending_approval", "approved_for_dispatch"]:
            assert work.count(status=status) == len(work.list(status=status))

    def test_delete_pending_approval_item(self, work):
        """Can delete an item that is pending_approval."""
        item = work.create(title="Delete me")
        work.mark_pending_approval(item["id"])
        assert work.delete(item["id"]) is True
        assert work.get(item["id"]) is None

    def test_delete_approved_for_dispatch_item(self, work):
        """Can delete an item that is approved_for_dispatch."""
        item = work.create(title="Delete dispatched")
        work.mark_pending_approval(item["id"])
        work.approve(item["id"])
        assert work.delete(item["id"]) is True
        assert work.get(item["id"]) is None


# ---------------------------------------------------------------------------
# 8. Mode-Aware Work Items
# ---------------------------------------------------------------------------

class TestModeAwareCreation:
    """Tests for dispatch-mode-aware work item creation.

    Work items now capture the active dispatch mode at creation time.
    In planned mode, items are auto-gated to pending_approval status.
    """

    # --- dispatch_mode field recording ---

    def test_create_with_autonomous_mode(self, work):
        """Item created in autonomous mode records mode and starts queued."""
        item = work.create(title="Auto task", dispatch_mode="autonomous")
        assert item["dispatch_mode"] == "autonomous"
        assert item["status"] == "queued"

    def test_create_with_supervised_mode(self, work):
        """Item created in supervised mode records mode and starts queued."""
        item = work.create(title="Supervised task", dispatch_mode="supervised")
        assert item["dispatch_mode"] == "supervised"
        assert item["status"] == "queued"

    def test_create_with_planned_mode_auto_gates(self, work):
        """Item created in planned mode auto-gates to pending_approval."""
        item = work.create(title="Planned task", dispatch_mode="planned")
        assert item["dispatch_mode"] == "planned"
        assert item["status"] == "pending_approval"

    def test_create_without_mode_defaults_to_none(self, work):
        """Legacy path: no mode supplied → dispatch_mode is None, status queued."""
        item = work.create(title="Legacy task")
        assert item["dispatch_mode"] is None
        assert item["status"] == "queued"

    def test_create_with_empty_mode_string(self, work):
        """Empty string mode is treated as None (legacy path)."""
        item = work.create(title="Empty mode", dispatch_mode="")
        assert item["dispatch_mode"] is None
        assert item["status"] == "queued"

    def test_create_with_invalid_mode_ignored(self, work):
        """Invalid mode value is silently ignored (stored as None)."""
        item = work.create(title="Bad mode", dispatch_mode="turbo")
        assert item["dispatch_mode"] is None
        assert item["status"] == "queued"

    # --- mode normalisation ---

    def test_mode_normalised_to_lowercase(self, work):
        """Mixed-case mode input is normalised to lowercase."""
        item = work.create(title="Case test", dispatch_mode="Planned")
        assert item["dispatch_mode"] == "planned"
        assert item["status"] == "pending_approval"

    def test_mode_strips_whitespace(self, work):
        """Leading/trailing whitespace in mode is stripped."""
        item = work.create(title="Whitespace", dispatch_mode="  autonomous  ")
        assert item["dispatch_mode"] == "autonomous"

    # --- planned mode auto-gate preserves metadata ---

    def test_planned_mode_preserves_metadata(self, work):
        """Auto-gated item preserves all creation metadata."""
        item = work.create(
            title="Gated feature",
            description="Needs careful review",
            priority=10,
            domain_tags="security,api",
            project="greymatter",
            feature="auth",
            dispatch_mode="planned",
        )
        assert item["status"] == "pending_approval"
        assert item["dispatch_mode"] == "planned"
        assert item["title"] == "Gated feature"
        assert item["description"] == "Needs careful review"
        assert item["priority"] == 10
        assert item["domain_tags"] == "security,api"
        assert item["project"] == "greymatter"
        assert item["feature"] == "auth"

    # --- planned mode items flow through approval gate ---

    def test_planned_mode_approve_lifecycle(self, work):
        """Auto-gated item can be approved and dispatched normally."""
        item = work.create(title="Plan lifecycle", dispatch_mode="planned")
        assert item["status"] == "pending_approval"

        # Approve
        result = work.approve(item["id"])
        assert result is not None
        assert result["status"] == "approved_for_dispatch"
        assert result["dispatch_mode"] == "planned"

        # Execute
        work.update_status(item["id"], "in_progress")
        result = work.complete(item["id"], result_summary="Done via planned gate")
        assert result["status"] == "completed"
        assert result["dispatch_mode"] == "planned"

    # --- mode-filtered listing ---

    def test_list_filter_by_dispatch_mode(self, work):
        """list(dispatch_mode=...) returns only items with that mode."""
        work.create(title="Auto 1", dispatch_mode="autonomous")
        work.create(title="Auto 2", dispatch_mode="autonomous")
        work.create(title="Plan 1", dispatch_mode="planned")
        work.create(title="Legacy", dispatch_mode=None)

        auto_items = work.list(dispatch_mode="autonomous")
        assert len(auto_items) == 2
        for item in auto_items:
            assert item["dispatch_mode"] == "autonomous"

        planned_items = work.list(dispatch_mode="planned")
        assert len(planned_items) == 1
        assert planned_items[0]["dispatch_mode"] == "planned"
        assert planned_items[0]["status"] == "pending_approval"

    def test_list_filter_by_status_and_mode(self, work):
        """list() supports combined status + dispatch_mode filtering."""
        work.create(title="Auto queued", dispatch_mode="autonomous")
        item2 = work.create(title="Auto done", dispatch_mode="autonomous")
        work.complete(item2["id"])
        work.create(title="Plan pending", dispatch_mode="planned")

        # Only queued + autonomous
        items = work.list(status="queued", dispatch_mode="autonomous")
        assert len(items) == 1
        assert items[0]["title"] == "Auto queued"

    # --- count consistency with mode ---

    def test_count_consistency_with_modes(self, work):
        """count() and list() agree for items with dispatch_mode."""
        work.create(title="A1", dispatch_mode="autonomous")
        work.create(title="A2", dispatch_mode="autonomous")
        work.create(title="P1", dispatch_mode="planned")

        # Autonomous items are queued
        assert work.count(status="queued") == 2
        # Planned items are pending_approval
        assert work.count(status="pending_approval") == 1
        assert work.count() == 3

    # --- approve_all includes auto-gated items ---

    def test_approve_all_includes_auto_gated_items(self, work):
        """approve_all() picks up items auto-gated by planned mode."""
        work.create(title="Plan A", dispatch_mode="planned")
        work.create(title="Plan B", dispatch_mode="planned")
        # Manually gated item too
        manual = work.create(title="Manual gate")
        work.mark_pending_approval(manual["id"])

        approved = work.approve_all()
        assert len(approved) == 3
        for item in approved:
            assert item["status"] == "approved_for_dispatch"

    # --- list_pending_approval includes auto-gated items ---

    def test_list_pending_approval_includes_auto_gated(self, work):
        """list_pending_approval() returns auto-gated planned items."""
        work.create(title="Planned item", dispatch_mode="planned")
        work.create(title="Autonomous item", dispatch_mode="autonomous")

        pending = work.list_pending_approval()
        assert len(pending) == 1
        assert pending[0]["title"] == "Planned item"
        assert pending[0]["dispatch_mode"] == "planned"


# ---------------------------------------------------------------------------
# 9. complete() vs update_status("completed") Semantics
# ---------------------------------------------------------------------------

class TestCompleteVsUpdateStatus:
    """Tests the critical behavioural difference between complete() and
    update_status(id, "completed").

    complete() is a specialised method that:
      - Sets status to "completed"
      - Sets completed_at timestamp
      - Stores result_summary

    update_status(id, "completed") is a generic setter that:
      - Sets status to "completed"
      - Does NOT touch completed_at or result_summary
    """

    def test_complete_sets_completed_at(self, work):
        """complete() populates the completed_at timestamp."""
        item = work.create(title="Via complete()")
        result = work.complete(item["id"])
        assert result["status"] == "completed"
        assert result["completed_at"] is not None

    def test_update_status_completed_does_not_set_completed_at(self, work):
        """update_status("completed") leaves completed_at as None."""
        item = work.create(title="Via update_status")
        result = work.update_status(item["id"], "completed")
        assert result["status"] == "completed"
        assert result["completed_at"] is None

    def test_complete_stores_result_summary(self, work):
        """complete() stores the result_summary text."""
        item = work.create(title="Summary test")
        result = work.complete(item["id"], result_summary="Changed 3 files")
        assert result["result_summary"] == "Changed 3 files"

    def test_update_status_completed_does_not_set_result_summary(self, work):
        """update_status("completed") leaves result_summary as None."""
        item = work.create(title="No summary")
        result = work.update_status(item["id"], "completed")
        assert result["result_summary"] is None

    def test_complete_after_update_status_fills_metadata(self, work):
        """Calling complete() after update_status("completed") populates timestamps."""
        item = work.create(title="Two-step")
        work.update_status(item["id"], "completed")
        first = work.get(item["id"])
        assert first["completed_at"] is None

        result = work.complete(item["id"], result_summary="Now really done")
        assert result["completed_at"] is not None
        assert result["result_summary"] == "Now really done"


# ---------------------------------------------------------------------------
# 10. Timestamp and Metadata Persistence Across Reopens
# ---------------------------------------------------------------------------

class TestReopenMetadataPersistence:
    """Tests whether completed_at and result_summary persist or get
    cleared when a completed item is reopened via update_status().

    Since update_status() is a generic setter, it only changes the status
    column -- all other fields remain untouched.
    """

    def test_completed_at_persists_after_reopen(self, work):
        """Reopening a completed item does NOT clear completed_at."""
        item = work.create(title="Reopen timestamp")
        work.complete(item["id"], result_summary="First run")
        completed = work.get(item["id"])
        assert completed["completed_at"] is not None

        reopened = work.update_status(item["id"], "in_progress")
        assert reopened["status"] == "in_progress"
        # completed_at is a stale value -- update_status doesn't clear it
        assert reopened["completed_at"] is not None

    def test_result_summary_persists_after_reopen(self, work):
        """Reopening a completed item does NOT clear result_summary."""
        item = work.create(title="Reopen summary")
        work.complete(item["id"], result_summary="commit abc1234")
        reopened = work.update_status(item["id"], "in_progress")
        assert reopened["result_summary"] == "commit abc1234"

    def test_complete_overwrites_stale_metadata_on_re_complete(self, work):
        """Re-completing after reopen overwrites the old completed_at and summary."""
        item = work.create(title="Re-complete")
        work.complete(item["id"], result_summary="First attempt")
        first_ts = work.get(item["id"])["completed_at"]

        work.update_status(item["id"], "in_progress")
        result = work.complete(item["id"], result_summary="Second attempt")

        assert result["status"] == "completed"
        assert result["result_summary"] == "Second attempt"
        assert result["completed_at"] is not None
        # Timestamp refreshed (could be same second, so just check it's set)

    def test_review_notes_persist_through_completion(self, work):
        """Review notes survive the approved → completed transition."""
        item = _create_at_status(work, "pending_review")
        work.update_review(item["id"], "approved", "LGTM, ship it")
        result = work.complete(item["id"], result_summary="Shipped")
        assert result["review_notes"] == "LGTM, ship it"
        assert result["result_summary"] == "Shipped"

    def test_review_notes_persist_through_reopen(self, work):
        """Review notes survive reopen → rework → re-complete."""
        item = _create_at_status(work, "pending_review")
        work.update_review(item["id"], "needs_revision", "Fix error handling")
        work.update_status(item["id"], "in_progress")
        # Notes still present during rework
        reworking = work.get(item["id"])
        assert reworking["review_notes"] == "Fix error handling"


# ---------------------------------------------------------------------------
# 11. mark_pending_approval from Non-Queued States
# ---------------------------------------------------------------------------

class TestMarkPendingApprovalFromAnyState:
    """mark_pending_approval() has no guard on the source status.
    It transitions ANY state to pending_approval.  These tests
    document that (lack-of-guard) behaviour.
    """

    def test_mark_pending_approval_from_in_progress(self, work):
        """Can gate an in_progress item to pending_approval."""
        item = _create_at_status(work, "in_progress")
        result = work.mark_pending_approval(item["id"])
        assert result["status"] == "pending_approval"

    def test_mark_pending_approval_from_assigned(self, work):
        """Can gate an assigned item to pending_approval."""
        item = _create_at_status(work, "assigned")
        result = work.mark_pending_approval(item["id"])
        assert result["status"] == "pending_approval"

    def test_mark_pending_approval_from_failed(self, work):
        """Can gate a failed item to pending_approval."""
        item = _create_at_status(work, "failed")
        result = work.mark_pending_approval(item["id"])
        assert result["status"] == "pending_approval"

    def test_mark_pending_approval_from_completed(self, work):
        """Can gate a completed item to pending_approval."""
        item = _create_at_status(work, "completed")
        result = work.mark_pending_approval(item["id"])
        assert result["status"] == "pending_approval"

    def test_mark_pending_approval_from_approved_for_dispatch(self, work):
        """Can gate an approved_for_dispatch item back to pending_approval."""
        item = _create_at_status(work, "approved_for_dispatch")
        result = work.mark_pending_approval(item["id"])
        assert result["status"] == "pending_approval"
        # Can now re-approve
        re_approved = work.approve(item["id"])
        assert re_approved["status"] == "approved_for_dispatch"

    def test_mark_pending_approval_idempotent(self, work):
        """Calling mark_pending_approval on a pending_approval item is a no-op."""
        item = work.create(title="Already pending")
        work.mark_pending_approval(item["id"])
        result = work.mark_pending_approval(item["id"])
        assert result["status"] == "pending_approval"


# ---------------------------------------------------------------------------
# 12. Parametrised Transition Matrix
# ---------------------------------------------------------------------------

# Every valid status in the system.
ALL_STATUSES = [
    "queued", "assigned", "in_progress", "pending_review",
    "needs_revision", "approved", "completed", "failed",
    "pending_approval", "approved_for_dispatch",
]


class TestTransitionMatrix:
    """Systematic coverage of the 10x10 status transition space.

    update_status() has no guards, so every from→to pair succeeds.
    This parametrised test documents that property exhaustively and
    ensures none of the 100 pairs raises an error or returns None.
    """

    @pytest.mark.parametrize("from_status", ALL_STATUSES)
    @pytest.mark.parametrize("to_status", ALL_STATUSES)
    def test_any_to_any_via_update_status(self, work, from_status, to_status):
        """update_status() accepts every from→to status pair."""
        item = _create_at_status(work, from_status, title=f"{from_status}→{to_status}")
        result = work.update_status(item["id"], to_status)
        assert result is not None, (
            f"update_status({from_status!r} → {to_status!r}) returned None"
        )
        assert result["status"] == to_status


# ---------------------------------------------------------------------------
# 13. Invalid / Unknown Status Strings
# ---------------------------------------------------------------------------

class TestInvalidStatusStrings:
    """update_status() performs no validation on the status value.
    These tests document that arbitrary strings are accepted and
    stored, and that such items are excluded from known-status queries.
    """

    def test_update_status_accepts_unknown_string(self, work):
        """An arbitrary status string is accepted and stored."""
        item = work.create(title="Unknown status")
        result = work.update_status(item["id"], "banana")
        assert result["status"] == "banana"

    def test_unknown_status_excluded_from_known_filters(self, work):
        """Items with unknown status don't appear in known-status list queries."""
        item = work.create(title="Rogue item")
        work.update_status(item["id"], "banana")

        for known in ALL_STATUSES:
            items = work.list(status=known)
            ids = [i["id"] for i in items]
            assert item["id"] not in ids, (
                f"Item with status 'banana' appeared in list(status={known!r})"
            )

    def test_unknown_status_included_in_unfiltered_list(self, work):
        """Items with unknown status appear in unfiltered list()."""
        item = work.create(title="Odd status")
        work.update_status(item["id"], "banana")

        all_items = work.list()
        ids = [i["id"] for i in all_items]
        assert item["id"] in ids

    def test_unknown_status_included_in_total_count(self, work):
        """Items with unknown status are counted in count() with no filter."""
        work.create(title="Normal item")
        rogue = work.create(title="Rogue")
        work.update_status(rogue["id"], "banana")

        assert work.count() == 2
        assert work.count(status="queued") == 1
        assert work.count(status="banana") == 1

    def test_empty_string_status(self, work):
        """Empty string is accepted as a status value (no validation)."""
        item = work.create(title="Empty status")
        result = work.update_status(item["id"], "")
        assert result["status"] == ""

    def test_complete_overrides_unknown_status(self, work):
        """complete() forces status to 'completed' regardless of current status."""
        item = work.create(title="From banana to done")
        work.update_status(item["id"], "banana")
        result = work.complete(item["id"], result_summary="Fixed it")
        assert result["status"] == "completed"
        assert result["completed_at"] is not None


# ---------------------------------------------------------------------------
# 14. Delete from Every Status
# ---------------------------------------------------------------------------

class TestDeleteFromEveryStatus:
    """Verifies that work items can be deleted from every known status."""

    @pytest.mark.parametrize("status", ALL_STATUSES)
    def test_delete_from_status(self, work, status):
        """delete() succeeds from any known status."""
        item = _create_at_status(work, status, title=f"Delete from {status}")
        assert work.delete(item["id"]) is True
        assert work.get(item["id"]) is None

    def test_delete_nonexistent_item(self, work):
        """delete() returns False for nonexistent items."""
        assert work.delete("wi-does-not-exist") is False

    def test_delete_decrements_count(self, work):
        """Deleting an item reduces the total count."""
        item = work.create(title="Deletable")
        assert work.count() == 1
        work.delete(item["id"])
        assert work.count() == 0
