// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Verify the `Mesh::welded_by_position` API against the calibration
//! report's claim that "after Trimesh(process=True) welds coincident
//! vertices, 53% (114/215 on duplex) become watertight."
//!
//! Three things this test confirms:
//!
//!   1. Welding a duplex element shrinks vertex count to (very close to)
//!      IfcOpenShell's pre-welded output, closing the 3× unwelded-soup
//!      gap the door/window calibration regression test documents.
//!   2. Welding makes the M_Fixed window mesh watertight (every edge is
//!      shared by exactly two triangles) — this is the property the
//!      report says 53% of duplex elements achieve.
//!   3. The default (unwelded) emission path still produces the
//!      triangle-soup output existing GPU consumers expect — welding is
//!      opt-in, never automatic.

use ifc_lite_core::{build_entity_index, EntityDecoder};
use ifc_lite_geometry::{GeometryRouter, Mesh};
use rustc_hash::FxHashMap;

const DUPLEX: &str = "../../tests/models/ara3d/duplex.ifc";

fn process(path: &str, element_id: u32) -> Option<Mesh> {
    if !std::path::Path::new(path).exists() {
        return None;
    }
    let content = std::fs::read_to_string(path).ok()?;
    let entity_index = build_entity_index(&content);
    let mut decoder = EntityDecoder::with_index(&content, entity_index);
    let router = GeometryRouter::with_units(&content, &mut decoder);
    let entity = decoder.decode_by_id(element_id).ok()?;
    router.process_element(&entity, &mut decoder).ok()
}

/// Count edges shared by exactly N triangles. A watertight mesh has every
/// edge shared by exactly 2 triangles (one on each side); any other count
/// means there's an open boundary, an interior face, or a non-manifold
/// junction.
fn edge_use_counts(mesh: &Mesh) -> FxHashMap<usize, usize> {
    let mut edges: FxHashMap<(u32, u32), usize> = FxHashMap::default();
    for chunk in mesh.indices.chunks_exact(3) {
        for (a, b) in [
            (chunk[0], chunk[1]),
            (chunk[1], chunk[2]),
            (chunk[2], chunk[0]),
        ] {
            let key = if a < b { (a, b) } else { (b, a) };
            *edges.entry(key).or_default() += 1;
        }
    }
    let mut counts: FxHashMap<usize, usize> = FxHashMap::default();
    for &use_count in edges.values() {
        *counts.entry(use_count).or_default() += 1;
    }
    counts
}

/// Welding the duplex M_Fixed window collapses the unwelded-triangle-soup
/// vertex count by ~3× — bringing it close to IfcOpenShell's emitted
/// vertex count for the same element (56 vs ifc-lite welded ~64).
#[test]
fn duplex_window_welds_to_ios_ballpark() {
    let Some(mesh) = process(DUPLEX, 6426) else {
        eprintln!("skipping: fixture missing");
        return;
    };
    let unwelded_verts = mesh.vertex_count();
    let welded = mesh.welded_by_position(1e-6);
    let welded_verts = welded.vertex_count();

    eprintln!("M_Fixed window #6426: unwelded={} welded={}", unwelded_verts, welded_verts);

    // The window's 4 IfcExtrudedAreaSolids each emit per-face-corner
    // vertices (no sharing). Welding should reduce vertex count by ~3×
    // (per Euler v ≈ t/2 for closed-mesh manifolds; pre-welding v ≈ 3t
    // for triangle soup). Ratio of 2x or better is the floor.
    assert!(
        unwelded_verts as f64 / welded_verts as f64 >= 2.0,
        "unwelded {} / welded {} ratio < 2.0 — welding didn't dedupe \
         enough (each extrusion's face corners should collapse)",
        unwelded_verts,
        welded_verts
    );
    // Triangle count must be preserved (welding never deletes or adds
    // triangles unless they degenerate, which a clean window mesh
    // shouldn't have).
    assert_eq!(mesh.triangle_count(), welded.triangle_count());
}

