#!/bin/sh
# ez3d test suite: exact facet counts on synthetic images, manifold
# verification on everything, and CLI error handling.
#
# Facet-count expectations under the current mesher model: the default
# --edge of -INFINITY cuts the outer half-pixel ring, so an NxN image
# of solid pixels delivers an (N-1)x(N-1) footprint (e.g. white4 ->
# bbox x [0.5, 3.5]). Every cell fans into 8 triangles, top and bottom,
# plus walls wherever a solid cell borders a cut cell.
#
#   white4  (4x4, no alpha): 16 solid cells -> 192 facets
#   one     (1x1):           all corners cut by the -INF edge, so the
#                           default run rejects it; --edge=-0.001 gives
#                           the 16-facet spike tent
#   holed8  (8x8, 5x5 cell hole): 592
#   stairs10 (10x10 diagonal staircase): 608
set -e

cd "$(dirname "$0")/.."
EZ3D=./ez3d
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

python3 tests/gen_png.py "$TMP"

fails=0
check() { # name expected condition-description command...
    name=$1; expected=$2; desc=$3; shift 3
    if out=$("$@" 2>&1); then :; else
        echo "FAIL $name: command exited $?"; echo "$out"; fails=$((fails+1)); return
    fi
    got=$(printf '%s\n' "$out" | sed -n 's/.*wrote [^:]*: \([0-9]*\) facets.*/\1/p')
    if [ "$got" != "$expected" ]; then
        echo "FAIL $name: expected $expected facets, got '$got'"; echo "$out"
        fails=$((fails+1)); return
    fi
    if ! printf '%s\n' "$out" | grep -q 'verify: PASS'; then
        echo "FAIL $name: verify did not PASS"; echo "$out"; fails=$((fails+1)); return
    fi
    echo "ok   $name ($desc)"
}

# --- exact facet counts --------------------------------------------------
check white4    192 "4x4 uniform: 16 cells x 8 tris, top + bottom + walls" \
    $EZ3D --verify "$TMP/white4.png" "$TMP/white4.stl"

check one       16  "1x1 image with --edge=-0.001: 16-facet spike tent" \
    $EZ3D --edge=-0.001 --verify "$TMP/one.png" "$TMP/one.stl"

check holed8    592 "8x8 with alpha-cut 5x5 cell hole" \
    $EZ3D --verify "$TMP/holed8.png" "$TMP/holed8.stl"

check stairs10  608 "diagonal staircase stays closed" \
    $EZ3D --verify "$TMP/stairs10.png" "$TMP/stairs10.stl"

# --- decimation -----------------------------------------------------------
# flat 4x4 at the coarsest level must collapse towards a minimal box;
# rim protection keeps the border ring of vertices, hence 80 not 12
if out=$($EZ3D --decimate 5 --verify "$TMP/white4.png" "$TMP/w4d.stl" 2>&1) \
   && printf '%s\n' "$out" | grep -q 'verify: PASS'; then
    n=$(printf '%s\n' "$out" | sed -n 's/.*decimated: [0-9]* -> \([0-9]*\) triangles.*/\1/p')
    if [ "$n" -gt 100 ]; then
        echo "FAIL white4 decimate: expected <= 100 facets, got $n"; fails=$((fails+1))
    else
        echo "ok   white4 --decimate 5 (flat box collapses to $n facets)"
    fi
else
    echo "FAIL white4 decimate: verify did not PASS"; printf '%s\n' "$out"
    fails=$((fails+1))
fi

# every level keeps the mesh closed and keeps the footprint (bump cells
# [3..12]x[3..12] minus the half-pixel border -> bbox x [2.5, 13.5]);
# counts must not grow as the level gets coarser
prev=""
for L in 1 2 3 4 5; do
    out=$($EZ3D --decimate $L --verify "$TMP/bump16.png" "$TMP/bump_d$L.stl" 2>&1)
    n=$(printf '%s\n' "$out" | sed -n 's/.*decimated: [0-9]* -> \([0-9]*\) triangles.*/\1/p')
    case "$out" in
        *"x [2.5, 13.5] y [2.5, 13.5]"*) : ;;
        *) echo "FAIL bump16 decimate L$L: footprint changed"; printf '%s\n' "$out"
           fails=$((fails+1)); prev=""; continue ;;
    esac
    if ! printf '%s\n' "$out" | grep -q 'verify: PASS'; then
        echo "FAIL bump16 decimate L$L: verify"; printf '%s\n' "$out"
        fails=$((fails+1)); continue
    fi
    if [ -n "$prev" ] && [ "$n" -gt "$prev" ]; then
        echo "FAIL bump16 decimate: L$L ($n) coarser-rated but bigger than L$((L-1)) ($prev)"
        fails=$((fails+1))
    fi
    prev=$n
done
[ -n "$prev" ] && echo "ok   bump16 decimate levels 1..5 (L5 = $prev facets)"

