`timescale 1ns/1ps
module arbiter_case #(parameter int N = 3) (input logic clk, output logic done);
    logic rst_n = 0;
    logic [N-1:0] request = '0;
    logic grant_ready = 0;
    logic [N-1:0] grant;
    logic grant_valid;
    int priority_index = 0;
    int held_index = -1;
    int transfers = 0;
    logic [N-1:0] pending = '0;
    logic [31:0] random_state = 32'h492a_8e19 ^ N;

    round_robin_arbiter #(.REQUESTERS(N)) dut (.*);

    task automatic cycle(input logic [N-1:0] offered, input logic ready);
        int winner, index;
        logic [N-1:0] expected;
        @(negedge clk);
        request = offered | pending;
        grant_ready = ready;

        winner = held_index;
        for (int distance = 0; distance < N; distance++) begin
            index = (priority_index + distance) % N;
            if (winner < 0 && request[index])
                winner = index;
        end
        expected = '0;
        if (winner >= 0)
            expected[winner] = 1;

        @(posedge clk);
        if (grant !== expected || grant_valid !== (winner >= 0))
            $fatal(1, "N=%0d request=%b ready=%b expected=%b grant=%b valid=%b",
                   N, request, ready, expected, grant, grant_valid);
        held_index = ready ? -1 : winner;
        pending = ready ? request & ~expected : request;
        if (winner >= 0 && ready) begin
            priority_index = (winner + 1) % N;
            transfers++;
        end
    endtask

    initial begin
        done = 0;
        repeat (3) @(negedge clk);
        if (grant_valid !== 0 || grant !== '0)
            $fatal(1, "Grant during reset, N=%0d", N);
        rst_n = 1;
        cycle('0, 1);
        // Full contention, then a stall with a new, higher-priority request.
        repeat (3 * N) cycle('1, 1);
        repeat (N) cycle('0, 1);
        cycle(N'(1) << (N - 1), 0);
        repeat (4) cycle('1, 0);
        cycle('1, 1);

        repeat (600) begin
            random_state = random_state ^ (random_state << 13);
            random_state = random_state ^ (random_state >> 17);
            random_state = random_state ^ (random_state << 5);
            cycle(N'(random_state), random_state[16]);
        end
        repeat (N + 1) cycle('0, 1);
        if (transfers < 50 || pending != '0)
            $fatal(1, "Insufficient activity or a request failed to drain, N=%0d", N);
        $display("PASS arbiter N=%0d (%0d accepted grants)", N, transfers);
        done = 1;
    end
endmodule

module tb_round_robin_arbiter;
    logic clk = 0;
    wire [3:0] done;
    always #5 clk = !clk;

    arbiter_case #(.N(1)) one (clk, done[0]);
    arbiter_case #(.N(3)) three (clk, done[1]);
    arbiter_case #(.N(4)) four (clk, done[2]);
    arbiter_case #(.N(5)) five (clk, done[3]);

    initial begin
        wait (&done);
        $finish;
    end
    initial begin
        #20000;
        $fatal(1, "Arbiter test timed out");
    end
endmodule
