// Cascade golden: resolve a HAND-CRAFTED seeded board to a stable state and
// snapshot the whole settle loop. The board carries a single horizontal match
// (BBB at row 3, cols 0-2) sitting on the run-free O/G checkerboard, painted so
// that clearing it is NOT the end: col 0 holds R . R R straddling that match's
// cell, so once the B is cleared and gravity packs the column, the three reds
// fall adjacent into a fresh vertical RRR — a SECOND match the first clear set
// up. So the loop runs at least two rounds before it stabilises. The exact
// sequence (per-round boards + final depth) is locked by the snapshot.
//
// Two things are asserted independently of the dump: the final board is stable
// (find_matches empty), and the public `resolve` reproduces the manual loop's
// depth, per-round cleared counts, and final board byte-for-byte. A control
// checkerboard with no initial match resolves at depth 0, untouched.
#import "modules/std.sx";
#import "board.sx";
t :: #import "test.sx";

SEED :: 7;

// Number of rounds the crafted cascade runs. Locked alongside the golden.
EXPECTED_DEPTH :: 2;

// Inverse of `gem_char`: map a board character back to its Gem so the starting
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
char_to_gem :: (c: u8) -> Gem {
    if c == EMPTY_CHAR { return .empty; }
    for 0..GEM_COUNT: (i) {
        if GEM_CHARS[i] == c { return cast(Gem) i; }
    }
    .red
}

// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars).
// The RNG is left unseeded — callers seed it before resolving.
load_board :: (rows: []string) -> Board {
    b : Board = ---;
    for 0..BOARD_ROWS: (row) {
        line := rows[row];
        for 0..BOARD_COLS: (col) {
            b.set(col, row, char_to_gem(line[col]));
        }
    }
    b
}

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

// The crafted cascade board: checkerboard everywhere except a horizontal BBB at
// row 3 (cols 0-2) and reds salted down col 0 (rows 2,4,5) around that match's
// cell. Clearing BBB punches the col-0 hole between the reds; gravity then packs
// R,R,R adjacent → a vertical match for round 2. Its RNG is seeded from SEED so
// the refill that follows each clear is reproducible.
cascade_board :: () -> Board {
    b := load_board(.[
        "OGOGOGOG",
        "GOGOGOGO",
        "RGOGOGOG",
        "BBBOGOGO",
        "RGOGOGOG",
        "ROGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ]);
    b.rng = rng_seeded(SEED);
    b
}

// A run-free checkerboard with no initial match — the depth-0 control.
checker_board :: () -> Board {
    b := load_board(.[
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ]);
    b.rng = rng_seeded(SEED);
    b
}

main :: () -> s32 {
    print("== cascade (resolution loop) ==\n");

    // Drive the loop one round at a time so each post-round board is visible in
    // the snapshot, recording the per-round cleared counts and the depth.
    b := cascade_board();
    out("start:\n");
    out(board_dump(@b));

    depth := 0;
    counts := List(s64).{};
    while true {
        n := resolve_step(@b);
        if n == 0 { break; }
        depth += 1;
        counts.append(n);
        print("round {}: cleared {} cells\n", depth, n);
        out(board_dump(@b));
    }
    print("cascade depth {}\n", depth);

    // The loop reached a stable board.
    fm := find_matches(@b);
    t.expect(fm.count() == 0, "cascade: final board has no matches");

    // A genuine multi-round chain at the expected depth.
    t.expect(depth == EXPECTED_DEPTH, "cascade: depth equals expected");
    t.expect(depth >= 2, "cascade: chained at least two rounds");

    // The public `resolve` on a fresh identical board reproduces the manual
    // loop exactly: same depth, same per-round cleared counts, same final board.
    b2 := cascade_board();
    c := resolve(@b2);
    t.expect(c.depth == depth, "cascade: resolve depth matches manual loop");
    same_counts := c.cleared.len == counts.len;
    if same_counts {
        for 0..counts.len: (i) {
            if c.cleared.items[i] != counts.items[i] { same_counts = false; }
        }
    }
    t.expect(same_counts, "cascade: resolve per-round counts match manual loop");
    t.expect(boards_equal(@b, @b2), "cascade: resolve final board matches manual loop");

    // Control: a checkerboard with no initial match resolves at depth 0 and is
    // left untouched (no clear, no collapse, no refill draw).
    ctrl := checker_board();
    before := ctrl;
    cc := resolve(@ctrl);
    t.expect(cc.depth == 0, "control: stable board resolves at depth 0");
    t.expect(cc.cleared.len == 0, "control: depth 0 yields an empty per-round list");
    t.expect(boards_equal(@before, @ctrl), "control: stable board left unchanged");

    print("ok: cascade resolves to a stable board\n");
    return 0;
}