# alpha-cut outline survives decimation closed
if out=$($EZ3D --decimate 3 --verify "$TMP/holed8.png" "$TMP/holed8d.stl" 2>&1) \
   && printf '%s\n' "$out" | grep -q 'verify: PASS'; then
    echo "ok   holed8 --decimate 3 (through-hole stays closed)"
else
    echo "FAIL holed8 decimate"; printf '%s\n' "$out"; fails=$((fails+1))
fi

# decimation must not bridge cut-out regions: two islands with a moat,
# and a 1px slot (the "gap between coils" shape), both at the coarsest
# level. Forbidden zones sit strictly inside the measured open spans
# (moat gap x in (5.5, 10.5); slot x in (7.5, 9.5), world y in (4.5, 12.5)
# with funnelled mouths at each end).
if out=$($EZ3D --decimate 5 --verify "$TMP/moat16.png" "$TMP/moat.stl" 2>&1) \
   && printf '%s\n' "$out" | grep -q 'verify: PASS' \
   && python3 tests/check_gap.py "$TMP/moat.stl" 6 10 1 7 >/dev/null; then
    echo "ok   moat16 --decimate 5 (islands stay separate)"
else
    echo "FAIL moat16: bridged or not closed"; printf '%s\n' "$out"
    python3 tests/check_gap.py "$TMP/moat.stl" 6 10 1 7 || :
    fails=$((fails+1))
fi
if out=$($EZ3D --decimate 5 --verify "$TMP/slot16.png" "$TMP/slot.stl" 2>&1) \
   && printf '%s\n' "$out" | grep -q 'verify: PASS' \
   && python3 tests/check_gap.py "$TMP/slot.stl" 7.75 9.25 5 11 >/dev/null; then
    echo "ok   slot16 --decimate 5 (1px slot stays open)"
else
    echo "FAIL slot16: bridged or not closed"; printf '%s\n' "$out"
    python3 tests/check_gap.py "$TMP/slot.stl" 7.75 9.25 5 11 || :
    fails=$((fails+1))
fi

if $EZ3D --decimate 6 "$TMP/one.png" "$TMP/x.stl" 2>/dev/null; then
    echo "FAIL decimate 6 accepted"; fails=$((fails+1))
else
    echo "ok   --decimate range enforced"
fi
if $EZ3D --simplify 6 "$TMP/one.png" "$TMP/x.stl" 2>/dev/null; then
    echo "FAIL simplify 6 accepted"; fails=$((fails+1))
else
    echo "ok   --simplify range enforced"
fi

# --- worst-case tiny outputs and fake alpha -----------------------------
# 1x1 with the default -INF edge: every corner is cut away, so the run
# must fail cleanly rather than emit garbage
if $EZ3D --verify tests/gimp/tiny_white_1x1.png "$TMP/tiny1.stl" 2>/dev/null; then
    echo "FAIL tiny1x1: default edge should cut everything away"; fails=$((fails+1))
else
    echo "ok   tiny1x1 (default -INF edge cuts all, rejected cleanly)"
fi

check tiny2x2    32  "2x2 image: 4 solid cells, closed tent" \
    $EZ3D --verify tests/gimp/tiny_white_2x2.png "$TMP/tiny2.stl"

if out=$($EZ3D --base=-3 --height=13 --verify tests/gimp/loop_test_noalpha.png \
              "$TMP/loop.stl" 2>&1) \
   && printf '%s\n' "$out" | grep -q 'verify: PASS'; then
    echo "ok   loop_test_noalpha with --base=-3 (fake alpha cut)"
else
    echo "FAIL loop_test_noalpha"; printf '%s\n' "$out"; fails=$((fails+1))
fi

# --- manifold + geometry on less trivial shapes ---------------------------
for t in grad8 bump16; do
    if out=$($EZ3D --simplify 1 --verify "$TMP/$t.png" "$TMP/$t.stl" 2>&1) \
       && printf '%s\n' "$out" | grep -q 'verify: PASS'; then
        echo "ok   $t (verify PASS with simplify)"
    else
        echo "FAIL $t"; printf '%s\n' "$out"; fails=$((fails+1))
    fi
done

# flat-region grouping on a real sample: groups must be discovered and
# the result must stay closed (this is the --simplify + --decimate path
# recommended for large images)
if out=$($EZ3D --simplify 1 --decimate 1 --verify tests/gimp/M4_hex_test.png \
              "$TMP/m4.stl" 2>&1) \
   && printf '%s\n' "$out" | grep -q 'verify: PASS' \
   && printf '%s\n' "$out" | grep -q 'discovered [1-9][0-9]* groups'; then
    echo "ok   M4_hex --simplify 1 --decimate 1 (groups + closed)"
else
    echo "FAIL M4_hex simplify+decimate"; printf '%s\n' "$out"; fails=$((fails+1))
fi

# black image with a base: flat 2mm slab (nothing above gray-0 height)
out=$($EZ3D --verify --base 2 "$TMP/black2.png" "$TMP/black.stl")
case "$out" in
    *"z [0, 2]"*) echo "ok   black + --base 2 (flat 0..2mm slab)";;
    *) echo "FAIL black slab: bbox not [0,2]"; printf '%s\n' "$out"
       fails=$((fails+1));;
