"""REC-235 Phase 3 — cross-batch composition assembly + render acceptance test.

``workspace.readmodel.build_cut_composition`` / ``build_pairwise_view`` assemble a
``CompositionManifest`` (Phase 1 schema) from atom-version URNs drawn from DIFFERENT
batches/episodes — PURE assembly: NO resolution, NO generation, NO writes. A pairwise
assessment view is a 2-member cross-batch CUT. The assembled manifest is rendered by the
EXISTING ``board_builder.render_composition_view`` (Phase 1) — ZERO generation.

The load-bearing milestone: an atom persisted in batch A composited next to an atom
persisted in a DIFFERENT batch B, in member order, read back from the composited pixels.
Grounds against the REAL flow — reuses ``_persist_panels`` (batch A) + ``_write_panel`` /
``_beat`` (a second batch B) from ``test_composition_render`` to write actual distinct-color
panel PNGs at the artifact paths a persisted atom-version resolves to, across two separate
persisted scenes, so the composited pixels are non-vacuously the real cross-batch panels.
"""

from __future__ import annotations

import pytest
from PIL import Image
from pydantic import ValidationError

from recoil.api.schemas.composition import CompositionManifest
from recoil.pipeline._lib import board_builder as bb
from recoil.pipeline.core.persistence import save_scene, scene_path
from recoil.pipeline.core.take import Scene
from recoil.pipeline.tests.test_composition_render import (
    _BLUE,
    _RED,
    _beat,
    _cell_geometry,
    _persist_panels,
    _write_panel,
)
from recoil.workspace.readmodel import (
    build_cut_composition,
    build_pairwise_view,
    resolve_atom_version,
)


def _persist_cross_batch(tmp_path, monkeypatch):
    """Persist atom A (RED @SH02) and atom B (BLUE @SH05) in DIFFERENT batches.

    Reuses ``_persist_panels`` for the project + batch A (its single-batch fixture:
    SH02 RED, SH03 BLUE, SH04 None — all in BATCH_001), then adds a SECOND persisted
    scene BATCH_002 holding SH05 BLUE via the same ``_write_panel`` / ``_beat`` /
    ``save_scene`` idiom. ``_persist_panels``'s own blue (SH03) is deliberately NOT used
    as atom B — it shares BATCH_001 with SH02, so it would not prove batch-independence.
    The milestone is cross-batch: urn_a resolves from BATCH_001, urn_b from BATCH_002.

    Returns ``(project, project_root, urn_a, urn_b)``.
    """
    project, project_root, urn_a, _blue_same_batch, _missing = _persist_panels(
        tmp_path, monkeypatch
    )
    _write_panel(project_root, "renders/ep_001/EP001_SH05/t0.png", _BLUE)
    save_scene(
        Scene(
            scene_id="BATCH_002",
            beats=[_beat("EP001_SH05", "renders/ep_001/EP001_SH05/t0.png")],
        ),
        scene_path(project, 1, "BATCH_002"),
    )
    return project, project_root, urn_a, "atom:EP001/beat/EP001_SH05@t0"


# ── Pure assembly (no persistence — assembly never resolves/generates) ──────────────


def test_build_cut_composition_is_cut_with_members_and_default_empty_layout():
    # URNs from two DIFFERENT episodes — assembly is batch/episode-independent and does
    # NOT resolve, so EP002 needs no on-disk scene.
    urns = ["atom:EP001/beat/EP001_SH02@t0", "atom:EP002/beat/EP002_SH09@t0"]
    manifest = build_cut_composition(urns)
    assert isinstance(manifest, CompositionManifest)
    assert manifest.kind == "CUT"
    assert manifest.members == urns
    assert manifest.layout == {}  # default when no layout passed


def test_build_pairwise_view_returns_cut_two_member_manifest():
    urn_a = "atom:EP001/beat/EP001_SH02@t0"
    urn_b = "atom:EP005/beat/EP005_SH01@t2"  # a different episode — pairwise is cross-batch
    manifest = build_pairwise_view(urn_a, urn_b)
    assert isinstance(manifest, CompositionManifest)
    assert manifest.kind == "CUT"
    assert manifest.members == [urn_a, urn_b]
    assert manifest.layout == {}  # the thin pairwise builder imposes no layout


def test_build_cut_composition_malformed_urn_raises():
    """Fail-loud at schema validation — a malformed member URN raises at construction."""
    with pytest.raises(ValidationError):
        build_cut_composition(["not-an-atom"])


def test_explicit_layout_is_honored():
    """The ``layout`` arg round-trips onto the manifest — an impl that drops it fails here."""
    urns = ["atom:EP001/beat/EP001_SH02@t0", "atom:EP001/beat/EP001_SH05@t0"]
    manifest = build_cut_composition(urns, layout={"slots": 4})
    assert manifest.layout == {"slots": 4}


# ── Cross-batch render milestone (REAL persisted panels in two batches) ─────────────


def test_cross_batch_pairwise_render_composites_a_next_to_b(tmp_path, monkeypatch):
    """The milestone: an atom persisted in batch A composited next to an atom in a
    DIFFERENT batch B, in member order — read back from the composited pixels."""
    project, project_root, urn_a, urn_b = _persist_cross_batch(tmp_path, monkeypatch)

    # Non-vacuity precondition: the two atoms genuinely resolve from DIFFERENT batches.
    scene_a = resolve_atom_version(urn_a, project).facets["spine_anchor"]["scene_id"]
    scene_b = resolve_atom_version(urn_b, project).facets["spine_anchor"]["scene_id"]
    assert scene_a != scene_b, (scene_a, scene_b)

    manifest = build_pairwise_view(urn_a, urn_b)
    assert manifest.kind == "CUT"
    assert manifest.members == [urn_a, urn_b]

    rel = bb.render_composition_view(
        manifest, project, out_path="prep/ep_001/storyboards/pairwise.png"
    )

    img = Image.open(project_root / rel).convert("RGB")
    img.load()
    width, height, cell_w, cell_h = _cell_geometry(4)  # 2 members → default slots=4
    assert img.size == (width, height)
    # cell-0 (top-left) = atom A from batch A (RED); cell-1 (top-right) = atom B from batch B (BLUE).
    assert img.getpixel((cell_w // 2, cell_h // 2)) == _RED
    assert img.getpixel((cell_w + cell_w // 2, cell_h // 2)) == _BLUE


def test_cross_batch_render_fires_zero_generation(tmp_path, monkeypatch):
    """ZERO generation: poison every generation/dispatch entrypoint the renderer's module
    exposes; the cross-batch render still succeeds (mirrors test_composition_render)."""
    project, project_root, urn_a, urn_b = _persist_cross_batch(tmp_path, monkeypatch)

    def _boom(*args, **kwargs):
        raise AssertionError(
            "cross-batch composition render must NEVER call a generation entrypoint"
        )

    monkeypatch.setattr(bb, "build_and_dispatch_board", _boom)
    monkeypatch.setattr(bb, "render_board_finish", _boom)
    monkeypatch.setattr(bb, "dispatch", _boom)

    rel = bb.render_composition_view(
        build_pairwise_view(urn_a, urn_b),
        project,
        out_path="prep/ep_001/storyboards/pairwise_zerogen.png",
    )

    img = Image.open(project_root / rel).convert("RGB")
    img.load()
    _w, _h, cell_w, cell_h = _cell_geometry(4)
    assert img.getpixel((cell_w // 2, cell_h // 2)) == _RED
    assert img.getpixel((cell_w + cell_w // 2, cell_h // 2)) == _BLUE
