from datetime import datetime, timedelta, timezone

import pytest

from api.config import settings
from api.services import ops_run_projection as projection


@pytest.fixture(autouse=True)
def reset_projection_cache() -> None:
    projection.reset_ops_run_projection_cache()


@pytest.mark.asyncio
async def test_ops_run_projection_counts_and_contract(monkeypatch) -> None:
    now = datetime.now(timezone.utc)
    payload = {
        "counts": {"open": 2, "claimed": 1, "succeeded": 9, "failed": 2},
        "items": [
            {
                "id": "run-open",
                "activity_definition_id": "definition-1",
                "target_repo": "state-hub",
                "state": "open",
                "created_at": (now - timedelta(hours=2)).isoformat(),
                "updated_at": (now - timedelta(hours=2)).isoformat(),
                "result": {},
            },
            {
                "id": "run-claimed",
                "activity_definition_id": "definition-2",
                "target_repo": "activity-core",
                "state": "claimed",
                "claim_owner": "rein-aharness@railiance01",
                "lease_until": (now + timedelta(minutes=10)).isoformat(),
                "attempt": 1,
                "created_at": (now - timedelta(minutes=10)).isoformat(),
                "updated_at": now.isoformat(),
                "result": {},
            },
            {
                "id": "run-failed",
                "activity_definition_id": "definition-3",
                "target_repo": "binky-control",
                "state": "failed",
                "created_at": (now - timedelta(hours=3)).isoformat(),
                "updated_at": (now - timedelta(hours=1)).isoformat(),
                "result": {"error": "executor timeout"},
            },
        ],
    }

    async def fake_fetch():
        return payload

    monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch)
    result = await projection.get_ops_run_projection(refresh=True)

    assert result.available is True
    assert result.open == 2
    assert result.claimed == 1
    assert result.failed_24h == 1
    assert result.stuck_open_or_claimed == 1
    assert {item.id for item in result.items} == {"run-open", "run-claimed", "run-failed"}
    failed = next(item for item in result.items if item.id == "run-failed")
    assert failed.last_error == "executor timeout"
    claimed = next(item for item in result.items if item.id == "run-claimed")
    assert claimed.lease["owner"] == "rein-aharness@railiance01"


@pytest.mark.asyncio
async def test_ops_run_projection_serves_stale_cache_on_refresh_failure(monkeypatch) -> None:
    async def initial_fetch():
        return {"counts": {"open": 1}, "items": []}

    monkeypatch.setattr(projection, "_fetch_ops_runs", initial_fetch)
    initial = await projection.get_ops_run_projection(refresh=True)
    assert initial.available is True

    async def failed_fetch():
        raise RuntimeError("upstream down")

    monkeypatch.setattr(projection, "_fetch_ops_runs", failed_fetch)
    stale = await projection.get_ops_run_projection(refresh=True)
    assert stale.available is True
    assert stale.stale is True
    assert "upstream down" in stale.error


@pytest.mark.asyncio
async def test_ops_run_projection_reports_unconfigured(monkeypatch) -> None:
    monkeypatch.setattr(settings, "activity_core_url", None)
    result = await projection.get_ops_run_projection(refresh=True)
    assert result.available is False
    assert "ACTIVITY_CORE_URL is not configured" in result.error


@pytest.mark.asyncio
async def test_ops_runs_summary_route(client, monkeypatch) -> None:
    async def fake_fetch():
        return {"counts": {"open": 3, "claimed": 2}, "items": []}

    monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch)
    response = await client.get("/ops-runs/summary", params={"refresh": "true"})
    assert response.status_code == 200
    assert response.json()["open"] == 3
    assert response.json()["claimed"] == 2
    assert (await client.post("/ops-runs/claim", json={})).status_code == 404


@pytest.mark.asyncio
async def test_state_summary_contains_ops_run_projection(client, monkeypatch) -> None:
    async def fake_fetch():
        return {"counts": {"open": 4}, "items": []}

    monkeypatch.setattr(projection, "_fetch_ops_runs", fake_fetch)
    response = await client.get("/state/summary", params={"refresh": "true"})
    assert response.status_code == 200
    assert response.json()["ops_runs"]["open"] == 4
