// Animation-layer determinism guard (P6.1): prove the swap/clear/fall animation
// timeline is PURELY VISUAL — it never changes the model's result. `plan_and_commit`
// commits the move on the real board (authoritative) AND records the visual
// timeline on a value-copy; this test asserts, on the SAME seed the app renders
// (SEED 1337):
//   - the board `plan_and_commit` leaves is byte-for-byte identical to an
//     independent `commit_swap` of the same move, with the same score + moves;
//   - the recorded timeline ENDS on that exact state: `move.final` equals the
//     model board, the rounds are contiguous (round 0 starts on the swapped board,
//     each later round starts on the prior round's settled board), and the last
//     round's `after` equals `final`;
//   - an illegal swap records no rounds and leaves the board untouched.
// It also guards the P6.1 input-lock fix: `accepts_input` rejects a gesture for
// the FULL in-flight animation window (mouse_down → settle), so a swipe begun
// while a move animates never latches a drag and never commits — even if it is
// released after the timeline ends.
// No rendering — it calls exactly what BoardView.handle_event calls. Links headless
// like tests/swipe_commit.sx; avoids tests/test.sx (its trace.sx pulls in a second
// `Frame` that collides with the UI one). Failure is a non-zero exit code.
#import "modules/std.sx";
#import "board.sx";
#import "board_anim.sx";

SEED :: 1337;

boards_equal :: (x: *Board, y: *Board) -> bool {
    for 0..BOARD_CELLS: (i) {
        if !(x.cells[i] == y.cells[i]) { return false; }
    }
    true
}

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

    // ── Legal swap: plan == model, timeline ends on the model ───────────────
    // (5,4)->(6,4): brings R into (5,4), completing R,R,R across cols 3-5 of row
    // 4 — the same legal swap tests/swipe_commit.sx commits.
    print("== legal swap: plan matches model ==\n");
    a := Cell.{ col = 5, row = 4 };
    b := Cell.{ col = 6, row = 4 };

    bm : Board = ---;
    bm.init(SEED);
    mvm := commit_swap(@bm, a, b);

    ba : Board = ---;
    ba.init(SEED);
    move := plan_and_commit(@ba, a, b);

    print("model: legal {} depth {} score {} moves {}\n",
          mvm.legal, mvm.cascade.depth, bm.score, bm.moves_made);
    print("plan:  legal {} rounds {} score {} moves {}\n",
          move.legal, move.rounds.len, ba.score, ba.moves_made);

    if !move.legal { fails += 1; }
    if !boards_equal(@ba, @bm) { fails += 1; }           // committed board == model
    if ba.score != bm.score { fails += 1; }
    if ba.moves_made != bm.moves_made { fails += 1; }
    if move.rounds.len != mvm.cascade.depth { fails += 1; }

    // move.final equals the model board.
    final_eq := true;
    for 0..BOARD_CELLS: (i) {
        if !(move.final[i] == bm.cells[i]) { final_eq = false; }
    }
    if !final_eq { fails += 1; }
    print("final==model {}\n", final_eq);

    // Timeline contiguity: round 0 starts on the swapped pre board; each later
    // round starts on the previous round's settled board; final == last after.
    contiguous := true;
    if move.rounds.len > 0 {
        ai := Board.idx(a.col, a.row);
        bi := Board.idx(b.col, b.row);
        r0 := @move.rounds.items[0];
        for 0..BOARD_CELLS: (i) {
            expect : Gem = move.pre[i];
            if i == ai { expect = move.pre[bi]; }
            else if i == bi { expect = move.pre[ai]; }
            if !(r0.before[i] == expect) { contiguous = false; }
        }
        for 1..move.rounds.len: (k) {
            prev := @move.rounds.items[k - 1];
            cur := @move.rounds.items[k];
            for 0..BOARD_CELLS: (i) {
                if !(cur.before[i] == prev.after[i]) { contiguous = false; }
            }
        }
        last := @move.rounds.items[move.rounds.len - 1];
        for 0..BOARD_CELLS: (i) {
            if !(last.after[i] == move.final[i]) { contiguous = false; }
        }
    }
    if !contiguous { fails += 1; }
    print("contiguous {}\n", contiguous);
    out("final board:\n");
    out(board_dump(@bm));

    // ── Illegal swap: no timeline, board untouched ──────────────────────────
    // (0,0)->(1,0): two reds → no match. plan_and_commit must leave the board
    // exactly as it was, spend no move, and record zero rounds.
    print("== illegal swap: untouched ==\n");
    bi2 : Board = ---;
    bi2.init(SEED);
    pre2 : Board = bi2;
    mi := plan_and_commit(@bi2, Cell.{ col = 0, row = 0 }, Cell.{ col = 1, row = 0 });
    print("legal {} rounds {} score {} moves {}\n", mi.legal, mi.rounds.len, bi2.score, bi2.moves_made);
    if mi.legal { fails += 1; }
    if mi.rounds.len != 0 { fails += 1; }
    if !boards_equal(@pre2, @bi2) { fails += 1; }
    if bi2.score != 0 { fails += 1; }
    if bi2.moves_made != 0 { fails += 1; }

    // ── Input gate: locked for the FULL in-flight animation ─────────────────
    // The view begins a drag on mouse_down only when `accepts_input` is true,
    // and commits on mouse_up only if a drag latched. So a gesture that BEGINS
    // while a move animates must NEVER commit — even if it is released after the
    // animation has fully settled. This guards the P6.1 input-lock fix.
    print("== input gate: locked while animating ==\n");
    gate : BoardAnim = ---;
    gate.init();
    idle_ok := accepts_input(@gate);     // no move in flight → accept
    gate.begin(move);                    // a legal move's timeline starts
    busy_ok := accepts_input(@gate);     // mouse_down DURING animation → reject
    while gate.active { gate.tick(0.05); }   // player holds; timeline plays out
    settled_ok := accepts_input(@gate);  // animation fully settled → accept
    print("accepts idle {} busy {} settled {}\n", idle_ok, busy_ok, settled_ok);
    if !idle_ok { fails += 1; }
    if busy_ok { fails += 1; }           // MUST be locked while animating
    if !settled_ok { fails += 1; }

    // The board MUST decide a gesture at PRESS, not at RELEASE. Over the exact
    // failure scenario — a gesture PRESSED while animating and RELEASED after the
    // timeline has settled — the two policies diverge:
    //   release-gate: commit unless animating AT RELEASE → COMMITS (the timeline
    //                 finished first), letting input slip through mid-transition.
    //   press-gate:   latch only if input accepted AT PRESS → DROPS, because
    //                 input was locked for the whole window the timeline ran.
    gate.init();
    gate.begin(move);
    accept_at_press := accepts_input(@gate);    // mouse_down while animating
    while gate.active { gate.tick(0.05); }
    accept_at_release := accepts_input(@gate);  // mouse_up after settle
    release_gate_commits := accept_at_release;
    press_gate_commits := accept_at_press;
    print("release_gate_commits {} press_gate_commits {}\n", release_gate_commits, press_gate_commits);
    if !release_gate_commits { fails += 1; }  // the scenario release-gating lets through
    if press_gate_commits { fails += 1; }     // the board press-gates: MUST NOT commit

    if fails == 0 {
        print("ok: animation layer leaves the model result unchanged\n");
        return 0;
    }
    print("FAIL: {} anim-determinism checks failed\n", fails);
    return 1;
}
