`timescale 1ns/1ps
module tb_async_fifo #(
    parameter int ADDR_WIDTH = 3,
    parameter int WRITE_HALF_PERIOD = 5,
    parameter int READ_HALF_PERIOD = 7
);
    localparam int DEPTH = 2 ** ADDR_WIDTH;
    logic arst_n = 1;
    logic wr_clk = 0, rd_clk = 0;
    logic wr_en = 0, rd_en = 0;
    logic [15:0] wr_data = '0, rd_data;
    logic wr_full, rd_valid, rd_empty;

    logic [15:0] expected [0:4095];
    int written = 0, received = 0;
    int blocked_writes = 0, blocked_reads = 0;
    logic [31:0] wr_random = 32'h42a7_ed19;
    logic [31:0] rd_random = 32'h1123_f1a9;

    async_fifo #(.DATA_WIDTH(16), .ADDR_WIDTH(ADDR_WIDTH)) dut (.*);

    // Phase offset makes the two sampling edges distinct for the scoreboard.
    always #(WRITE_HALF_PERIOD) wr_clk = !wr_clk;
    initial begin
        #1;
        forever #(READ_HALF_PERIOD) rd_clk = !rd_clk;
    end

    always @(posedge wr_clk) begin
        if (arst_n && wr_en) begin
            if (wr_full)
                blocked_writes++;
            else begin
                if (written - received >= DEPTH)
                    $fatal(1, "FIFO accepted a write when at capacity");
                expected[written] = wr_data;
                written++;
            end
        end
    end

    always @(posedge rd_clk) begin
        logic take;
        logic [15:0] expected_data;
        take = arst_n && rd_en && !rd_empty;
        if (arst_n && rd_en && rd_empty)
            blocked_reads++;
        if (take) begin
            if (received >= written)
                $fatal(1, "FIFO accepted a read with no queued word");
            expected_data = expected[received];
            received++;
        end
        #0.5;
        if (rd_valid !== take)
            $fatal(1, "rd_valid mismatch at %0t", $time);
        if (take && rd_data !== expected_data)
            $fatal(1, "FIFO order/data mismatch at item %0d: expected %h got %h",
                   received - 1, expected_data, rd_data);
    end

    function automatic logic [31:0] advance(input logic [31:0] seed);
        logic [31:0] next_seed;
        next_seed = seed ^ (seed << 13);
        next_seed = next_seed ^ (next_seed >> 17);
        return next_seed ^ (next_seed << 5);
    endfunction

    task automatic flush;
        #2;
        arst_n = 0;
        #2;
        if (wr_full !== 1 || rd_empty !== 1 || rd_valid !== 0)
            $fatal(1, "Reset did not block operations");
        written = 0;
        received = 0;
        repeat (4) @(negedge wr_clk);
        arst_n = 1;
        wait (!wr_full);
        if (rd_empty !== 1)
            $fatal(1, "FIFO contained data after reset");
    endtask

    task automatic fill_queue(input logic [15:0] base);
        repeat (DEPTH + 6) begin
            @(negedge wr_clk);
            wr_en = 1;
            wr_data = base + 16'(written);
        end
        @(negedge wr_clk);
        wr_en = 0;
        if (wr_full !== 1 || written - received != DEPTH)
            $fatal(1, "Expected exactly %0d buffered words", DEPTH);
    endtask

    task automatic drain_queue;
        @(negedge rd_clk);
        rd_en = 1;
        wait (received == written);
        repeat (5) @(negedge rd_clk);
        rd_en = 0;
        if (rd_empty !== 1)
            $fatal(1, "Expected empty after draining");
    endtask

    initial begin
        flush();
        fill_queue(16'h1000);
        drain_queue();

        fork
            begin
                repeat (1000) begin
                    @(negedge wr_clk);
                    wr_random = advance(wr_random);
                    wr_en = wr_random[0] || wr_random[1];
                    wr_data = 16'h2000 + 16'(written);
                end
                @(negedge wr_clk);
                wr_en = 0;
            end
            begin
                repeat (1000 * WRITE_HALF_PERIOD / READ_HALF_PERIOD + 20) begin
                    @(negedge rd_clk);
                    rd_random = advance(rd_random);
                    rd_en = rd_random[0] || rd_random[1];
                end
                @(negedge rd_clk);
                rd_en = 0;
            end
        join
        drain_queue();
        if (received < 100 || blocked_reads == 0 || blocked_writes == 0)
            $fatal(1, "Missing FIFO coverage");
        $display("FIFO depth=%0d checked %0d ordered reads before reset", DEPTH, received);

        fill_queue(16'h7000);
        flush();
        // A flush while full must discard old data before the FIFO is reused.
        fill_queue(16'h9000);
        drain_queue();
        $display("PASS asynchronous FIFO depth=%0d (clock periods %0d/%0d ns)",
                 DEPTH, 2 * WRITE_HALF_PERIOD, 2 * READ_HALF_PERIOD);
        $finish;
    end

    initial begin
        #100000;
        $fatal(1, "FIFO test timed out");
    end
endmodule
