import asyncio
import json
import os

from recoil.workspace.tests._board_test_helpers import _json_body


def test_board_route_registered():
    from recoil.workspace.server import app

    routes = [r.path for r in app.routes if hasattr(r, "path")]
    assert "/api/episode/{project}/{episode_id}/board" in routes


def test_board_route_returns_build_episode_board_shape(board_route_project):
    ws_server = board_route_project["server"]
    project = board_route_project["project"]

    response = asyncio.run(ws_server.get_episode_board(project, "EP001"))
    body = _json_body(response)

    assert body["episode_id"] == "EP001"
    assert body["summary"] == {
        "covered": 2,
        "total": 2,
        "awaiting": 0,
        "review_count": 1,
    }
    assert len(body["batches"]) == 1
    batch = body["batches"][0]
    assert batch["batch_id"] == "BATCH_001"
    assert batch["board_artifact"] == "prep/ep_001/storyboards/BATCH_001_v06.png"
    assert batch["version"] == 6
    assert batch["status"] == "proposed"
    assert batch["coverage_summary"] == {"covered": 2, "total": 2, "awaiting": 0}


def test_board_route_mtime_key_failure_serves_cache_not_500(
    board_route_project, monkeypatch
):
    """REC-231 MINOR: a corrupt/cross-batch manifest makes _episode_board_mtime_key raise.
    The endpoint must NOT 500 the whole wall — a fresh cached payload is served if present,
    else it degrades to the rebuild path (400 on real corruption), never an unhandled 500."""
    ws_server = board_route_project["server"]
    project = board_route_project["project"]

    # Warm the cache with a good build.
    first = asyncio.run(ws_server.get_episode_board(project, "EP001"))
    assert first.status_code == 200
    cached = _json_body(first)

    # Force the pre-cache mtime-key computation to raise (corrupt-manifest analogue).
    def _boom(*a, **k):
        raise KeyError("manifest schema_version mismatch")

    monkeypatch.setattr(ws_server, "_episode_board_mtime_key", _boom)

    resp = asyncio.run(ws_server.get_episode_board(project, "EP001"))
    assert resp.status_code == 200          # served from cache, NOT a 500
    assert _json_body(resp) == cached

    # No cache + mtime-key still failing → rebuild path (valid fixture here) → 200/400, never 500.
    ws_server._board_cache.clear()
    ws_server._board_cache_mtime_key.clear()
    ws_server._board_cache_time.clear()
    resp2 = asyncio.run(ws_server.get_episode_board(project, "EP001"))
    assert resp2.status_code in (200, 400)


def test_board_route_sidecar_content_edit_invalidates_mtime_cache(board_route_project):
    ws_server = board_route_project["server"]
    project = board_route_project["project"]
    sidecar = board_route_project["sidecar"]

    first = asyncio.run(ws_server.get_episode_board(project, "1"))
    first_body = _json_body(first)
    # REC-231 Phase 4: board_artifact resolves from the pointed sidecar JSON's
    # `artifact` field when present, else the pointer artifact (here BATCH_001_v06).
    assert (
        first_body["batches"][0]["board_artifact"]
        == "prep/ep_001/storyboards/BATCH_001_v06.png"
    )
    cache_key = "fixture:EP001"
    first_mtime_key = ws_server._board_cache_mtime_key[cache_key]

    sidecar.write_text(
        json.dumps({"artifact": "prep/ep_001/storyboards/BATCH_001_v07.png"}),
        encoding="utf-8",
    )
    bumped = sidecar.stat().st_mtime_ns + 1_000_000
    os.utime(sidecar, ns=(bumped, bumped))

    second = asyncio.run(ws_server.get_episode_board(project, "1"))
    second_body = _json_body(second)

    assert ws_server._board_cache_mtime_key[cache_key] != first_mtime_key
    assert (
        second_body["batches"][0]["board_artifact"]
        == "prep/ep_001/storyboards/BATCH_001_v07.png"
    )


def test_board_route_bad_episode_id_returns_400(board_route_project):
    ws_server = board_route_project["server"]
    project = board_route_project["project"]

    response = asyncio.run(ws_server.get_episode_board(project, "nope"))
    body = _json_body(response)

    assert response.status_code == 400
    assert "nope" in body["error"]
    assert body["episode_id"] == "nope"

