from __future__ import annotations

import json
import sys

import pytest

from recoil.core.paths import ProjectPaths
from recoil.pipeline._lib import derivation_manifest
from recoil.pipeline._lib.derivation_sha import (
    board_content_freshness_sha,
    plan_structural_sha,
    shotset_hash,
)
from recoil.pipeline.cli import generate


PROJECT = "tartarus"
EPISODE = 1
SHOT_A = "EP001_SH001"
SHOT_B = "EP001_SH002"


@pytest.fixture
def project_paths(tmp_path, monkeypatch) -> ProjectPaths:
    root = tmp_path / "projects"
    root.mkdir()
    (root / ".recoil-data-root").touch()
    project_root = root / PROJECT
    project_root.mkdir()
    monkeypatch.setenv("RECOIL_PROJECTS_ROOT", str(root))
    monkeypatch.setattr(
        derivation_manifest.episode_script,
        "episode_script_sha",
        lambda project, episode: "script-sha",
    )
    return ProjectPaths.for_project(PROJECT)


def _raw_shot(shot_id: str, span_hash: str | None, scene_index: int) -> dict:
    return {
        "shot_id": shot_id,
        "scene_index": scene_index,
        "source_text_hash": span_hash,
        "routing_data": {
            "is_env_only": False,
            "target_editorial_duration_s": 1.0,
            "has_dialogue": False,
        },
        "asset_data": {"location_id": "LOC_A", "characters": []},
        "prompt_data": {"shot_type": "MS"},
        "spatial_data": {},
    }


def _write_plan(paths: ProjectPaths, spans: dict[str, str | None]) -> dict:
    raw = {
        "episode_id": "EP001",
        "project": PROJECT,
        "total_shots": len(spans),
        "shots": [
            _raw_shot(shot_id, span_hash, index)
            for index, (shot_id, span_hash) in enumerate(spans.items(), start=1)
        ],
    }
    paths.plans_dir.mkdir(parents=True, exist_ok=True)
    (paths.plans_dir / "ep_001_plan.json").write_text(
        json.dumps(raw),
        encoding="utf-8",
    )
    return raw


def _stamp_fresh_chain(
    paths: ProjectPaths,
    spans: dict[str, str | None],
    *,
    coverage_plan_sha: str | None = None,
) -> dict:
    raw = _write_plan(paths, spans)
    structural = plan_structural_sha(raw)
    derivation_manifest.stamp_stage(
        PROJECT,
        EPISODE,
        "camera_tested",
        kind="derived",
        content_sha="sha256:CAMERA",
        source={"script_sha": "script-sha"},
        builder="test",
    )
    derivation_manifest.stamp_bible(
        PROJECT,
        content_sha="sha256:BIBLE",
        builder="test",
        built_at="2026-06-20T00:00:00+00:00",
    )
    derivation_manifest.stamp_stage(
        PROJECT,
        EPISODE,
        "plan",
        kind="derived",
        structural_sha=structural,
        content_sha="sha256:PLAN",
        source={
            "camera_tested_content_sha": "sha256:CAMERA",
            "bible_content_sha": "sha256:BIBLE",
        },
        builder="test",
    )
    derivation_manifest.stamp_stage(
        PROJECT,
        EPISODE,
        "coverage_passes",
        kind="derived",
        content_sha="sha256:COVERAGE",
        source={"plan_structural_sha": coverage_plan_sha or structural},
        builder="test",
    )
    derivation_manifest.stamp_stage(
        PROJECT,
        EPISODE,
        "scenes",
        kind="derived",
        content_sha="sha256:SCENES",
        source={"plan_structural_sha": structural},
        builder="test",
        extra={"shot_script_spans": {"SC_001": dict(spans)}},
    )
    return raw


def _stamp_board(
    shot_ids: list[str],
    *,
    spans_for_sha: dict[str, str | None],
) -> str:
    key = shotset_hash(shot_ids)
    derivation_manifest.stamp_board(
        PROJECT,
        EPISODE,
        key,
        {
            "status": "approved",
            "artifact": "prep/ep_001/storyboards/board.png",
            "source_sha256": "source-v2",
            "fingerprint_version": 2,
            "covered_shot_ids": list(shot_ids),
            "shot_script_spans": dict(spans_for_sha),
            "content_freshness_sha": board_content_freshness_sha(spans_for_sha),
            "approved_by": "test",
            "updated_at": "2026-06-20T00:00:00Z",
        },
    )
    return key


def _run_currency_check(monkeypatch, capsys) -> tuple[int, str]:
    monkeypatch.setattr(
        sys,
        "argv",
        [
            "generate",
            "currency-check",
            "--project",
            PROJECT,
            "--episode",
            "1",
        ],
    )
    code = generate.main()
    captured = capsys.readouterr()
    return code, captured.out


def test_currency_check_cli_reports_stale(project_paths, monkeypatch, capsys):
    _stamp_fresh_chain(project_paths, {SHOT_A: "span-a"})
    monkeypatch.setattr(
        derivation_manifest.episode_script,
        "episode_script_sha",
        lambda project, episode: "script-sha-new",
    )

    upstream_code, upstream_out = _run_currency_check(monkeypatch, capsys)

    assert upstream_code != 0
    assert "STALE: plan (broken at camera_tested)" in upstream_out

    monkeypatch.setattr(
        derivation_manifest.episode_script,
        "episode_script_sha",
        lambda project, episode: "script-sha",
    )
    current_spans = {SHOT_A: "span-a-new", SHOT_B: "span-b"}
    _stamp_fresh_chain(
        project_paths,
        current_spans,
        coverage_plan_sha="sha256:STALE-PLAN",
    )
    board_key = _stamp_board(
        [SHOT_A],
        spans_for_sha={SHOT_A: "span-a-old"},
    )

    code, out = _run_currency_check(monkeypatch, capsys)

    assert code != 0
    assert "FRESH: plan" in out
    assert "STALE: coverage_passes (broken at coverage_passes)" in out
    assert "FRESH: scenes" in out
    assert f"STALE: board[{board_key}] (broken at board)" in out


def test_currency_check_cli_all_fresh(project_paths, monkeypatch, capsys):
    spans = {SHOT_A: "span-a", SHOT_B: "span-b"}
    _stamp_fresh_chain(project_paths, spans)
    board_key = _stamp_board([SHOT_A, SHOT_B], spans_for_sha=spans)

    code, out = _run_currency_check(monkeypatch, capsys)

    assert code == 0
    assert "FRESH: plan" in out
    assert "FRESH: coverage_passes" in out
    assert "FRESH: scenes" in out
    assert f"FRESH: board[{board_key}]" in out


def test_currency_check_cli_no_board(project_paths, monkeypatch, capsys):
    _stamp_fresh_chain(project_paths, {SHOT_A: "span-a"})

    code, out = _run_currency_check(monkeypatch, capsys)

    assert code == 0
    assert "FRESH: plan" in out
    assert "FRESH: coverage_passes" in out
    assert "FRESH: scenes" in out
    assert "board: none" in out
