#!/usr/bin/env python3
"""Generate and verify deterministic repo22 G5 compression fixtures."""

from __future__ import annotations

import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import struct
import subprocess
from typing import Any

from erofs_fixture import ErofsImage, Inode, SUPER, sha256


BLOCK_SIZE = 4096
FIXED_UUID = "00000000-0000-0000-0000-000000000000"
FEATURE_INCOMPAT_COMPR_HEAD2 = 0x00000008
LAYOUT_COMPRESSED_FULL = 1
LAYOUT_COMPRESSED_COMPACT = 3
Z_EROFS_ADVISE_COMPACTED_2B = 0x0001
Z_EROFS_ADVISE_BIG_PCLUSTER_1 = 0x0002
Z_EROFS_ADVISE_BIG_PCLUSTER_2 = 0x0004
Z_EROFS_ADVISE_INLINE_PCLUSTER = 0x0008
Z_EROFS_ADVISE_INTERLACED_PCLUSTER = 0x0010
Z_EROFS_LCLUSTER_TYPE_MASK = 0x0003
Z_EROFS_LCLUSTER_TYPE_PLAIN = 0
Z_EROFS_LCLUSTER_TYPE_HEAD1 = 1
Z_EROFS_LCLUSTER_TYPE_NONHEAD = 2
Z_EROFS_LCLUSTER_TYPE_HEAD2 = 3
Z_EROFS_LI_PARTIAL_REF = 0x8000
Z_EROFS_LI_D0_CBLKCNT = 0x0800
ALGORITHMS = {
    "lz4": 0,
    "lzma": 1,
    "deflate": 2,
    "zstd": 3,
}


class FixtureError(RuntimeError):
    pass


def align(value: int, alignment: int) -> int:
    return (value + alignment - 1) & -alignment


def relative(path: Path, root: Path) -> str:
    return str(path.relative_to(root))


def run(
    command: list[str],
    *,
    log: Path | None = None,
    check: bool = True,
) -> subprocess.CompletedProcess[str]:
    result = subprocess.run(
        command,
        check=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
    )
    if log is not None:
        log.write_text(
            "$ " + " ".join(command) + "\n" + result.stdout,
            encoding="utf-8",
        )
    if check and result.returncode != 0:
        raise FixtureError(
            f"command failed ({result.returncode}): {' '.join(command)}\n"
            f"{result.stdout}"
        )
    return result


