# recoil/pipeline/tests/test_asset_ops.py
"""Pure-Python asset_ops layer. Wraps ref_resolver for reads, writes
filesystem + sidecar for mutations, logs every mutation to ops.log.jsonl.
"""

import pytest


@pytest.fixture
def project_root(tmp_path):
    """Synthetic project root under the v3 layout.

    asset_ops writes heroes to assets/{class}/{subject}/hero.{ext} and the
    ops log to _pipeline/state/visual/ops.log.jsonl (via ProjectPaths).
    """
    for cls in ("char", "loc", "prop"):
        (tmp_path / "assets" / cls).mkdir(parents=True)
    (tmp_path / "_pipeline" / "state" / "visual").mkdir(parents=True)
    return tmp_path


def _make_source(tmp_path, name="source.png"):
    src = tmp_path / name
    src.write_bytes(b"\x89PNG\r\n\x1a\n" + b"0" * 1024)
    return src


def test_set_hero_creates_canonical_file_and_sidecar(project_root, tmp_path):
    from recoil.pipeline._lib import asset_ops

    src = _make_source(tmp_path)
    asset_ops.set_hero(
        project_root=project_root,
        asset_type="character",
        asset_id="sadie",
        source=src,
    )
    hero = project_root / "assets" / "char" / "sadie" / "hero.png"
    sidecar = hero.parent / "_meta" / "hero.png.json"
    assert hero.exists()
    assert sidecar.exists()


def test_set_hero_with_phase_writes_phased_filename(project_root, tmp_path):
    from recoil.pipeline._lib import asset_ops

    src = _make_source(tmp_path)
    asset_ops.set_hero(
        project_root=project_root,
        asset_type="character",
        asset_id="sadie",
        source=src,
        phase="ph2",
    )
    phased = project_root / "assets" / "char" / "sadie" / "hero_ph2.png"
    assert phased.exists()


@pytest.mark.xfail(
    strict=True,
    reason="REC-154: asset_ops.set_hero writes non-conformant 'hero.{ext}' which "
    "ref_resolver cannot read; round-trip resolves to {}. Un-xfail when REC-154 lands.",
)
def test_set_hero_replaces_extension_collision(project_root, tmp_path):
    """The 9-round bug class: setting a .jpg hero must hide the previous .png hero."""
    from recoil.pipeline._lib import asset_ops
    from recoil.core import ref_resolver
    from recoil.core.paths import ProjectPaths

    src_png = _make_source(tmp_path, "old.png")
    src_jpg = _make_source(tmp_path, "new.jpg")

    asset_ops.set_hero(
        project_root=project_root,
        asset_type="character",
        asset_id="sadie",
        source=src_png,
    )
    asset_ops.set_hero(
        project_root=project_root,
        asset_type="character",
        asset_id="sadie",
        source=src_jpg,
    )

    refs = ref_resolver.resolve_character_refs(
        ProjectPaths.from_root(project_root), "sadie"
    )
    # Whatever the resolver returns, the hero should be the .jpg version
    # Accept either a dict with "hero" key OR a list where we find hero
    if isinstance(refs, dict):
        hero = refs.get("hero")
    else:
        hero = next(
            (r for r in refs if getattr(r, "name", "").startswith("hero")), None
        )
    assert hero is not None
    assert str(hero).endswith(".jpg"), f"expected .jpg hero, got {hero}"


def test_add_ref_appends_without_promoting(project_root, tmp_path):
    from recoil.pipeline._lib import asset_ops

    src = _make_source(tmp_path)
    asset_ops.add_ref(
        project_root=project_root,
        asset_type="character",
        asset_id="sadie",
        source=src,
    )
    canonical = project_root / "assets" / "char" / "sadie"
    assert any(canonical.glob("source*.png"))
    assert not (canonical / "hero.png").exists()


def test_delete_ref_removes_file_and_sidecar(project_root, tmp_path):
    from recoil.pipeline._lib import asset_ops

    src = _make_source(tmp_path)
    asset_ops.set_hero(
        project_root=project_root, asset_type="character", asset_id="sadie", source=src
    )

    asset_ops.delete_ref(
        project_root=project_root,
        asset_type="character",
        asset_id="sadie",
        filename="hero.png",
    )

    canonical = project_root / "assets" / "char" / "sadie"
    assert not (canonical / "hero.png").exists()
    assert not (canonical / "_meta" / "hero.png.json").exists()


@pytest.mark.xfail(
    strict=True,
    reason="REC-154: list_refs delegates to ref_resolver, which cannot read the "
    "non-conformant 'hero.{ext}' that set_hero writes; returns {}. "
    "Un-xfail when REC-154 lands.",
)
def test_list_refs_returns_something(project_root, tmp_path):
    """Shape-agnostic — just verify list_refs returns truthy after set_hero."""
    from recoil.pipeline._lib import asset_ops

    src = _make_source(tmp_path)
    asset_ops.set_hero(
        project_root=project_root, asset_type="character", asset_id="sadie", source=src
    )

    refs = asset_ops.list_refs(
        project_root=project_root, asset_type="character", asset_id="sadie"
    )
    # Could be dict, list, or something else — just check it's not empty/None
    assert refs is not None
    if isinstance(refs, dict):
        assert len(refs) > 0
    elif isinstance(refs, (list, tuple)):
        assert len(refs) > 0


def test_set_hero_writes_op_log_entry(project_root, tmp_path):
    from recoil.pipeline._lib import asset_ops
    import json

    src = _make_source(tmp_path)
    asset_ops.set_hero(
        project_root=project_root, asset_type="character", asset_id="sadie", source=src
    )

    log_path = project_root / "_pipeline" / "state" / "visual" / "ops.log.jsonl"
    assert log_path.exists()
    lines = [json.loads(l) for l in log_path.read_text().strip().split("\n")]
    pending = [l for l in lines if l["status"] == "pending"]
    completed = [l for l in lines if l["status"] == "completed"]
    assert len(pending) == 1
    assert len(completed) == 1
    assert pending[0]["name"] == "asset.hero.set"
