"""Stream A integration tests — claim TTL, retry/DLQ, workflow CRUD, cache round-trip."""

from __future__ import annotations

from datetime import datetime, timedelta, timezone

import pytest

from loom.graph import cache, store
from loom.graph.task import Priority, Task, TaskStatus
from loom.ids import task_id as gen_task_id


# ---------------------------------------------------------------------------
# Claim TTL
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_set_claim_expiry(pool, redis_conn, project):
    """set_claim_expiry sets claim_expires_at on a claimed task."""
    task = Task(id=gen_task_id(), project_id=project, title="TTL task")
    task = await store.create_task(pool, task)
    task = await store.claim_task(pool, task.id, "agent-1")

    expires = datetime.now(timezone.utc) + timedelta(minutes=10)
    updated = await store.set_claim_expiry(pool, task.id, expires)

    assert updated.claim_expires_at is not None
    assert abs((updated.claim_expires_at - expires).total_seconds()) < 2


@pytest.mark.asyncio
async def test_get_expired_claims(pool, project):
    """get_expired_claims returns only tasks whose TTL has passed."""
    # Create and claim two tasks
    t1 = await store.create_task(pool, Task(id=gen_task_id(), project_id=project, title="Expired"))
    t2 = await store.create_task(pool, Task(id=gen_task_id(), project_id=project, title="Still valid"))

    t1 = await store.claim_task(pool, t1.id, "agent-1")
    t2 = await store.claim_task(pool, t2.id, "agent-2")

    # Set t1 to already-expired, t2 to far future
    past = datetime.now(timezone.utc) - timedelta(minutes=5)
    future = datetime.now(timezone.utc) + timedelta(hours=1)
    await store.set_claim_expiry(pool, t1.id, past)
    await store.set_claim_expiry(pool, t2.id, future)

    expired = await store.get_expired_claims(pool, project)
    expired_ids = [t.id for t in expired]
    assert t1.id in expired_ids
    assert t2.id not in expired_ids


@pytest.mark.asyncio
async def test_release_expired_claim(pool, project):
    """release_expired_claim resets task to pending with no assignee."""
    task = await store.create_task(pool, Task(id=gen_task_id(), project_id=project, title="Release me"))
    task = await store.claim_task(pool, task.id, "agent-1")
    assert task.status == TaskStatus.CLAIMED
    assert task.assignee == "agent-1"

    released = await store.release_expired_claim(pool, task.id)
    assert released.status == TaskStatus.PENDING
    assert released.assignee is None
    assert released.claim_expires_at is None
    assert released.claimed_at is None


# ---------------------------------------------------------------------------
# Retry + Dead Letter
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_increment_retry(pool, project):
    """increment_retry bumps count and sets retry_after."""
    task = await store.create_task(pool, Task(id=gen_task_id(), project_id=project, title="Retry me"))
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "oops")

    retry_at = datetime.now(timezone.utc) + timedelta(minutes=5)
    updated = await store.increment_retry(pool, task.id, retry_at)

    assert updated.retry_count == 1
    assert updated.retry_after is not None
    assert updated.last_failed_at is not None


@pytest.mark.asyncio
async def test_get_retryable_tasks(pool, project):
    """get_retryable_tasks returns failed tasks past their retry_after time."""
    task = await store.create_task(pool, Task(id=gen_task_id(), project_id=project, title="Retryable"))
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "error")

    # Set retry_after to the past
    past = datetime.now(timezone.utc) - timedelta(minutes=1)
    await store.increment_retry(pool, task.id, past)

    retryable = await store.get_retryable_tasks(pool, project)
    assert len(retryable) == 1
    assert retryable[0].id == task.id


@pytest.mark.asyncio
async def test_get_retryable_excludes_dead_letter(pool, project):
    """Dead-lettered tasks are not returned by get_retryable_tasks."""
    task = await store.create_task(pool, Task(id=gen_task_id(), project_id=project, title="Dead"))
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "fatal")

    past = datetime.now(timezone.utc) - timedelta(minutes=1)
    await store.increment_retry(pool, task.id, past)
    await store.mark_dead_letter(pool, task.id, "Max retries exceeded")

    retryable = await store.get_retryable_tasks(pool, project)
    assert len(retryable) == 0


@pytest.mark.asyncio
async def test_mark_dead_letter(pool, project):
    """mark_dead_letter sets dead_letter flag and clears retry_after."""
    task = await store.create_task(pool, Task(id=gen_task_id(), project_id=project, title="DLQ"))
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "error")

    future = datetime.now(timezone.utc) + timedelta(hours=1)
    await store.increment_retry(pool, task.id, future)

    dead = await store.mark_dead_letter(pool, task.id, "Giving up")
    assert dead.dead_letter is True
    assert dead.dead_letter_reason == "Giving up"
    assert dead.retry_after is None


# ---------------------------------------------------------------------------
# Workflow CRUD
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_create_and_get_workflow_run(pool, project):
    """Workflow run can be created and fetched."""
    run = await store.create_workflow_run(pool, project, "ship_feature", {"goal": "test"})

    assert run["workflow_name"] == "ship_feature"
    assert run["status"] == "running"
    assert run["inputs"]["goal"] == "test"
    assert run["id"]

    fetched = await store.get_workflow_run(pool, run["id"])
    assert fetched["id"] == run["id"]
    assert fetched["workflow_name"] == "ship_feature"