esac

# --base adds thickness below gray-0 while the walls still reach z=0
out=$($EZ3D --verify --base 2 --height 4 "$TMP/white4.png" "$TMP/base.stl")
case "$out" in
    *"z [0, 6]"*) echo "ok   base (solid 0..2 under terrain 2..6)";;
    *) echo "FAIL base: bbox not [0,6]"; printf '%s\n' "$out"
       fails=$((fails+1));;
esac

# footprint matches pixel dimensions minus the half-pixel border cut
out=$($EZ3D --verify "$TMP/white4.png" "$TMP/fp.stl")
case "$out" in
    *"x [0.5, 3.5] y [0.5, 3.5]"*) echo "ok   footprint 3x3 units (4x4 px, -INF edge)";;
    *) echo "FAIL footprint"; printf '%s\n' "$out"; fails=$((fails+1));;
esac

# --- ASCII output ---------------------------------------------------------
$EZ3D --ascii --edge=-0.001 "$TMP/one.png" "$TMP/one_ascii.stl"
if head -1 "$TMP/one_ascii.stl" | grep -q '^solid' && \
   grep -q '^endsolid' "$TMP/one_ascii.stl"; then
    echo "ok   ascii output"
else
    echo "FAIL ascii output"; fails=$((fails+1))
fi

# --- regression on the real sample, when present ---------------------------
DIAMOND=tests/gimp/diamond_test.png
if [ -f "$DIAMOND" ]; then
    if out=$($EZ3D --base=2 --height=18 --decimate=3 --verify \
                  "$DIAMOND" "$TMP/diamond.stl" 2>&1) \
       && printf '%s\n' "$out" | grep -q 'verify: PASS'; then
        echo "ok   diamond --decimate 3"
    else
        echo "FAIL diamond decimate"; printf '%s\n' "$out"; fails=$((fails+1))
    fi
fi
SNAKE=/home/zaiuser/test_3d_snake.png
if [ -f "$SNAKE" ]; then
    out=$($EZ3D --decimate 3 --verify "$SNAKE" "$TMP/snake_d3.stl" 2>&1)
    n=$(printf '%s\n' "$out" | sed -n 's/.*decimated: [0-9]* -> \([0-9]*\) triangles.*/\1/p')
    if printf '%s\n' "$out" | grep -q 'verify: PASS' && [ "$n" -lt 150000 ]; then
        echo "ok   snake --decimate 3 ($n facets, closed)"
    else
        echo "FAIL snake decimate"; printf '%s\n' "$out"; fails=$((fails+1))
    fi
    # no folded facets: terrain must face up, cap must face down (folded
    # facets render dark in shader-based viewers like Orca)
    folds=$(python3 -c "
import struct, sys
data = open('$TMP/snake_d3.stl','rb').read()
n = struct.unpack_from('<I', data, 80)[0]
bad = 0
for i in range(n):
    v = struct.unpack_from('<9f', data, 84+50*i+12)
    zs = (v[2], v[5], v[8])
    nz = (v[3]-v[0])*(v[7]-v[1]) - (v[4]-v[1])*(v[6]-v[0])
    if (min(zs) > 0.01 and nz < 0) or (max(zs) < 0.01 and nz > 0):
        bad += 1
print(bad)")
    if [ "$folds" = "0" ]; then
        echo "ok   snake decimated output has no folded facets"
    else
        echo "FAIL snake folds: $folds facets face the wrong way"
        fails=$((fails+1))
    fi
fi

# --- error handling --------------------------------------------------------
if $EZ3D "$TMP/does-not-exist.png" "$TMP/x.stl" 2>/dev/null; then
    echo "FAIL missing input: exited 0"; fails=$((fails+1))
else
    echo "ok   missing input rejected"
fi
if $EZ3D --height abc "$TMP/one.png" "$TMP/x.stl" 2>/dev/null; then
    echo "FAIL bad number: exited 0"; fails=$((fails+1))
else
    echo "ok   bad numeric option rejected"
fi
# fully transparent image must be rejected
python3 - "$TMP/trivial.png" <<'EOF'
import sys; sys.path.insert(0, "tests"); from gen_png import write_png
write_png(sys.argv[1], 4, 4, [255]*16, [0]*16)
EOF
if $EZ3D "$TMP/trivial.png" "$TMP/x.stl" 2>/dev/null; then
    echo "FAIL all-transparent: exited 0"; fails=$((fails+1))
else
    echo "ok   fully transparent image rejected"
fi
# ...but --alpha-thresh 0 recovers it
if $EZ3D --alpha-thresh 0 --verify "$TMP/trivial.png" "$TMP/x.stl" \
    >/dev/null 2>&1; then
    echo "ok   --alpha-thresh 0 keeps everything"
else
    echo "FAIL alpha-thresh 0"; fails=$((fails+1))
fi

echo
if [ "$fails" -eq 0 ]; then
    echo "all tests passed"
else
    echo "$fails test(s) FAILED"
    exit 1
fi
