`timescale 1ns / 1ps

module tb_spi_master_controller;
    reg clk;
    reg rst;
    reg start;
    reg [7:0] tx_data;
    reg miso;

    wire mosi;
    wire sclk;
    wire cs_n;
    wire busy;
    wire done;
    wire [7:0] rx_data;

    integer errors = 0;
    integer tests_run = 0;
    integer cycle_count = 0;
    integer bit_idx;

    spi_master_controller uut (
        .clk(clk),
        .rst(rst),
        .start(start),
        .tx_data(tx_data),
        .miso(miso),
        .mosi(mosi),
        .sclk(sclk),
        .cs_n(cs_n),
        .busy(busy),
        .done(done),
        .rx_data(rx_data)
    );

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

    task apply_cycle;
        input [255:0] tag;
        input next_rst;
        input next_start;
        input [7:0] next_tx_data;
        input next_miso;
        input exp_busy;
        input exp_cs_n;
        input exp_sclk;
        input exp_done;
        input check_mosi;
        input exp_mosi;
        input check_rx;
        input [7:0] exp_rx;
        begin
            rst = next_rst;
            start = next_start;
            tx_data = next_tx_data;
            miso = next_miso;

            @(posedge clk);
            #1;

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

            if (busy !== exp_busy) begin
                $display("ERROR [%s cycle %0d]: busy mismatch. expected=%b got=%b",
                         tag, cycle_count, exp_busy, busy);
                errors = errors + 1;
            end

            if (cs_n !== exp_cs_n) begin
                $display("ERROR [%s cycle %0d]: cs_n mismatch. expected=%b got=%b",
                         tag, cycle_count, exp_cs_n, cs_n);
                errors = errors + 1;
            end

            if (sclk !== exp_sclk) begin
                $display("ERROR [%s cycle %0d]: sclk mismatch. expected=%b got=%b",
                         tag, cycle_count, exp_sclk, sclk);
                errors = errors + 1;
            end

            if (done !== exp_done) begin
                $display("ERROR [%s cycle %0d]: done mismatch. expected=%b got=%b",
                         tag, cycle_count, exp_done, done);
                errors = errors + 1;
            end

            if (check_mosi && (mosi !== exp_mosi)) begin
                $display("ERROR [%s cycle %0d]: mosi mismatch. expected=%b got=%b",
                         tag, cycle_count, exp_mosi, mosi);
                errors = errors + 1;
            end

            if (check_rx && (rx_data !== exp_rx)) begin
                $display("ERROR [%s cycle %0d]: rx_data mismatch. expected=0x%02h got=0x%02h",
                         tag, cycle_count, exp_rx, rx_data);
                errors = errors + 1;
            end
        end
    endtask

    task idle_cycle;
        input [255:0] tag;
        begin
            apply_cycle(tag, 1'b0, 1'b0, 8'h00, 1'b1,
                        1'b0, 1'b1, 1'b0, 1'b0,
                        1'b0, 1'b0, 1'b0, 8'h00);
        end
    endtask

    task run_transfer;
        input [255:0] tag;
        input [7:0] tx_byte;
        input [7:0] rx_byte;
        begin
            apply_cycle({tag, "_accept"}, 1'b0, 1'b1, tx_byte, rx_byte[7],
                        1'b1, 1'b0, 1'b0, 1'b0,
                        1'b1, tx_byte[7], 1'b0, 8'h00);

            for (bit_idx = 7; bit_idx >= 0; bit_idx = bit_idx - 1) begin
                apply_cycle({tag, "_sample"}, 1'b0, 1'b0, tx_byte, rx_byte[bit_idx],
                            1'b1, 1'b0, 1'b1, 1'b0,
                            1'b1, tx_byte[bit_idx], 1'b0, 8'h00);

                if (bit_idx != 0) begin
                    apply_cycle({tag, "_shift"}, 1'b0, 1'b0, tx_byte, rx_byte[bit_idx - 1],
                                1'b1, 1'b0, 1'b0, 1'b0,
                                1'b1, tx_byte[bit_idx - 1], 1'b0, 8'h00);
                end
            end

            apply_cycle({tag, "_done"}, 1'b0, 1'b0, tx_byte, 1'b1,
                        1'b0, 1'b1, 1'b0, 1'b1,
                        1'b0, 1'b0, 1'b1, rx_byte);
        end
    endtask

    initial begin
        rst = 1'b0;
        start = 1'b0;
        tx_data = 8'h00;
        miso = 1'b1;

        $display("===========================================");
        $display("        SPI Master Controller Testbench");
        $display("===========================================");

        // Reset and idle behavior.
        apply_cycle("reset_idle",      1'b1, 1'b0, 8'h00, 1'b1,
                    1'b0, 1'b1, 1'b0, 1'b0,
                    1'b0, 1'b0, 1'b0, 8'h00);
        apply_cycle("reset_with_start",1'b1, 1'b1, 8'hA6, 1'b0,
                    1'b0, 1'b1, 1'b0, 1'b0,
                    1'b0, 1'b0, 1'b0, 8'h00);
        idle_cycle("post_reset_idle_0");
        idle_cycle("post_reset_idle_1");

        // Main transfer and start-while-busy check.
        apply_cycle("busy_accept", 1'b0, 1'b1, 8'hA6, 1'b0,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_sample7", 1'b0, 1'b0, 8'hA6, 1'b0,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_shift6_with_start", 1'b0, 1'b1, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_sample6", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_shift5", 1'b0, 1'b0, 8'h55, 1'b0,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_sample5", 1'b0, 1'b0, 8'h55, 1'b0,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_shift4", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_sample4", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_shift3", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_sample3", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_shift2", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_sample2", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_shift1", 1'b0, 1'b0, 8'h55, 1'b0,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_sample1", 1'b0, 1'b0, 8'h55, 1'b0,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("busy_shift0", 1'b0, 1'b0, 8'h55, 1'b0,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_sample0", 1'b0, 1'b0, 8'h55, 1'b0,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b0, 1'b0, 8'h00);
        apply_cycle("busy_done", 1'b0, 1'b0, 8'h55, 1'b1,
                    1'b0, 1'b1, 1'b0, 1'b1,
                    1'b0, 1'b0, 1'b1, 8'h5C);
        idle_cycle("busy_done_clear");

        // Back-to-back transfers.
        run_transfer("back_to_back_1", 8'h3C, 8'hC3);
        run_transfer("back_to_back_2", 8'h81, 8'h7E);
        idle_cycle("after_back_to_back");

        // Reset in the middle of a transfer aborts it immediately.
        apply_cycle("reset_mid_accept", 1'b0, 1'b1, 8'hF0, 1'b1,
                    1'b1, 1'b0, 1'b0, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("reset_mid_sample7", 1'b0, 1'b0, 8'hF0, 1'b0,
                    1'b1, 1'b0, 1'b1, 1'b0,
                    1'b1, 1'b1, 1'b0, 8'h00);
        apply_cycle("reset_mid_assert", 1'b1, 1'b0, 8'hF0, 1'b1,
                    1'b0, 1'b1, 1'b0, 1'b0,
                    1'b0, 1'b0, 1'b0, 8'h00);
        idle_cycle("reset_mid_release0");
        idle_cycle("reset_mid_release1");

        $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