@pytest.mark.asyncio
async def test_update_workflow_run(pool, project):
    """Workflow run fields can be updated."""
    run = await store.create_workflow_run(pool, project, "test_wf", {})

    updated = await store.update_workflow_run(
        pool, run["id"],
        status="paused",
        current_step="confirm",
        state={"outputs": {"step1": "done"}},
    )
    assert updated["status"] == "paused"
    assert updated["current_step"] == "confirm"
    assert updated["state"]["outputs"]["step1"] == "done"


@pytest.mark.asyncio
async def test_list_workflow_runs(pool, project):
    """list_workflow_runs returns runs filtered by status."""
    await store.create_workflow_run(pool, project, "wf1", {})
    run2 = await store.create_workflow_run(pool, project, "wf2", {})
    await store.update_workflow_run(pool, run2["id"], status="completed")

    all_runs = await store.list_workflow_runs(pool, project)
    assert len(all_runs) == 2

    running = await store.list_workflow_runs(pool, project, status="running")
    assert len(running) == 1
    assert running[0]["workflow_name"] == "wf1"

    completed = await store.list_workflow_runs(pool, project, status="completed")
    assert len(completed) == 1
    assert completed[0]["workflow_name"] == "wf2"


@pytest.mark.asyncio
async def test_record_workflow_step(pool, project):
    """Workflow steps are logged to the workflow_steps table."""
    run = await store.create_workflow_run(pool, project, "test_wf", {})

    step_id = await store.record_workflow_step(
        pool, run["id"], "write_spec", "completed",
        inputs={"goal": "test"}, outputs={"spec": "done"},
    )
    assert step_id is not None

    # Verify the step exists
    async with pool.acquire() as conn:
        row = await conn.fetchrow(
            "SELECT * FROM workflow_steps WHERE id = $1", step_id
        )
    assert row["step_name"] == "write_spec"
    assert row["status"] == "completed"
    assert row["completed_at"] is not None


@pytest.mark.asyncio
async def test_workflow_step_running_no_completed_at(pool, project):
    """A step logged as 'running' should not have completed_at set."""
    run = await store.create_workflow_run(pool, project, "test_wf", {})
    step_id = await store.record_workflow_step(pool, run["id"], "step1", "running")

    async with pool.acquire() as conn:
        row = await conn.fetchrow("SELECT * FROM workflow_steps WHERE id = $1", step_id)
    assert row["completed_at"] is None


@pytest.mark.asyncio
async def test_get_workflow_run_not_found(pool):
    """LookupError raised for missing workflow run."""
    import uuid
    with pytest.raises(LookupError):
        await store.get_workflow_run(pool, str(uuid.uuid4()))


# ---------------------------------------------------------------------------
# Cache round-trip with new fields
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_cache_round_trip_new_fields(pool, redis_conn, project):
    """New Phase 3 fields survive Redis cache round-trip."""
    task = Task(id=gen_task_id(), project_id=project, title="Cache test")
    task = await store.create_task(pool, task)
    task = await store.claim_task(pool, task.id, "agent-1")

    # Set claim expiry
    expires = datetime.now(timezone.utc) + timedelta(minutes=10)
    task = await store.set_claim_expiry(pool, task.id, expires)

    # Sync to Redis
    await cache.sync_task(redis_conn, task)

    # Read back from Redis
    cached = await cache.get_task(redis_conn, pool, project, task.id)
    assert cached.claim_expires_at is not None
    assert cached.retry_count == 0
    assert cached.dead_letter is False
    assert cached.max_retries == 3


@pytest.mark.asyncio
async def test_cache_round_trip_dead_letter(pool, redis_conn, project):
    """Dead letter fields survive Redis cache round-trip."""
    task = Task(id=gen_task_id(), project_id=project, title="DLQ cache")
    task = await store.create_task(pool, task)
    task = await store.claim_task(pool, task.id, "agent-1")
    task = await store.fail_task(pool, task.id, "error")
    task = await store.mark_dead_letter(pool, task.id, "Too many failures")

    await cache.sync_task(redis_conn, task)
    cached = await cache.get_task(redis_conn, pool, project, task.id)

    assert cached.dead_letter is True
    assert cached.dead_letter_reason == "Too many failures"


# ---------------------------------------------------------------------------
# Event types exist
# ---------------------------------------------------------------------------


def test_phase3_event_types():
    """All Phase 3 event types are defined."""
    from loom.bus.events import EventType
    assert EventType.TASK_RETRIED == "task.retried"
    assert EventType.TASK_DEAD_LETTER == "task.dead_letter"
    assert EventType.CLAIM_EXPIRED == "claim.expired"
    assert EventType.WORKFLOW_STARTED == "workflow.started"
    assert EventType.WORKFLOW_STEP_COMPLETED == "workflow.step_completed"
    assert EventType.WORKFLOW_COMPLETED == "workflow.completed"
    assert EventType.WORKFLOW_FAILED == "workflow.failed"


# ---------------------------------------------------------------------------
# Channel patterns exist
# ---------------------------------------------------------------------------


def test_phase3_channel_patterns():
    """All Phase 3 Redis key patterns are defined."""
    from loom.bus.channels import claim_ttl_key, dead_letter_set, workflow_channel
    pid = "test-project-id"
    assert claim_ttl_key(pid) == f"loom:{pid}:claims:ttl"
    assert dead_letter_set(pid) == f"loom:{pid}:tasks:dead_letter"
    assert workflow_channel(pid) == f"loom:{pid}:workflows"
