// Level / turn-goal state-machine golden (P7.1).
//
// Proves the pure level loop layered over the model in board.sx:
//
//   * `level_status` derives in_progress / won / lost from `Board.score`,
//     `Board.target_score` and the move budget — won the moment the goal is met,
//     lost when the budget is spent short of it.
//   * the win and loss TRANSITIONS across a committed move (`play_turn`).
//   * the deadlock RESHUFFLE rule: a provably stuck board (no legal swap, no
//     immediate match) reshuffles — via the seeded RNG — into a board with >=1
//     legal move and still no immediate match, consuming no move.
//   * `restart` resets score/moves/status and, for a fixed seed, reproduces the
//     exact same starting board.
//
// Deterministic: fixed seeds and crafted boards, every transition asserted AND
// printed so the golden is self-explanatory.
#import "modules/std.sx";
#import "board.sx";
t :: #import "test.sx";

START_SEED     :: 1337;
RESHUFFLE_SEED :: 1337;

// Inverse of `gem_char`: map a board character back to its Gem so each 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),
// seeded RNG, running score zeroed, the turn counters reset to a fresh game, and
// the per-level goal set.
load_board :: (rows: []string, seed: s64, move_limit: s64, target_score: s64) -> 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.rng = rng_seeded(seed);
    b.score = 0;
    b.moves_made = 0;
    b.move_limit = move_limit;
    b.target_score = target_score;
    b
}

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

