//! CD.2.6 byte-fixture validation against voxlap C.
//!
//! Validates that roxlap's `ScumCtx + delslab + compilerle + voxalloc`
//! pipeline produces byte-identical column slab data to voxlap C
//! after a known `setspans` carve.
//!
//! Fixtures generated by `voxlaptest/tests/edit_fixture/edit_fixture.c`:
//! a 5×5 column patch at (1024, 1024) carved at z=200..230 on top of
//! the default `loadnul` world. Pre and post .vxl files capture the
//! whole 2048×2048 world; we compare only the touched columns.
//!
//! Scope (this is "A1" of CD.2.6 — single-fixture, single-edit). Full
//! suite (multi-edit, setcube/setsphere/setrect/edge cases) deferred
//! per memory note.

#![allow(
    clippy::cast_sign_loss,
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::items_after_statements
)]

use std::io::Read;

use flate2::read::GzDecoder;

use roxlap_formats::edit::{delslab, set_spans_with_colfunc, ScumCtx, SpanOp, Vspan};
use roxlap_formats::vxl;

const SETSPANS_PRE: &[u8] = include_bytes!("fixtures/edit/setspans_pre.vxl.gz");
const SETSPANS_POST: &[u8] = include_bytes!("fixtures/edit/setspans_post.vxl.gz");

/// Mirrors the C-side fixture parameters in
/// `tests/edit_fixture/edit_fixture.c`.
const BASE_X: i32 = 1024;
const BASE_Y: i32 = 1024;
const PATCH_W: i32 = 5;
const PATCH_H: i32 = 5;
const CARVE_Z0: i32 = 200;
/// Voxlap's setspans does `z1 = lst[i].z1 + 1`. The fixture stores
/// `lst.z1 = 229`, yielding the half-open carve `[200, 230)`.
const CARVE_Z1_EXCLUSIVE: i32 = 230;

fn parse_vxl(gz_bytes: &[u8]) -> vxl::Vxl {
    let mut decoder = GzDecoder::new(gz_bytes);
    let mut bytes = Vec::with_capacity(40 * 1024 * 1024);
    decoder.read_to_end(&mut bytes).expect("ungzip fixture");
    vxl::parse(&bytes).expect("parse fixture .vxl")
}

#[test]
fn setspans_5x5_patch_byte_equal_to_voxlap_c() {
    let mut pre = parse_vxl(SETSPANS_PRE);
    let post = parse_vxl(SETSPANS_POST);
    assert_eq!(pre.vsid, 2048, "fixture VSID");
    assert_eq!(post.vsid, pre.vsid);

    // Reserve enough headroom for ~25 column re-encodes. Each touched
    // column grows to ~3 slabs of ~2-3 voxels each (12-24 bytes) +
    // headers. 256 KB is comfortably above any plausible upper bound.
    pre.reserve_edit_capacity(256 * 1024);

    {
        let mut ctx = ScumCtx::new(&mut pre);
        // The C harness sets vx5.colfunc = curcolfunc + curcol =
        // FIXTURE_COL so newly-exposed voxels (above/below the
        // carved span, which weren't in the original column's
        // floor list) get a deterministic colour. Mirror it here.
        const FIXTURE_COL: roxlap_formats::VoxColor = roxlap_formats::VoxColor(0x80aa_bbcc);
        ctx.set_colfunc(|_x, _y, _z| FIXTURE_COL);
        // Voxlap iterates spans in (y, x) ascending order.
        for dy in 0..PATCH_H {
            for dx in 0..PATCH_W {
                let x = BASE_X + dx;
                let y = BASE_Y + dy;
                let b2 = ctx.scum2(x, y).expect("column in bounds");
                delslab(b2, CARVE_Z0, CARVE_Z1_EXCLUSIVE);
            }
        }
        ctx.finish();
    }

    // Compare the touched columns' bytes against voxlap C's post-edit
    // bytes.
    let vsid = pre.vsid as i32;
    let mut mismatches = Vec::new();
    for dy in 0..PATCH_H {
        for dx in 0..PATCH_W {
            let x = BASE_X + dx;
            let y = BASE_Y + dy;
            let idx = (y as usize) * (vsid as usize) + (x as usize);
            let our = pre.column_data(idx);
            let theirs = post.column_data(idx);
            if our != theirs {
                mismatches.push((x, y, our.to_vec(), theirs.to_vec()));
            }
        }
    }

    if !mismatches.is_empty() {
        let summary: Vec<String> = mismatches
            .iter()
            .take(3)
            .map(|(x, y, our, theirs)| {
                format!(
                    "  ({x:4}, {y:4}): roxlap={} bytes, voxlap-C={} bytes",
                    our.len(),
                    theirs.len()
                )
            })
            .collect();
        panic!(
            "{} of {} touched columns mismatch voxlap C:\n{}",
            mismatches.len(),
            (PATCH_W * PATCH_H),
            summary.join("\n")
        );
    }
}

#[test]
fn set_spans_with_colfunc_5x5_patch_byte_equal_to_voxlap_c() {
    // Same fixture as the lower-level test above, but driven through
    // the CD.3 set_spans_with_colfunc wrapper. Asserts true byte-
    // equality (not just b2 shape) by passing the same FIXTURE_COL
    // the voxlap-C harness uses. Validates the wrapper composes the
    // full CD.2 stack (ScumCtx + with_column dedup + delslab +
    // colfunc plumb-through) correctly.
    const FIXTURE_COL: roxlap_formats::VoxColor = roxlap_formats::VoxColor(0x80aa_bbcc);

    let mut pre = parse_vxl(SETSPANS_PRE);
    let post = parse_vxl(SETSPANS_POST);
    pre.reserve_edit_capacity(256 * 1024);

    let mut spans = Vec::with_capacity((PATCH_W * PATCH_H) as usize);
    for dy in 0..PATCH_H {
        for dx in 0..PATCH_W {
            spans.push(Vspan {
                x: (BASE_X + dx) as u32,
                y: (BASE_Y + dy) as u32,
                z0: CARVE_Z0 as u8,
                z1: (CARVE_Z1_EXCLUSIVE - 1) as u8,
            });
        }
    }

    set_spans_with_colfunc(&mut pre, &spans, SpanOp::Carve, |_x, _y, _z| FIXTURE_COL);

    let vsid = pre.vsid as i32;
    let mut mismatches = 0;
    for dy in 0..PATCH_H {
        for dx in 0..PATCH_W {
            let x = BASE_X + dx;
            let y = BASE_Y + dy;
            let idx = (y as usize) * (vsid as usize) + (x as usize);
            if pre.column_data(idx) != post.column_data(idx) {
                mismatches += 1;
            }
        }
    }
    assert_eq!(
        mismatches, 0,
        "set_spans_with_colfunc: {mismatches} of 25 columns mismatch voxlap C"
    );
}