/// `Mesh::welded_by_position` dramatically improves edge-sharing — closing
/// the gap between unwelded triangle soup (0 manifold edges, every edge
/// unique to one triangle) and a topologically connected mesh (most edges
/// shared by exactly 2 triangles).
///
/// On a compound element like a window (4 IfcExtrudedAreaSolids sharing
/// the frame/sash/glass interfaces), some edges land in non-manifold
/// junctions where multiple solids meet — that's expected and matches
/// the calibration report's note that "~47% [of duplex elements] are
/// either legitimately open reps or have genuine holes." Pin the
/// improvement, not 100 % perfection.
#[test]
fn duplex_window_welding_dramatically_improves_edge_sharing() {
    let Some(mesh) = process(DUPLEX, 6426) else {
        return;
    };

    let unwelded_counts = edge_use_counts(&mesh);
    let unwelded_manifold = unwelded_counts.get(&2).copied().unwrap_or(0);
    let unwelded_total: usize = unwelded_counts.values().sum();
    let unwelded_fraction = unwelded_manifold as f32 / unwelded_total.max(1) as f32;

    let welded = mesh.welded_by_position(1e-6);
    let welded_counts = edge_use_counts(&welded);
    let welded_manifold = welded_counts.get(&2).copied().unwrap_or(0);
    let welded_total: usize = welded_counts.values().sum();

    eprintln!(
        "M_Fixed window edge sharing: unwelded manifold={}/{} ({:.0}%), \
         welded manifold={}/{} ({:.0}%)",
        unwelded_manifold,
        unwelded_total,
        100.0 * unwelded_manifold as f32 / unwelded_total.max(1) as f32,
        welded_manifold,
        welded_total,
        100.0 * welded_manifold as f32 / welded_total.max(1) as f32,
    );

    // Welding must dramatically improve the manifold fraction. The
    // existing extrusion code shares a few cap-vertex pairs within each
    // single extrusion (the two cap triangles share three corner
    // vertices), so the unwelded mesh already has ~30 % manifold edges.
    // The welding fix must push that to >90 % by deduping vertices
    // across the four extrusions' adjoining faces.
    let welded_fraction = welded_manifold as f32 / welded_total.max(1) as f32;
    assert!(
        welded_fraction > 0.9,
        "welded window should have >90% manifold edges; got {:.1}%",
        welded_fraction * 100.0
    );
    assert!(
        welded_fraction > unwelded_fraction + 0.4,
        "welding must improve manifold-edge fraction by at least 40 \
         percentage points; got unwelded={:.1}% welded={:.1}%",
        unwelded_fraction * 100.0,
        welded_fraction * 100.0
    );
}

/// Welding must NOT happen by default. GPU consumers expect the
/// unwelded-soup emission so per-face flat normals survive the upload.
/// Pin the property by checking that the unwelded mesh has ~3 vertices
/// per triangle (soup), while welding shrinks it dramatically.
#[test]
fn process_element_does_not_weld_by_default() {
    let Some(mesh) = process(DUPLEX, 6426) else {
        return;
    };
    let unwelded_v_per_t = mesh.vertex_count() as f32 / mesh.triangle_count().max(1) as f32;
    let welded = mesh.welded_by_position(1e-6);
    let welded_v_per_t = welded.vertex_count() as f32 / welded.triangle_count().max(1) as f32;

    eprintln!(
        "v/t ratio: unwelded={:.2} welded={:.2}",
        unwelded_v_per_t, welded_v_per_t
    );

    // Unwelded mesh: most vertices used by 1-2 triangles (per-face
    // corners shared between the two triangles forming a quad).
    // Welded mesh: each vertex used by ~6 triangles (4-6 typical for
    // closed-mesh manifolds), so v/t falls below 0.7.
    assert!(
        unwelded_v_per_t > 1.0,
        "default emission must be ≥1 vertex per triangle (soup); got {}",
        unwelded_v_per_t
    );
    assert!(
        welded_v_per_t < 0.7,
        "welded emission must drop v/t below 0.7 (manifold-ish); got {}",
        welded_v_per_t
    );
    // The shrink ratio is the actual property the calibration report
    // cared about — 3× soup → manifold.
    assert!(
        unwelded_v_per_t / welded_v_per_t > 2.0,
        "welding must shrink v/t ratio by 2× or more; got {} / {} = {:.2}",
        unwelded_v_per_t,
        welded_v_per_t,
        unwelded_v_per_t / welded_v_per_t
    );
}