main :: () -> s32 {
    print("== level (turn / goal state machine) ==\n");

    // ── Start: a fresh seeded board reads in_progress with the default goal ──
    print("== start-in-progress ==\n");
    b0 : Board = ---;
    b0.init(START_SEED);
    out(board_dump(@b0));
    print("score {} target {} moves_remaining {} status {}\n",
          b0.score, b0.target_score, b0.moves_remaining(), status_name(level_status(@b0)));
    t.expect(level_status(@b0) == .in_progress, "start: status in_progress");
    t.expect(b0.target_score == DEFAULT_TARGET_SCORE, "start: default goal");

    // ── WIN transition: one legal swap crosses a low goal ───────────────────
    // The commit-legal-len3 board: swapping (2,0)<->(2,1) completes RRR for an
    // award of 30. With the goal set to 30, that single move flips in_progress
    // -> won, and wins with moves still in the budget (win triggers on the goal,
    // not on the budget running out).
    print("== win-transition ==\n");
    bw := load_board(.[
        "RROGOGOG",
        "GGROGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ], 7, 5, 30);
    out(board_dump(@bw));
    print("before: score {} target {} moves_remaining {} status {}\n",
          bw.score, bw.target_score, bw.moves_remaining(), status_name(level_status(@bw)));
    t.expect(level_status(@bw) == .in_progress, "win: in_progress before move");
    tw := play_turn(@bw, Cell.{ col = 2, row = 0 }, Cell.{ col = 2, row = 1 });
    print("after: legal {} awarded {} reshuffled {}\n", tw.move.legal, tw.move.cascade.awarded, tw.reshuffled);
    print("after: score {} moves_remaining {} status {}\n",
          bw.score, bw.moves_remaining(), status_name(tw.status));
    out(board_dump(@bw));
    t.expect(tw.move.legal, "win: swap committed");
    t.expect(bw.score >= bw.target_score, "win: goal reached");
    t.expect(tw.status == .won, "win: turn reports won");
    t.expect(level_status(@bw) == .won, "win: board reads won");
    t.expect(bw.moves_remaining() > 0, "win: won with moves to spare");
    t.expect(!tw.reshuffled, "win: no reshuffle once won");

    // ── Frozen when WON: a finished level rejects further moves ──────────────
    // The level is over the instant it is won. Until `restart`, every `play_turn`
    // must be REJECTED (accepted=false): no swap applied, no move spent, no score
    // change, status stays won — even for an otherwise-legal swap. (4,0)<->(4,1)
    // is a legal swap on the won board, so WITHOUT the freeze it would commit and
    // move score/budget; the freeze is what keeps them put.
    print("== frozen-when-won ==\n");
    won_score  := bw.score;
    won_made   := bw.moves_made;
    won_remain := bw.moves_remaining();
    twf := play_turn(@bw, Cell.{ col = 4, row = 0 }, Cell.{ col = 4, row = 1 });
    print("post-win play_turn: accepted {} legal {} status {}\n",
          twf.accepted, twf.move.legal, status_name(twf.status));
    print("unchanged: score {} moves_made {} moves_remaining {}\n",
          bw.score, bw.moves_made, bw.moves_remaining());
    t.expect(!twf.accepted, "frozen-won: move rejected");
    t.expect(!twf.move.legal, "frozen-won: no swap committed");
    t.expect(twf.status == .won, "frozen-won: turn still reports won");
    t.expect(bw.score == won_score, "frozen-won: score unchanged");
    t.expect(bw.moves_made == won_made, "frozen-won: no move spent");
    t.expect(bw.moves_remaining() == won_remain, "frozen-won: budget unchanged");
    t.expect(level_status(@bw) == .won, "frozen-won: board still won");

    // ── LOSS transition: the move budget runs out short of the goal ──────────
    // Same legal swap (award 30) but a 1-move budget and an unreachable goal:
    // after the move moves_remaining hits 0 with score < goal, flipping
    // in_progress -> lost.
    print("== lose-transition ==\n");
    bl := load_board(.[
        "RROGOGOG",
        "GGROGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
        "OGOGOGOG",
        "GOGOGOGO",
    ], 7, 1, 1000000);
    out(board_dump(@bl));
    print("before: score {} target {} moves_remaining {} status {}\n",
          bl.score, bl.target_score, bl.moves_remaining(), status_name(level_status(@bl)));
    t.expect(level_status(@bl) == .in_progress, "lose: in_progress before move");
    tl := play_turn(@bl, Cell.{ col = 2, row = 0 }, Cell.{ col = 2, row = 1 });
    print("after: legal {} score {} moves_remaining {} status {}\n",
          tl.move.legal, bl.score, bl.moves_remaining(), status_name(tl.status));
    t.expect(tl.move.legal, "lose: swap committed");
    t.expect(bl.moves_remaining() == 0, "lose: budget spent");
    t.expect(bl.score < bl.target_score, "lose: short of goal");
    t.expect(tl.status == .lost, "lose: turn reports lost");
    t.expect(level_status(@bl) == .lost, "lose: board reads lost");
    t.expect(!tl.reshuffled, "lose: no reshuffle once lost");

    // ── Frozen when LOST: a finished level rejects further moves ─────────────
    // Symmetric to the won case. (4,0)<->(4,1) is a legal swap on the lost board,
    // so the REJECTION — not the spent budget — is what holds score and moves
    // put: `commit_swap` itself does not refuse a swap once the budget is gone.
    print("== frozen-when-lost ==\n");
    lost_score  := bl.score;
    lost_made   := bl.moves_made;
    lost_remain := bl.moves_remaining();
    tlf := play_turn(@bl, Cell.{ col = 4, row = 0 }, Cell.{ col = 4, row = 1 });
    print("post-lose play_turn: accepted {} legal {} status {}\n",
          tlf.accepted, tlf.move.legal, status_name(tlf.status));
    print("unchanged: score {} moves_made {} moves_remaining {}\n",
          bl.score, bl.moves_made, bl.moves_remaining());
    t.expect(!tlf.accepted, "frozen-lost: move rejected");
    t.expect(!tlf.move.legal, "frozen-lost: no swap committed");
    t.expect(tlf.status == .lost, "frozen-lost: turn still reports lost");
    t.expect(bl.score == lost_score, "frozen-lost: score unchanged");
    t.expect(bl.moves_made == lost_made, "frozen-lost: no move spent");
    t.expect(bl.moves_remaining() == lost_remain, "frozen-lost: budget unchanged");
    t.expect(level_status(@bl) == .lost, "frozen-lost: board still lost");

    // ── No-legal-moves rule: RESHUFFLE ──────────────────────────────────────
    // A provably deadlocked board: gem(col,row) = (col-row) mod 6, a diagonal
    // Latin square. Equal gems lie only on slope-1 diagonals, so no row/column
    // holds two of a kind within reach — there is no immediate match, and no
    // orthogonal swap can line up three. `reshuffle` re-arranges the SAME gems
    // (seeded RNG) into a board with a legal move and no immediate match, and
    // spends no move.
    print("== deadlock-reshuffle ==\n");
    bd := load_board(.[
        "ROYGBPRO",
        "PROYGBPR",
        "BPROYGBP",
        "GBPROYGB",
        "YGBPROYG",
        "OYGBPROY",
        "ROYGBPRO",
        "PROYGBPR",
    ], RESHUFFLE_SEED, 10, 1500);
    out(board_dump(@bd));
    pre_m  := find_matches(@bd);
    pre_sw := legal_swaps(@bd);
    print("before: matches {} legal_swaps {} has_legal_swap {}\n",
          pre_m.count(), pre_sw.len, has_legal_swap(@bd));
    t.expect(pre_m.count() == 0, "deadlock: no immediate match before");
    t.expect(pre_sw.len == 0, "deadlock: no legal swap before");
    t.expect(!has_legal_swap(@bd), "deadlock: has_legal_swap false before");
    score_before := bd.score;
    made_before  := bd.moves_made;
    ok := reshuffle(@bd);
    post_m  := find_matches(@bd);
    post_sw := legal_swaps(@bd);
    print("reshuffled {} matches {} legal_swaps {}\n", ok, post_m.count(), post_sw.len);
    print("after: score {} moves_made {}\n", bd.score, bd.moves_made);
    out(board_dump(@bd));
    t.expect(ok, "deadlock: reshuffle succeeded");
    t.expect(post_m.count() == 0, "deadlock: no immediate match after reshuffle");
    t.expect(post_sw.len > 0, "deadlock: >=1 legal move after reshuffle");
    t.expect(bd.score == score_before, "deadlock: reshuffle spent no score");
    t.expect(bd.moves_made == made_before, "deadlock: reshuffle spent no move");

    // ── Restart: reset progress, reproduce the seeded starting board ─────────
    print("== restart ==\n");
    br : Board = ---;
    br.init(START_SEED);
    // Dirty the state as a partial game would: spend moves, accrue score, and
    // mutate a cell so the layout differs from a fresh board.
    br.score = 500;
    br.moves_made = 7;
    br.set(0, 0, .empty);
    print("dirty: score {} moves_made {} status {}\n",
          br.score, br.moves_made, status_name(level_status(@br)));
    restart(@br, START_SEED);
    ref : Board = ---;
    ref.init(START_SEED);
    print("after restart: score {} moves_made {} moves_remaining {} status {}\n",
          br.score, br.moves_made, br.moves_remaining(), status_name(level_status(@br)));
    out(board_dump(@br));
    t.expect(br.score == 0, "restart: score reset");
    t.expect(br.moves_made == 0, "restart: moves reset");
    t.expect(level_status(@br) == .in_progress, "restart: status in_progress");
    t.expect(boards_equal(@br, @ref), "restart: same seed reproduces starting board");

    // restart RE-ENABLES play: the fresh in-progress level accepts moves again
    // (it was frozen only while terminal). A legal swap on the restored board now
    // commits — accepted, one move spent — proving the entry point is live again.
    rsw := legal_swaps(@br);
    t.expect(rsw.len > 0, "restart: fresh board has a legal move");
    rmove := rsw.items[0];
    tr := play_turn(@br, rmove.a, rmove.b);
    print("re-enabled: ({},{})-({},{}) accepted {} legal {} moves_made {}\n",
          rmove.a.col, rmove.a.row, rmove.b.col, rmove.b.row,
          tr.accepted, tr.move.legal, br.moves_made);
    t.expect(tr.accepted, "restart: play_turn accepted after restart");
    t.expect(tr.move.legal, "restart: legal swap committed after restart");
    t.expect(br.moves_made == 1, "restart: a move spent after restart");

    print("ok: level / turn-goal state machine\n");
    return 0;
}
