// Per-gem animation determinism guard (P6.3): prove the idle/select/land/clear
// poses are PURELY VISUAL pure functions of the animation clock, and — most
// importantly — that at clock t==0 EVERY gem's idle pose is EXACTLY the resting
// sprite. That t==0-rest invariant is what lets the pre-P6.3 goldens (p4_board,
// p4_hud, …) reproduce under the deterministic capture (M3TE_ANIM_TIME=0): if the
// idle ramp were dropped, gems would already be deformed at t=0 and every prior
// golden would churn. It also pins the reactions to rest at their window ends and
// bounds the idle amplitude so the polish stays tasteful.
// No rendering — pure math over gem_anim.sx. Failure is a non-zero exit code.
#import "modules/std.sx";
#import "modules/math";
#import "board.sx";
#import "gem_anim.sx";

// Local f32 abs with explicit 0.0 literals: the stdlib generic `abs` mis-types
// its untyped `0` literals under f32 and returns 0.0 for every f32 input. The
// shipped game never calls abs; only this test needs it, so it rolls its own.
fabs :: (x: f32) -> f32 { if x < 0.0 then 0.0 - x else x }
approx :: (a: f32, b: f32) -> bool { fabs(a - b) < 0.0001 }

main :: () -> s32 {
    fails : s64 = 0;

    // 1. t==0 idle pose is EXACTLY rest for every cell (the determinism invariant).
    print("== idle t=0 is rest for all cells ==\n");
    rest_ok := true;
    for 0..BOARD_ROWS: (row) {
        for 0..BOARD_COLS: (col) {
            p := idle_pose(0.0, col, row);
            if !(p.scale_x == 1.0 and p.scale_y == 1.0 and p.dx == 0.0 and p.dy == 0.0) {
                rest_ok = false;
            }
        }
    }
    print("idle_t0_rest {}\n", rest_ok);
    if !rest_ok { fails += 1; }

    // 2. Past the ramp the idle actually moves, but stays tasteful (bounded).
    print("== idle mid-phase deforms, bounded ==\n");
    moved := false;
    bounded := true;
    for 0..BOARD_ROWS: (row) {
        for 0..BOARD_COLS: (col) {
            p := idle_pose(0.6, col, row);
            if fabs(p.scale_x - 1.0) > 0.0005 { moved = true; }
            if fabs(p.scale_x - 1.0) > 0.05 { bounded = false; }
            if fabs(p.dy) > 0.05 { bounded = false; }
        }
    }
    print("idle_mid_moves {} idle_bounded {}\n", moved, bounded);
    if !moved { fails += 1; }
    if !bounded { fails += 1; }

    // 3. Select pop: rest at both window ends, a pop in the middle.
    print("== select pop envelope ==\n");
    s_start := approx(select_pop_scale(0.0), 1.0);
    s_end := approx(select_pop_scale(SELECT_DUR), 1.0);
    s_mid := select_pop_scale(SELECT_DUR * 0.5) > 1.05;
    print("select_start_rest {} select_end_rest {} select_mid_pops {}\n", s_start, s_end, s_mid);
    if !s_start { fails += 1; }
    if !s_end { fails += 1; }
    if !s_mid { fails += 1; }

    // 4. Land squash: rest at both window ends, a wobble just after impact.
    print("== land squash envelope ==\n");
    l_start := approx(land_squash(0.0), 0.0);
    l_end := approx(land_squash(LAND_DUR), 0.0);
    l_mid := fabs(land_squash(LAND_DUR * 0.12)) > 0.01;
    print("land_start_rest {} land_end_rest {} land_mid_wobbles {}\n", l_start, l_end, l_mid);
    if !l_start { fails += 1; }
    if !l_end { fails += 1; }
    if !l_mid { fails += 1; }

    // 5. Clear pop: full at t=0, gone at t=1, overshoots above 1 in between.
    print("== clear pop envelope ==\n");
    c_start := approx(clear_pop_scale(0.0), 1.0);
    c_end := approx(clear_pop_scale(1.0), 0.0);
    c_peak := clear_pop_scale(0.30) > 1.1;
    print("clear_start_full {} clear_end_gone {} clear_overshoots {}\n", c_start, c_end, c_peak);
    if !c_start { fails += 1; }
    if !c_end { fails += 1; }
    if !c_peak { fails += 1; }

    // 6. GemMotion land bookkeeping: fresh state is unpinned at t=0, a never-
    //    landed cell rests, and a freshly-stamped land reads age 0.
    print("== gem motion land bookkeeping ==\n");
    m : GemMotion = ---;
    m.init();
    init_ok := m.clock == 0.0 and !m.pinned;
    no_land := land_squash(m.land_local(0)) == 0.0;
    m.clock = 2.0;
    m.stamp_land(10);
    fresh_land := approx(m.land_local(10), 0.0);
    print("motion_init {} motion_no_land {} motion_fresh_land {}\n", init_ok, no_land, fresh_land);
    if !init_ok { fails += 1; }
    if !no_land { fails += 1; }
    if !fresh_land { fails += 1; }

    // 7. Restart resets the landing bounce. The restart button (BoardView.
    //    do_restart) reseeds the model AND must clear the per-gem landing state,
    //    or a restart fired right after a terminal cascade carries that move's
    //    squash onto the freshly seeded board. reset_landings is the factored
    //    reset do_restart calls: a cell stamped a moment ago is mid-squash, and
    //    after reset_landings every cell is back at rest — while the idle clock
    //    is deliberately left running (idle resumes from its normal phase).
    print("== gem motion restart resets landings ==\n");
    r : GemMotion = ---;
    r.init();
    r.clock = 5.0;
    r.stamp_land(3);
    r.stamp_land(42);
    r.clock = 5.08;   // 0.08s past impact: well inside LAND_DUR, so mid-squash.
    pre_squashing := fabs(land_squash(r.land_local(3))) > 0.01
                 and fabs(land_squash(r.land_local(42))) > 0.01;
    r.reset_landings();
    post_rest := land_squash(r.land_local(3)) == 0.0
             and land_squash(r.land_local(42)) == 0.0
             and land_squash(r.land_local(0)) == 0.0;
    clock_kept := r.clock == 5.08;
    print("restart_pre_squashing {} restart_post_rest {} restart_clock_kept {}\n",
          pre_squashing, post_rest, clock_kept);
    if !pre_squashing { fails += 1; }
    if !post_rest { fails += 1; }
    if !clock_kept { fails += 1; }

    if fails == 0 {
        print("ok: per-gem animation rests at t=0 and stays bounded\n");
        return 0;
    }
    print("FAIL: {} gem-anim checks failed\n", fails);
    return 1;
}
