`timescale 1ns / 1ps

module tb_top_of_book_builder;
    reg clk;
    reg rst;
    reg upd_valid;
    reg upd_side;
    reg [3:0] price_idx;
    reg [15:0] upd_qty;

    wire book_valid;
    wire [3:0] best_bid_idx;
    wire [15:0] best_bid_qty;
    wire [3:0] best_ask_idx;
    wire [15:0] best_ask_qty;
    wire [3:0] spread_ticks;

    integer errors = 0;
    integer tests_run = 0;
    integer cycle_count = 0;
    integer i;
    integer rng_seed;

    reg rand_valid;
    reg rand_side;
    reg [3:0] rand_idx;
    reg [15:0] rand_qty;

    reg [15:0] model_bid [0:15];
    reg [15:0] model_ask [0:15];

    reg expected_book_valid;
    reg [3:0] expected_best_bid_idx;
    reg [15:0] expected_best_bid_qty;
    reg [3:0] expected_best_ask_idx;
    reg [15:0] expected_best_ask_qty;
    reg [3:0] expected_spread_ticks;

    top_of_book_builder uut (
        .clk(clk),
        .rst(rst),
        .upd_valid(upd_valid),
        .upd_side(upd_side),
        .price_idx(price_idx),
        .upd_qty(upd_qty),
        .book_valid(book_valid),
        .best_bid_idx(best_bid_idx),
        .best_bid_qty(best_bid_qty),
        .best_ask_idx(best_ask_idx),
        .best_ask_qty(best_ask_qty),
        .spread_ticks(spread_ticks)
    );

    initial begin
        clk = 1'b0;
        forever #5 clk = ~clk;
    end

    task clear_model;
        integer level;
        begin
            for (level = 0; level < 16; level = level + 1) begin
                model_bid[level] = 16'd0;
                model_ask[level] = 16'd0;
            end

            expected_book_valid = 1'b0;
            expected_best_bid_idx = 4'd0;
            expected_best_bid_qty = 16'd0;
            expected_best_ask_idx = 4'd0;
            expected_best_ask_qty = 16'd0;
            expected_spread_ticks = 4'd0;
        end
    endtask

    task recompute_expected_from_model;
        integer level;
        reg bid_found;
        reg ask_found;
        begin
            expected_book_valid = 1'b0;
            expected_best_bid_idx = 4'd0;
            expected_best_bid_qty = 16'd0;
            expected_best_ask_idx = 4'd0;
            expected_best_ask_qty = 16'd0;
            expected_spread_ticks = 4'd0;

            bid_found = 1'b0;
            for (level = 15; level >= 0; level = level - 1) begin
                if (!bid_found && (model_bid[level] != 16'd0)) begin
                    expected_best_bid_idx = level[3:0];
                    expected_best_bid_qty = model_bid[level];
                    bid_found = 1'b1;
                end
            end

            ask_found = 1'b0;
            for (level = 0; level < 16; level = level + 1) begin
                if (!ask_found && (model_ask[level] != 16'd0)) begin
                    expected_best_ask_idx = level[3:0];
                    expected_best_ask_qty = model_ask[level];
                    ask_found = 1'b1;
                end
            end

            if (bid_found && ask_found) begin
                expected_book_valid = 1'b1;
                expected_spread_ticks = expected_best_ask_idx - expected_best_bid_idx;
            end else begin
                expected_best_bid_idx = 4'd0;
                expected_best_bid_qty = 16'd0;
                expected_best_ask_idx = 4'd0;
                expected_best_ask_qty = 16'd0;
                expected_spread_ticks = 4'd0;
            end
        end
    endtask

    task model_apply_update;
        input next_side;
        input [3:0] next_price_idx;
        input [15:0] next_upd_qty;
        begin
            if (next_side == 1'b0)
                model_bid[next_price_idx] = next_upd_qty;
            else
                model_ask[next_price_idx] = next_upd_qty;

            recompute_expected_from_model;
        end
    endtask

    task check_outputs;
        input [255:0] tag;
        input next_rst;
        input next_upd_valid;
        input next_upd_side;
        input [3:0] next_price_idx;
        input [15:0] next_upd_qty;
        begin
            if (book_valid !== expected_book_valid) begin
                $display("ERROR [cycle %0d][%0s]: book_valid mismatch. Expected %b, got %b (rst=%b upd_valid=%b side=%b idx=%0d qty=%0d)",
                         cycle_count, tag, expected_book_valid, book_valid,
                         next_rst, next_upd_valid, next_upd_side, next_price_idx, next_upd_qty);
                errors = errors + 1;
            end

            if (best_bid_idx !== expected_best_bid_idx) begin
                $display("ERROR [cycle %0d][%0s]: best_bid_idx mismatch. Expected %0d, got %0d (rst=%b upd_valid=%b side=%b idx=%0d qty=%0d)",
                         cycle_count, tag, expected_best_bid_idx, best_bid_idx,
                         next_rst, next_upd_valid, next_upd_side, next_price_idx, next_upd_qty);
                errors = errors + 1;
            end

            if (best_bid_qty !== expected_best_bid_qty) begin
                $display("ERROR [cycle %0d][%0s]: best_bid_qty mismatch. Expected %0d, got %0d (rst=%b upd_valid=%b side=%b idx=%0d qty=%0d)",
                         cycle_count, tag, expected_best_bid_qty, best_bid_qty,
                         next_rst, next_upd_valid, next_upd_side, next_price_idx, next_upd_qty);
                errors = errors + 1;
            end

            if (best_ask_idx !== expected_best_ask_idx) begin
                $display("ERROR [cycle %0d][%0s]: best_ask_idx mismatch. Expected %0d, got %0d (rst=%b upd_valid=%b side=%b idx=%0d qty=%0d)",
                         cycle_count, tag, expected_best_ask_idx, best_ask_idx,
                         next_rst, next_upd_valid, next_upd_side, next_price_idx, next_upd_qty);
                errors = errors + 1;
            end

            if (best_ask_qty !== expected_best_ask_qty) begin
                $display("ERROR [cycle %0d][%0s]: best_ask_qty mismatch. Expected %0d, got %0d (rst=%b upd_valid=%b side=%b idx=%0d qty=%0d)",
                         cycle_count, tag, expected_best_ask_qty, best_ask_qty,
                         next_rst, next_upd_valid, next_upd_side, next_price_idx, next_upd_qty);
                errors = errors + 1;
            end

            if (spread_ticks !== expected_spread_ticks) begin
                $display("ERROR [cycle %0d][%0s]: spread_ticks mismatch. Expected %0d, got %0d (rst=%b upd_valid=%b side=%b idx=%0d qty=%0d)",
                         cycle_count, tag, expected_spread_ticks, spread_ticks,
                         next_rst, next_upd_valid, next_upd_side, next_price_idx, next_upd_qty);
                errors = errors + 1;
            end
        end
    endtask

    task step_cycle;
        input [255:0] tag;
        input next_rst;
        input next_upd_valid;
        input next_upd_side;
        input [3:0] next_price_idx;
        input [15:0] next_upd_qty;
        begin
            rst = next_rst;
            upd_valid = next_upd_valid;
            upd_side = next_upd_side;
            price_idx = next_price_idx;
            upd_qty = next_upd_qty;

            @(posedge clk);
            #1;

            cycle_count = cycle_count + 1;
            tests_run = tests_run + 1;

            if (next_rst) begin
                clear_model;
            end else if (next_upd_valid) begin
                model_apply_update(next_upd_side, next_price_idx, next_upd_qty);
            end

            check_outputs(tag, next_rst, next_upd_valid, next_upd_side, next_price_idx, next_upd_qty);
        end
    endtask

    task run_random_regression;
        input integer num_cycles;
        integer iter;
        integer rand_word;
        begin
            for (iter = 0; iter < num_cycles; iter = iter + 1) begin
                rand_word = $random(rng_seed);
                rand_valid = (rand_word[2:0] != 3'b000);

                rand_side = $random(rng_seed) & 1;

                if (rand_side == 1'b0)
                    rand_idx = $random(rng_seed) & 4'h7;
                else
                    rand_idx = 4'd8 | ($random(rng_seed) & 4'h7);

                rand_qty = $random(rng_seed) & 16'h01ff;
                if (($random(rng_seed) & 16'h3) == 16'h0)
                    rand_qty = 16'd0;

                if ((iter % 53) == 0)
                    step_cycle("random_reset", 1'b1, 1'b1, rand_side, rand_idx, rand_qty);
                else
                    step_cycle("random", 1'b0, rand_valid, rand_side, rand_idx, rand_qty);
            end
        end
    endtask

    initial begin
        rst = 1'b0;
        upd_valid = 1'b0;
        upd_side = 1'b0;
        price_idx = 4'd0;
        upd_qty = 16'd0;
        rng_seed = 32'h42c0ffee;

        clear_model;

        $display("===========================================");
        $display("  Fixed-Ladder Top-of-Book Builder TB");
        $display("===========================================");

        step_cycle("reset_clear_0",             1'b1, 1'b0, 1'b0, 4'd0, 16'd0);
        step_cycle("reset_ignore_update",       1'b1, 1'b1, 1'b0, 4'd6, 16'd55);
        step_cycle("post_reset_idle",           1'b0, 1'b0, 1'b1, 4'd4, 16'd123);

        step_cycle("bid_only_level5",           1'b0, 1'b1, 1'b0, 4'd5, 16'd100);
        step_cycle("idle_hold_invalid",         1'b0, 1'b0, 1'b1, 4'd1, 16'd999);
        step_cycle("ask9_make_valid",           1'b0, 1'b1, 1'b1, 4'd9, 16'd200);
        step_cycle("idle_hold_valid",           1'b0, 1'b0, 1'b0, 4'd2, 16'd333);
        step_cycle("bid3_deeper_nochange",      1'b0, 1'b1, 1'b0, 4'd3, 16'd50);
        step_cycle("ask12_deeper_nochange",     1'b0, 1'b1, 1'b1, 4'd12, 16'd70);
        step_cycle("bid7_improves_top",         1'b0, 1'b1, 1'b0, 4'd7, 16'd80);
        step_cycle("ask8_tightens_top",         1'b0, 1'b1, 1'b1, 4'd8, 16'd60);
        step_cycle("bid7_qty_replace",          1'b0, 1'b1, 1'b0, 4'd7, 16'd33);
        step_cycle("bid7_clear_expose5",        1'b0, 1'b1, 1'b0, 4'd7, 16'd0);
        step_cycle("ask8_clear_expose9",        1'b0, 1'b1, 1'b1, 4'd8, 16'd0);
        step_cycle("bid5_clear_expose3",        1'b0, 1'b1, 1'b0, 4'd5, 16'd0);
        step_cycle("bid3_clear_invalidates",    1'b0, 1'b1, 1'b0, 4'd3, 16'd0);
        step_cycle("idle_invalid_after_clear",  1'b0, 1'b0, 1'b1, 4'd14, 16'd222);

        step_cycle("bid7_repopulate",           1'b0, 1'b1, 1'b0, 4'd7, 16'd25);
        step_cycle("ask9_clear_expose12",       1'b0, 1'b1, 1'b1, 4'd9, 16'd0);
        step_cycle("ask12_clear_invalidates",   1'b0, 1'b1, 1'b1, 4'd12, 16'd0);
        step_cycle("ask15_only",                1'b0, 1'b1, 1'b1, 4'd15, 16'd99);
        step_cycle("bid14_make_valid",          1'b0, 1'b1, 1'b0, 4'd14, 16'd31);
        step_cycle("midstream_reset",           1'b1, 1'b0, 1'b0, 4'd0, 16'd0);
        step_cycle("post_midreset_idle",        1'b0, 1'b0, 1'b1, 4'd10, 16'd500);
        step_cycle("restart_bid1",              1'b0, 1'b1, 1'b0, 4'd1, 16'd11);
        step_cycle("restart_ask10",             1'b0, 1'b1, 1'b1, 4'd10, 16'd22);
        step_cycle("restart_bid4",              1'b0, 1'b1, 1'b0, 4'd4, 16'd44);
        step_cycle("restart_ask11",             1'b0, 1'b1, 1'b1, 4'd11, 16'd66);
        step_cycle("restart_idle_noise",        1'b0, 1'b0, 1'b0, 4'd15, 16'd777);

        run_random_regression(300);

        $display("");
        $display("===========================================");
        $display("  Tests Run: %0d", tests_run);
        $display("===========================================");

        if (errors == 0) begin
            $display("TEST_RESULT: PASS");
        end else begin
            $display("TEST_RESULT: FAIL (%0d errors)", errors);
        end

        $finish;
    end
endmodule
