// Swap-legality golden: exercise swap / adjacency / swap_legal and the
// legal_swaps enumeration. The predicate cases run over HAND-CRAFTED boards and
// are asserted directly; the move enumeration runs over the seeded board and is
// dumped deterministically and locked as a snapshot.
//
// Every hand-crafted board sits on the run-free O/G checkerboard from
// match_detect (adjacent cells always differ, so it has zero pre-existing
// matches) with only the cells under test overridden — so any match observed is
// purely the trial swap's doing, never a pre-existing run.
#import "modules/std.sx";
#import "board.sx";
t :: #import "test.sx";

SEED :: 1337;

// Inverse of `gem_char`: map a gem character back to its Gem so each board can
// be written as a human-readable grid of GEM_CHARS.
char_to_gem :: (c: u8) -> Gem {
    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 gem
// characters).
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
}

// Whole-board equality, cell by cell — used to prove a trial swap leaves the
// board untouched.
boards_equal :: (x: *Board, y: *Board) -> bool {
    for 0..BOARD_CELLS: (i) {
        if !(x.cells[i] == y.cells[i]) { return false; }
    }
    true
}

cell :: (col: s64, row: s64) -> Cell {
    Cell.{ col = col, row = row }
}

main :: () -> s32 {
    print("== swap & legality ==\n");

    // Board whose ONLY swap-formable match is the adjacent (2,3)<->(3,3)
    // exchange: it slides an R into row 3 to complete R,R,R across cols 0-2. The
    // B at (2,3) keeps the pre-swap row run-free. Reused by the revert check.
    scene_3run : []string = .[
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "RRBRGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ];

    // Legal: an adjacent swap that forms a 3-run. Swapping (3,3)<->(2,3) brings
    // R to (2,3), completing R,R,R on row 3 — the match lands on the SECOND
    // swapped cell, so a one-sided check would miss it.
    legal_3run := load_board(scene_3run);
    t.expect(swap_legal(@legal_3run, cell(3, 3), cell(2, 3)) == true,
             "legal: adjacent swap forms a 3-run");

    // Illegal: an adjacent swap that forms NO match. The R guards at (3,2) and
    // (4,4) break the runs the displaced checkerboard gems would otherwise make.
    no_match := load_board(.[
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOROGOG",
        "GOGOGOGO",
        "OGOGRGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ]);
    t.expect(swap_legal(@no_match, cell(3, 3), cell(4, 3)) == false,
             "illegal: adjacent swap forms no match");

    // Illegal: a NON-adjacent pair, rejected before any match check. The board
    // is rigged so that the (0,3)<->(2,3) exchange WOULD complete a B column run
    // if it were allowed — proving the rejection is by adjacency, not by an
    // absent match.
    non_adjacent := load_board(.[
        "OGOGOGOG",
        "GOGOGOGO",
        "BGOGOGOG",
        "YOBOGOGO",
        "BGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ]);
    t.expect(adjacent(cell(0, 3), cell(2, 3)) == false,
             "non-adjacent pair is not adjacent");
    t.expect(swap_legal(@non_adjacent, cell(0, 3), cell(2, 3)) == false,
             "illegal: non-adjacent pair rejected without a match check");

    // Illegal: a DIAGONAL pair, likewise rejected before any match check. The
    // (3,3)<->(4,4) exchange WOULD complete a P column run if it were allowed.
    diagonal := load_board(.[
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOPOGOG",
        "GOGYGOGO",
        "OGOPPGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ]);
    t.expect(adjacent(cell(3, 3), cell(4, 4)) == false,
             "diagonal pair is not adjacent");
    t.expect(swap_legal(@diagonal, cell(3, 3), cell(4, 4)) == false,
             "illegal: diagonal pair rejected without a match check");

    // Legal because only the OTHER swapped gem matches. Player moves the gem at
    // (4,3) to (5,3); the gem that comes back from (5,3) lands at (4,3) and
    // completes a B column run there, while the moved gem at (5,3) matches
    // nothing. Either swapped cell participating is enough.
    other_gem := load_board(.[
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGBGOG",
        "GOGOYBGO",
        "OGOGBGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ]);
    t.expect(swap_legal(@other_gem, cell(4, 3), cell(5, 3)) == true,
             "legal: only the other swapped gem matches");

    // Non-mutating: a legality probe and a manual test-then-revert both leave
    // the board byte-for-byte identical. `swap` is its own inverse.
    print("== swap revert (non-mutating) ==\n");
    revert := load_board(scene_3run);
    fresh := load_board(scene_3run);
    out("before:\n");
    out(board_dump(@revert));

    probe := swap_legal(@revert, cell(3, 3), cell(2, 3));  // trials + reverts
    swap(@revert, cell(3, 3), cell(2, 3));                 // mutate
    swap(@revert, cell(3, 3), cell(2, 3));                 // revert (self-inverse)

    out("after:\n");
    out(board_dump(@revert));
    t.expect(probe == true, "probe swap was indeed legal");
    t.expect(boards_equal(@revert, @fresh),
             "board unchanged after probe + test-then-revert");

    // Enumeration over the seeded board: a fixed, deterministic list locked as a
    // snapshot. Order is row-major over the top-left cell, right neighbour
    // before down neighbour.
    print("== legal_swaps: seeded {} ==\n", SEED);
    seeded : Board = ---;
    seeded.init(SEED);
    out(board_dump(@seeded));
    out("--\n");
    moves := legal_swaps(@seeded);
    out(dump_swaps(@moves));

    print("ok: swap legality over hand-crafted boards\n");
    return 0;
}