def write_repeated(path: Path, size: int, label: str) -> None:
    token = (label.encode("ascii") + b"\n") * 64
    chunk = (token * ((1024 * 1024 + len(token) - 1) // len(token)))[
        : 1024 * 1024
    ]
    remaining = size
    with path.open("wb") as output:
        while remaining:
            amount = min(remaining, len(chunk))
            output.write(chunk[:amount])
            remaining -= amount


def mixed_chunk(seed: str, chunk_index: int, size: int) -> bytes:
    output = bytearray()
    block_index = 0
    while len(output) < size:
        amount = min(BLOCK_SIZE, size - len(output))
        if block_index % 4 == 0:
            material = hashlib.shake_256(
                f"{seed}:{chunk_index}:{block_index}".encode("ascii")
            ).digest(amount)
        else:
            token = (
                f"repo22-g5:{seed}:{chunk_index % 7}:{block_index % 11}\n"
            ).encode("ascii")
            material = (token * ((amount + len(token) - 1) // len(token)))[
                :amount
            ]
        output.extend(material)
        block_index += 1
    return bytes(output)


def write_mixed(path: Path, size: int, seed: str) -> None:
    chunk_size = 1024 * 1024
    remaining = size
    index = 0
    with path.open("wb") as output:
        while remaining:
            amount = min(remaining, chunk_size)
            output.write(mixed_chunk(seed, index, amount))
            remaining -= amount
            index += 1


def write_interlaced(path: Path, size: int) -> None:
    remaining = size
    extent = 0
    with path.open("wb") as output:
        while remaining:
            compressible_size = min(remaining, 16384)
            token = f"repo22-interlaced-{extent % 13:02d}\n".encode("ascii")
            output.write(
                (token * ((compressible_size + len(token) - 1) // len(token)))[
                    :compressible_size
                ]
            )
            remaining -= compressible_size
            if not remaining:
                break
            random_size = min(remaining, BLOCK_SIZE)
            output.write(
                hashlib.shake_256(
                    f"repo22-interlaced-random-{extent}".encode("ascii")
                ).digest(random_size)
            )
            remaining -= random_size
            extent += 1


def create_sources(source_root: Path) -> None:
    directories = {
        "lz4": source_root / "lz4",
        "large": source_root / "large",
        "levels": source_root / "levels",
        "lzma-large": source_root / "lzma-large",
        "microlzma": source_root / "microlzma",
        "partial": source_root / "partial",
        "partial-deflate": source_root / "partial-deflate",
        "shape": source_root / "shape",
        "ztail": source_root / "ztail",
    }
    for directory in directories.values():
        directory.mkdir(parents=True)

    write_mixed(directories["lz4"] / "compressed.bin", 8 * 1024 * 1024, "lz4")
    write_mixed(
        directories["large"] / "large.bin", 256 * 1024 * 1024, "large"
    )
    write_mixed(
        directories["levels"] / "level.dat", 8 * 1024 * 1024, "levels"
    )
    write_mixed(
        directories["lzma-large"] / "large.bin",
        100 * 1024 * 1024 + 1,
        "lzma-large",
    )
    (directories["microlzma"] / "one-byte.bin").write_bytes(b"G")
    write_repeated(
        directories["microlzma"] / "block-4k.bin", BLOCK_SIZE, "microlzma-4k"
    )
    write_repeated(
        directories["microlzma"] / "boundary-16k.bin",
        4 * BLOCK_SIZE,
        "microlzma-16k",
    )
    write_repeated(
        directories["partial"] / "a.dat", 1024 * 1024, "partial-stream"
    )
    source_a = directories["partial"] / "a.dat"
    source_b = directories["partial"] / "b.dat"
    with source_a.open("rb") as source, source_b.open("wb") as target:
        target.write(source.read(700000))
    write_mixed(
        directories["partial"] / "control.bin",
        32768,
        "partial-control",
    )
    write_repeated(
        directories["partial-deflate"] / "a.dat",
        1024 * 1024,
        "partial-stream",
    )
    with (directories["partial-deflate"] / "a.dat").open("rb") as source, (
        directories["partial-deflate"] / "b.dat"
    ).open("wb") as target:
        target.write(source.read(100000))
    write_mixed(
        directories["partial-deflate"] / "control.bin",
        32768,
        "partial-control",
    )
    write_interlaced(directories["shape"] / "shape.dat", 1024 * 1024)
    write_repeated(directories["ztail"] / "inline.dat", 131071, "ztail-inline")
    write_mixed(
        directories["ztail"] / "exact-pcluster.dat", BLOCK_SIZE, "ztail-exact"
    )
    write_repeated(
        directories["ztail"] / "one-byte-tail.dat",
        BLOCK_SIZE + 1,
        "ztail-one-byte",
    )
    write_repeated(
        directories["ztail"] / "max-tail.dat",
        BLOCK_SIZE * 2 - 1,
        "ztail-max",
    )
    (directories["ztail"] / "zero-tail.dat").write_bytes(b"")


def source_inventory(source_root: Path) -> list[dict[str, Any]]:
    inventory = []
    for path in sorted(item for item in source_root.rglob("*") if item.is_file()):
        inventory.append(
            {
                "path": relative(path, source_root.parent),
                "size": path.stat().st_size,
                "sha256": sha256(path),
            }
        )
    return inventory


def mkfs_image(
    *,
    output_root: Path,
    image_name: str,
    source: Path,
    options: list[str],
    command_log: list[dict[str, Any]],
) -> Path:
    image = output_root / "images" / image_name
    command = [
        "mkfs.erofs",
        "--workers=1",
        "--sort=path",
        "-T0",
        "--all-time",
        f"-U{FIXED_UUID}",
        "--all-root",
        *options,
        str(image),
        str(source),
    ]
    log = output_root / "logs" / f"mkfs-{image.stem}.log"
    run(command, log=log)
    command_log.append(
        {
            "image": relative(image, output_root),
            "source": relative(source, output_root),
            "options": options,
            "log": relative(log, output_root),
        }
    )
    parsed = ErofsImage.load(image)
    parsed.validate_superblock()
    return image


def inode_for(image: ErofsImage, path: str) -> Inode:
    _, entry = image.resolve_root_entry(path)
    return image.inode(entry.nid)


def map_header(image: ErofsImage, inode: Inode) -> dict[str, int]:
    if inode.layout not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT):
        raise FixtureError(f"inode layout {inode.layout} is not compressed")
    offset = align(inode.offset + inode.inode_size + inode.xattr_size, 8)
    if offset > len(image.data) - 8:
        raise FixtureError("compressed map header exceeds image")
    raw0, advise, algorithm, clusterbits = struct.unpack_from(
        "<IHBB", image.data, offset
    )
    return {
        "offset": offset,
        "raw0": raw0,
        "advise": advise,
        "algorithm_raw": algorithm,
        "head1_algorithm": algorithm & 0x0F,
        "head2_algorithm": algorithm >> 4,
        "clusterbits_raw": clusterbits,
        "lcluster_bits": image.block_bits + (clusterbits & 0x07),
        "idata_size": raw0 >> 16,
    }


def full_index_offset(image: ErofsImage, inode: Inode) -> int:
    header = map_header(image, inode)
    return align(header["offset"] + 8, 8) + 8


def full_record(image: ErofsImage, inode: Inode, lcn: int) -> dict[str, int]:
    if inode.layout != LAYOUT_COMPRESSED_FULL:
        raise FixtureError("full record requested from a non-full inode")
    offset = full_index_offset(image, inode) + lcn * 8
    if offset > len(image.data) - 8:
        raise FixtureError("full index record exceeds image")
    advise, clusterofs, word = struct.unpack_from("<HHI", image.data, offset)
    return {
        "offset": offset,
        "advise": advise,
        "type": advise & Z_EROFS_LCLUSTER_TYPE_MASK,
        "partial_ref": int(bool(advise & Z_EROFS_LI_PARTIAL_REF)),
        "clusterofs": clusterofs,
        "word": word,
        "pblk": word,
        "delta0": word & 0xFFFF,
        "delta1": word >> 16,
    }


def full_index_summary(image: ErofsImage, inode: Inode) -> dict[str, Any]:
    header = map_header(image, inode)
    count = (inode.size + (1 << header["lcluster_bits"]) - 1) >> header[
        "lcluster_bits"
    ]
    types: Counter[int] = Counter()
    heads = []
    partial_heads = []
    for lcn in range(count):
        record = full_record(image, inode, lcn)
        types[record["type"]] += 1
        if record["type"] in (
            Z_EROFS_LCLUSTER_TYPE_PLAIN,
            Z_EROFS_LCLUSTER_TYPE_HEAD1,
            Z_EROFS_LCLUSTER_TYPE_HEAD2,
        ):
            if len(heads) < 16:
                heads.append(
                    {
                        "lcn": lcn,
                        "offset": record["offset"],
                        "type": record["type"],
                        "pblk": record["pblk"],
                        "clusterofs": record["clusterofs"],
                    }
                )
            if record["partial_ref"]:
                partial_heads.append(
                    {
                        "lcn": lcn,
                        "offset": record["offset"],
                        "pblk": record["pblk"],
                    }
                )
    return {
        "record_count": count,
        "type_counts": {str(key): value for key, value in sorted(types.items())},
        "first_heads": heads,
        "partial_heads": partial_heads,
    }


EXTENT_RE = re.compile(
    r"^\s*\d+:\s*(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*:"
    r"\s*(\d+)\.\.\s*(\d+)\s*\|\s*(\d+)\s*$"
)


def dump_extents(image: Path, path: str, log: Path | None = None) -> list[dict[str, int]]:
    result = run(
        ["dump.erofs", f"--path={path}", "-e", str(image)],
        log=log,
    )
    extents = []
    for line in result.stdout.splitlines():
        match = EXTENT_RE.match(line)
        if match is None:
            continue
        logical_start, logical_end, logical_length, physical_start, physical_end, physical_length = (
            int(value) for value in match.groups()
        )
        if logical_end - logical_start != logical_length:
            raise FixtureError("dump.erofs reported an inconsistent logical extent")
        if physical_end - physical_start != physical_length:
            raise FixtureError("dump.erofs reported an inconsistent physical extent")
        extents.append(
            {
                "logical_start": logical_start,
                "logical_end": logical_end,
                "logical_length": logical_length,
                "physical_start": physical_start,
                "physical_end": physical_end,
                "physical_length": physical_length,
            }
        )
    if not extents:
        raise FixtureError(f"dump.erofs reported no extents for {image}:{path}")
    return extents


def extent_summary(extents: list[dict[str, int]]) -> dict[str, Any]:
    transitions = []
    for index in range(1, len(extents)):
        previous = extents[index - 1]
        current = extents[index]
        previous_plain = previous["logical_length"] == previous["physical_length"]
        current_plain = current["logical_length"] == current["physical_length"]
        if previous_plain != current_plain:
            transitions.append(
                {
                    "logical_offset": current["logical_start"],
                    "previous_plain": previous_plain,
                    "current_plain": current_plain,
                }
            )
    return {
        "count": len(extents),
        "first": extents[:8],
        "last": extents[-1],
        "max_logical_length": max(item["logical_length"] for item in extents),
        "max_physical_length": max(item["physical_length"] for item in extents),
        "plain_count": sum(
            item["physical_length"] != 0
            and item["logical_length"] == item["physical_length"]
            for item in extents
        ),
        "compressed_count": sum(
            item["physical_length"] != 0
            and item["logical_length"] > item["physical_length"]
            for item in extents
        ),
        "hole_or_fragment_count": sum(
            item["physical_length"] == 0 for item in extents
        ),
        "transitions": transitions[:8],
    }


def inspect_path(
    output_root: Path,
    image_path: Path,
    path: str,
    *,
    write_log: bool = True,
) -> dict[str, Any]:
    image = ErofsImage.load(image_path)
    image.validate_superblock()
    inode = inode_for(image, path)
    details: dict[str, Any] = {
        "path": path,
        "nid": inode.nid,
        "inode_offset": inode.offset,
        "inode_size": inode.inode_size,
        "xattr_size": inode.xattr_size,
        "layout": inode.layout,
        "size": inode.size,
    }
    if inode.layout in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT):
        details["map_header"] = map_header(image, inode)
        log = None
        if write_log:
            safe_path = path.removeprefix("/").replace("/", "-")
            log = output_root / "logs" / f"dump-{image_path.stem}-{safe_path}.log"
            details["dump_log"] = relative(log, output_root)
        details["extents"] = extent_summary(dump_extents(image_path, path, log))
        if inode.layout == LAYOUT_COMPRESSED_FULL:
            details["full_index"] = full_index_summary(image, inode)
    return details


def assert_compressed(
    details: dict[str, Any],
    *,
    algorithm: str,
    layout: int | None = None,
) -> None:
    if details["layout"] not in (LAYOUT_COMPRESSED_FULL, LAYOUT_COMPRESSED_COMPACT):
        raise FixtureError(f"{details['path']} was not compressed")
    if layout is not None and details["layout"] != layout:
        raise FixtureError(
            f"{details['path']} layout {details['layout']} != expected {layout}"
        )
    actual = details["map_header"]["head1_algorithm"]
    if actual != ALGORITHMS[algorithm]:
        raise FixtureError(
            f"{details['path']} algorithm {actual} != {ALGORITHMS[algorithm]}"
        )


def patch_partial_reference(
    base: Path,
    output: Path,
    *,
    algorithm: str,
) -> dict[str, Any]:
    image = ErofsImage.load(base)
    inode_a = inode_for(image, "/a.dat")
    inode_b = inode_for(image, "/b.dat")
    if inode_a.layout != LAYOUT_COMPRESSED_FULL or inode_b.layout != LAYOUT_COMPRESSED_FULL:
        raise FixtureError("partial-reference transform requires full indexes")
    header_a = map_header(image, inode_a)
    header_b = map_header(image, inode_b)
    expected_algorithm = ALGORITHMS[algorithm]
    if (
        header_a["head1_algorithm"] != expected_algorithm
        or header_b["head1_algorithm"] != expected_algorithm
    ):
        raise FixtureError("partial-reference source algorithm mismatch")
    record_a = full_record(image, inode_a, 0)
    record_b = full_record(image, inode_b, 0)
    record_a_next = full_record(image, inode_a, 1)
    record_b_next = full_record(image, inode_b, 1)
    if record_a["type"] != Z_EROFS_LCLUSTER_TYPE_HEAD1:
        raise FixtureError("a.dat first record is not HEAD1")
    if record_b["type"] != Z_EROFS_LCLUSTER_TYPE_HEAD1:
        raise FixtureError("b.dat first record is not HEAD1")
    if record_a["pblk"] == record_b["pblk"]:
        raise FixtureError("mkfs unexpectedly reused the partial pcluster")
    if record_b["advise"] & Z_EROFS_LI_PARTIAL_REF:
        raise FixtureError("b.dat was already marked partial")
    if inode_b.size >= inode_a.size:
        raise FixtureError("partial file is not shorter than the complete stream")
    if (
        record_a_next["type"] != Z_EROFS_LCLUSTER_TYPE_NONHEAD
        or record_b_next["type"] != Z_EROFS_LCLUSTER_TYPE_NONHEAD
        or not record_a_next["delta0"] & Z_EROFS_LI_D0_CBLKCNT
        or not record_b_next["delta0"] & Z_EROFS_LI_D0_CBLKCNT
    ):
        raise FixtureError("partial source lacks a first-pcluster block count")

    struct.pack_into(
        "<H", image.data, record_b["offset"], record_b["advise"] | Z_EROFS_LI_PARTIAL_REF
    )
    struct.pack_into("<I", image.data, record_b["offset"] + 4, record_a["pblk"])
    struct.pack_into(
        "<H", image.data, record_b_next["offset"] + 4, record_a_next["delta0"]
    )
    image.update_checksum()
    image.save(output)

    reopened = ErofsImage.load(output)
    reopened.validate_superblock()
    new_a = full_record(reopened, inode_for(reopened, "/a.dat"), 0)
    new_b = full_record(reopened, inode_for(reopened, "/b.dat"), 0)
    new_b_next = full_record(reopened, inode_for(reopened, "/b.dat"), 1)
    if not new_b["partial_ref"] or new_b["pblk"] != new_a["pblk"]:
        raise FixtureError("partial-reference transform did not survive reopen")
    if new_b_next["delta0"] != record_a_next["delta0"]:
        raise FixtureError("partial-reference pcluster length did not survive reopen")
    return {
        "algorithm": algorithm,
        "a_nid": inode_a.nid,
        "a_size": inode_a.size,
        "a_index_offset": record_a["offset"],
        "a_pblk": record_a["pblk"],
        "b_nid": inode_b.nid,
        "b_size": inode_b.size,
        "b_index_offset": record_b["offset"],
        "b_old_advise": record_b["advise"],
        "b_new_advise": new_b["advise"],
        "b_old_pblk": record_b["pblk"],
        "b_new_pblk": new_b["pblk"],
        "b_old_compressed_blocks": record_b_next["delta0"]
        & ~Z_EROFS_LI_D0_CBLKCNT,
        "b_new_compressed_blocks": new_b_next["delta0"]
        & ~Z_EROFS_LI_D0_CBLKCNT,
        "source_stream_reused": True,
        "partial_ref": True,
    }


def patch_head2(base: Path, output: Path) -> dict[str, Any]:
    image = ErofsImage.load(base)
    inode = inode_for(image, "/shape.dat")
    if inode.layout != LAYOUT_COMPRESSED_FULL:
        raise FixtureError("HEAD2 transform requires full indexes")
    header = map_header(image, inode)
    if header["head1_algorithm"] != ALGORITHMS["lz4"]:
        raise FixtureError("HEAD2 base is not LZ4")
    if not header["advise"] & Z_EROFS_ADVISE_BIG_PCLUSTER_1:
        raise FixtureError("HEAD2 base lacks HEAD1 big-pcluster advise")

    count = (inode.size + (1 << header["lcluster_bits"]) - 1) >> header[
        "lcluster_bits"
    ]
    target_lcn = None
    target = None
    for lcn in range(count):
        record = full_record(image, inode, lcn)
        if record["type"] == Z_EROFS_LCLUSTER_TYPE_HEAD1:
            target_lcn = lcn
            target = record
            break
    if target_lcn is None or target is None:
        raise FixtureError("HEAD2 base has no HEAD1 record")

    old_incompat = image.feature_incompat
    old_advise = header["advise"]
    old_algorithm = header["algorithm_raw"]
    new_advise = old_advise | Z_EROFS_ADVISE_BIG_PCLUSTER_2
    new_algorithm = (old_algorithm & 0x0F) | ((old_algorithm & 0x0F) << 4)
    new_record_advise = (
        target["advise"] & ~Z_EROFS_LCLUSTER_TYPE_MASK
    ) | Z_EROFS_LCLUSTER_TYPE_HEAD2
    image.put_u32(SUPER + 80, old_incompat | FEATURE_INCOMPAT_COMPR_HEAD2)
    struct.pack_into("<H", image.data, header["offset"] + 4, new_advise)
    image.data[header["offset"] + 6] = new_algorithm
    struct.pack_into("<H", image.data, target["offset"], new_record_advise)
    image.update_checksum()
    image.save(output)

    reopened = ErofsImage.load(output)
    reopened.validate_superblock()
    new_inode = inode_for(reopened, "/shape.dat")
    new_header = map_header(reopened, new_inode)
    new_record = full_record(reopened, new_inode, target_lcn)
    if not reopened.feature_incompat & FEATURE_INCOMPAT_COMPR_HEAD2:
        raise FixtureError("HEAD2 incompat feature did not survive reopen")
    if not new_header["advise"] & Z_EROFS_ADVISE_BIG_PCLUSTER_2:
        raise FixtureError("HEAD2 big-pcluster advise did not survive reopen")
    if new_header["head2_algorithm"] != ALGORITHMS["lz4"]:
        raise FixtureError("HEAD2 algorithm nibble did not survive reopen")
    if new_record["type"] != Z_EROFS_LCLUSTER_TYPE_HEAD2:
        raise FixtureError("HEAD2 index type did not survive reopen")
    return {
        "nid": inode.nid,
        "header_offset": header["offset"],
        "index_offset": target["offset"],
        "target_lcn": target_lcn,
        "pblk": target["pblk"],
        "feature_incompat_before": old_incompat,
        "feature_incompat_after": reopened.feature_incompat,
        "header_advise_before": old_advise,
        "header_advise_after": new_header["advise"],
        "algorithm_raw_before": old_algorithm,
        "algorithm_raw_after": new_header["algorithm_raw"],
        "record_advise_before": target["advise"],
        "record_advise_after": new_record["advise"],
    }


def corrupt_target_extent(
    image_path: Path,
    output: Path,
    *,
    path: str,
    algorithm: str,
) -> dict[str, Any]:
    source = ErofsImage.load(image_path)
    inode = inode_for(source, path)
    header = map_header(source, inode)
    if header["head1_algorithm"] != ALGORITHMS[algorithm]:
        raise FixtureError("corruption target algorithm mismatch")
    extents = dump_extents(image_path, path)
    target = next(
        (
            extent
            for extent in extents
            if extent["physical_length"] > 0
            and extent["logical_length"] > extent["physical_length"]
        ),
        None,
    )
    if target is None:
        raise FixtureError("no compressed extent available for corruption")
    start = target["physical_start"]
    end = target["physical_end"]
    if start < source.checksum_end or end > len(source.data):
        raise FixtureError("target compressed extent is outside payload bounds")
    payload = source.data[start:end]
    nonzero = [index for index, value in enumerate(payload) if value != 0]
    if len(nonzero) < 16:
        raise FixtureError("target compressed extent has too little encoded data")
    patch_start = start + nonzero[0]
    patch_length = min(64, end - patch_start)
    old = bytes(source.data[patch_start : patch_start + patch_length])
    for index in range(patch_start, patch_start + patch_length):
        source.data[index] ^= 0xA5
    new = bytes(source.data[patch_start : patch_start + patch_length])
    if old == new:
        raise AssertionError("compressed corruption did not change bytes")
    if not source.checksum_valid():
        raise FixtureError("payload corruption invalidated the superblock checksum")
    source.save(output)
    reopened = ErofsImage.load(output)
    reopened.validate_superblock()
    return {
        "path": path,
        "algorithm": algorithm,
        "algorithm_id": ALGORITHMS[algorithm],
        "nid": inode.nid,
        "map_header_offset": header["offset"],
        "extent_logical_start": target["logical_start"],
        "extent_logical_length": target["logical_length"],
        "pcluster_start": start,
        "pcluster_length": target["physical_length"],
        "patch_offset": patch_start,
        "patch_length": patch_length,
        "old_sha256": hashlib.sha256(old).hexdigest(),
        "new_sha256": hashlib.sha256(new).hexdigest(),
        "superblock_checksum_valid": True,
    }


def write_checksum_file(root: Path, paths: list[Path], name: str) -> None:
    with (root / name).open("w", encoding="ascii") as output:
        for path in sorted(paths):
            output.write(f"{sha256(path)}  {relative(path, root)}\n")


def explicit_probe(
    output_root: Path,
    attempt_image: Path,
    utils_source: Path | None,
) -> dict[str, Any]:
    attempt = inspect_path(output_root, attempt_image, "/shape.dat")
    header = attempt["map_header"]
    tokens = [
        "Z_EROFS_ADVISE_EXTENTS",
        "z_erofs_extent_recsize",
        "struct z_erofs_extent {",
    ]
    matches: dict[str, list[str]] = {token: [] for token in tokens}
    if utils_source is not None:
        for base in (utils_source / "include", utils_source / "lib"):
            if not base.is_dir():
                continue
            for path in sorted(item for item in base.rglob("*") if item.is_file()):
                try:
                    content = path.read_text(encoding="utf-8", errors="ignore")
                except OSError:
                    continue
                for token in tokens:
                    if token in content:
                        matches[token].append(str(path))
    return {
        "status": "SHELVED",
        "attempt_image": relative(attempt_image, output_root),
        "attempt_layout": attempt["layout"],
        "attempt_map_header_offset": header["offset"],
        "attempt_h_advise": header["advise"],
        "attempt_explicit_bit": bool(
            attempt["layout"] == LAYOUT_COMPRESSED_FULL
            and header["advise"] & Z_EROFS_ADVISE_COMPACTED_2B
        ),
        "mapped_payload_generated": False,
        "erofs_utils_source": str(utils_source) if utils_source else None,
        "source_token_matches": matches,
        "structured_transform_attempt": (
            "Refused: converting legacy lcluster indexes into variable-size "
            "extent records requires relocating subsequent metadata and payload; "
            "the helper cannot validate a mapped result with erofs-utils 1.8.6."
        ),
    }


def build_fixtures(args: argparse.Namespace) -> None:
    output_root = args.output.resolve()
    if output_root.exists():
        raise FixtureError(f"output already exists: {output_root}")
    (output_root / "images").mkdir(parents=True)
    (output_root / "logs").mkdir()
    (output_root / "sources").mkdir()
    (output_root / "base-images").mkdir()
    create_sources(output_root / "sources")

    version = run(["mkfs.erofs", "-V"]).stdout.strip()
    if "1.8.6" not in version:
        raise FixtureError(f"mkfs.erofs 1.8.6 is required, got: {version}")

    commands: list[dict[str, Any]] = []
    images: dict[str, Path] = {}
    targets: dict[str, dict[str, Any]] = {}

    def generate(
        name: str,
        source_dir: str,
        options: list[str],
        path: str,
        algorithm: str,
        layout: int | None = None,
    ) -> dict[str, Any]:
        image = mkfs_image(
            output_root=output_root,
            image_name=name,
            source=output_root / "sources" / source_dir,
            options=options,
            command_log=commands,
        )
        images[name] = image
        details = inspect_path(output_root, image, path)
        assert_compressed(details, algorithm=algorithm, layout=layout)
        targets[f"{name}:{path}"] = details
        return details

    compact_4k = generate(
        "lz4-compact-4k.erofs",
        "lz4",
        ["-zlz4", "-C4096"],
        "/compressed.bin",
        "lz4",
        LAYOUT_COMPRESSED_COMPACT,
    )
    full_4k = generate(
        "lz4-full-4k.erofs",
        "lz4",
        ["-zlz4", "-C4096", "-Elegacy-compress"],
        "/compressed.bin",
        "lz4",
        LAYOUT_COMPRESSED_FULL,
    )
    compact_64k = generate(
        "lz4-compact-64k.erofs",
        "lz4",
        ["-zlz4", "-C65536"],
        "/compressed.bin",
        "lz4",
        LAYOUT_COMPRESSED_COMPACT,
    )
    compact_256k = generate(
        "lz4-compact-256k.erofs",
        "lz4",
        ["-zlz4", "-C262144"],
        "/compressed.bin",
        "lz4",
        LAYOUT_COMPRESSED_COMPACT,
    )
    generate(
        "lz4-large.erofs",
        "large",
        ["-zlz4", "-C65536"],
        "/large.bin",
        "lz4",
    )
    ztail = generate(
        "lz4-ztail.erofs",
        "ztail",
        ["-zlz4", "-C4096", "-Eztailpacking"],
        "/inline.dat",
        "lz4",
    )
    for path in (
        "/exact-pcluster.dat",
        "/one-byte-tail.dat",
        "/max-tail.dat",
        "/zero-tail.dat",
    ):
        details = inspect_path(output_root, images["lz4-ztail.erofs"], path)
        targets[f"lz4-ztail.erofs:{path}"] = details

    if not compact_4k["map_header"]["advise"] & Z_EROFS_ADVISE_COMPACTED_2B:
        raise FixtureError("4K compact image lacks compacted index advise")
    if full_4k["map_header"]["advise"] & Z_EROFS_ADVISE_COMPACTED_2B:
        raise FixtureError("full-index image advertises compacted indexes")
    for details, requested in ((compact_64k, 65536), (compact_256k, 262144)):
        header = details["map_header"]
        if not header["advise"] & Z_EROFS_ADVISE_BIG_PCLUSTER_1:
            raise FixtureError(f"{requested} image lacks big-pcluster advise")
        if details["extents"]["max_physical_length"] <= BLOCK_SIZE:
            raise FixtureError(f"{requested} image has no multi-block pcluster")
    if not ztail["map_header"]["advise"] & Z_EROFS_ADVISE_INLINE_PCLUSTER:
        raise FixtureError("ztailpacking target lacks inline-pcluster advise")
    if ztail["map_header"]["idata_size"] == 0:
        raise FixtureError("ztailpacking target has zero inline encoded size")

    for level in (1, 6, 9):
        generate(
            f"deflate-level{level}.erofs",
            "levels",
            [f"-zdeflate,level={level}", "-C65536", "-Elegacy-compress"],
            "/level.dat",
            "deflate",
            LAYOUT_COMPRESSED_FULL,
        )
    for level in (1, 15, 22):
        generate(
            f"zstd-level{level}.erofs",
            "levels",
            [f"-zzstd,level={level}", "-C65536", "-Elegacy-compress"],
            "/level.dat",
            "zstd",
            LAYOUT_COMPRESSED_FULL,
        )
    generate(
        "lzma-level6.erofs",
        "levels",
        ["-zlzma,level=6", "-C65536", "-Elegacy-compress"],
        "/level.dat",
        "lzma",
        LAYOUT_COMPRESSED_FULL,
    )
    generate(
        "lzma-large.erofs",
        "lzma-large",
        ["-zlzma,level=6", "-C65536", "-Elegacy-compress"],
        "/large.bin",
        "lzma",
        LAYOUT_COMPRESSED_FULL,
    )
    generate(
        "microlzma-edge.erofs",
        "microlzma",
        ["-zlzma,level=6", "-C4096", "-Elegacy-compress"],
        "/boundary-16k.bin",
        "lzma",
        LAYOUT_COMPRESSED_FULL,
    )
    for path in ("/one-byte.bin", "/block-4k.bin"):
        details = inspect_path(output_root, images["microlzma-edge.erofs"], path)
        targets[f"microlzma-edge.erofs:{path}"] = details

    partials: dict[str, Any] = {}
    corruptions: dict[str, Any] = {}
    for algorithm, compressor in (
        ("deflate", "-zdeflate,level=1"),
        ("zstd", "-zzstd,level=1"),
        ("lzma", "-zlzma,level=6"),
    ):
        base_name = f"{algorithm}-partial-base.erofs"
        base = mkfs_image(
            output_root=output_root,
            image_name=f"../base-images/{base_name}",
            source=output_root
            / "sources"
            / ("partial-deflate" if algorithm == "deflate" else "partial"),
            options=[compressor, "-C1048576", "-Elegacy-compress"],
            command_log=commands,
        )
        valid = output_root / "images" / f"{algorithm}-partial-ref.erofs"
        partials[algorithm] = patch_partial_reference(
            base, valid, algorithm=algorithm
        )
        images[valid.name] = valid
        for path in ("/a.dat", "/b.dat"):
            details = inspect_path(output_root, valid, path)
            assert_compressed(
                details,
                algorithm=algorithm,
                layout=LAYOUT_COMPRESSED_FULL,
            )
            targets[f"{valid.name}:{path}"] = details
        corrupt = output_root / "images" / f"{algorithm}-partial-ref-corrupt.erofs"
        corruptions[algorithm] = corrupt_target_extent(
            valid,
            corrupt,
            path="/a.dat",
            algorithm=algorithm,
        )
        images[corrupt.name] = corrupt

    head2_base = mkfs_image(
        output_root=output_root,
        image_name="../base-images/head2-base.erofs",
        source=output_root / "sources" / "shape",
        options=["-zlz4", "-C65536", "-Elegacy-compress"],
        command_log=commands,
    )
    head2 = output_root / "images" / "head2.erofs"
    head2_transform = patch_head2(head2_base, head2)
    images[head2.name] = head2
    head2_details = inspect_path(output_root, head2, "/shape.dat")
    assert_compressed(
        head2_details,
        algorithm="lz4",
        layout=LAYOUT_COMPRESSED_FULL,
    )
    targets[f"{head2.name}:/shape.dat"] = head2_details
    head2_corrupt = output_root / "images" / "head2-corrupt.erofs"
    corruptions["head2"] = corrupt_target_extent(
        head2,
        head2_corrupt,
        path="/shape.dat",
        algorithm="lz4",
    )
    images[head2_corrupt.name] = head2_corrupt

    interlaced = generate(
        "interlaced.erofs",
        "shape",
        ["-zlz4", "-C4096", "-Efragments"],
        "/shape.dat",
        "lz4",
    )
    if not interlaced["map_header"]["advise"] & Z_EROFS_ADVISE_INTERLACED_PCLUSTER:
        raise FixtureError("interlaced target lacks interlaced advise")
    if not interlaced["extents"]["transitions"]:
        raise FixtureError("interlaced target has no compressed/plain transition")

    extent_attempt = generate(
        "extent-attempt.erofs",
        "shape",
        [
            "-zlz4",
            "-C65536",
            "-Elegacy-compress",
            "--max-extent-bytes=65536",
        ],
        "/shape.dat",
        "lz4",
        LAYOUT_COMPRESSED_FULL,
    )
    if extent_attempt["map_header"]["advise"] & Z_EROFS_ADVISE_COMPACTED_2B:
        raise FixtureError("extent attempt unexpectedly selected the explicit bit")

    image_paths = sorted(set(images.values()))
    source_paths = sorted(
        path for path in (output_root / "sources").rglob("*") if path.is_file()
    )
    write_checksum_file(output_root, image_paths, "SHA256SUMS")
    write_checksum_file(output_root, source_paths, "SOURCE-SHA256SUMS")

    manifest = {
        "schema": 1,
        "mkfs_version": version,
        "fixed_uuid": FIXED_UUID,
        "source_inventory": source_inventory(output_root / "sources"),
        "commands": commands,
        "images": {
            relative(path, output_root): {
                "size": path.stat().st_size,
                "sha256": sha256(path),
            }
            for path in image_paths
        },
        "targets": targets,
        "partial_references": partials,
        "corruptions": corruptions,
        "head2_transform": head2_transform,
        "explicit_extent": explicit_probe(
            output_root,
            images["extent-attempt.erofs"],
            args.erofs_utils_source.resolve()
            if args.erofs_utils_source is not None
            else None,
        ),
    }
    (output_root / "fixture-manifest.json").write_text(
        json.dumps(manifest, indent=2, sort_keys=True) + "\n",
        encoding="ascii",
    )
    verify_output(output_root)


def verify_output(output_root: Path) -> None:
    output_root = output_root.resolve()
    manifest_path = output_root / "fixture-manifest.json"
    manifest = json.loads(manifest_path.read_text(encoding="ascii"))
    for path_text, expected in manifest["images"].items():
        path = output_root / path_text
        if not path.is_file():
            raise FixtureError(f"missing image: {path_text}")
        if path.stat().st_size != expected["size"]:
            raise FixtureError(f"image size changed: {path_text}")
        if sha256(path) != expected["sha256"]:
            raise FixtureError(f"image hash changed: {path_text}")
        ErofsImage.load(path).validate_superblock()
    for expected in manifest["source_inventory"]:
        path = output_root / expected["path"]
        if path.stat().st_size != expected["size"] or sha256(path) != expected["sha256"]:
            raise FixtureError(f"source changed: {expected['path']}")
    for key, expected in manifest["targets"].items():
        image_name, path = key.split(":", 1)
        actual = inspect_path(
            output_root,
            output_root / "images" / image_name,
            path,
            write_log=False,
        )
        actual.pop("dump_log", None)
        comparable = dict(expected)
        comparable.pop("dump_log", None)
        if actual != comparable:
            raise FixtureError(f"structured target fields changed: {key}")


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)
    create = subparsers.add_parser("create")
    create.add_argument("--output", type=Path, required=True)
    create.add_argument("--erofs-utils-source", type=Path)
    verify = subparsers.add_parser("verify")
    verify.add_argument("--output", type=Path, required=True)
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    if args.command == "create":
        build_fixtures(args)
    else:
        verify_output(args.output)


if __name__ == "__main__":
    main()
