`timescale 1ns / 1ps

module tb_async_dual_clock_fifo;
    localparam integer DEPTH = 8;
    localparam integer QUEUE_SIZE = 256;

    reg wr_clk;
    reg wr_rst;
    reg wr_en;
    reg [15:0] wr_data;
    wire wr_full;

    reg rd_clk;
    reg rd_rst;
    reg rd_en;
    wire [15:0] rd_data;
    wire rd_empty;

    integer errors = 0;
    integer checks_run = 0;
    integer wr_cycle_count = 0;
    integer rd_cycle_count = 0;
    integer accepted_writes = 0;
    integer accepted_reads = 0;
    integer blocked_writes = 0;
    integer blocked_reads = 0;
    integer actual_occupancy = 0;
    integer exp_head = 0;
    integer exp_tail = 0;
    integer wr_full_release_deadline = 0;
    integer rd_empty_release_deadline = 0;
    integer before_count;
    integer i;
    integer wait_loops;

    reg wr_full_release_pending = 1'b0;
    reg rd_empty_release_pending = 1'b0;
    reg pre_wr_en;
    reg pre_wr_full;
    reg [15:0] pre_wr_data;
    reg pre_rd_en;
    reg pre_rd_empty;
    reg [15:0] expected_fifo [0:QUEUE_SIZE-1];

    async_dual_clock_fifo uut (
        .wr_clk(wr_clk),
        .wr_rst(wr_rst),
        .wr_en(wr_en),
        .wr_data(wr_data),
        .wr_full(wr_full),
        .rd_clk(rd_clk),
        .rd_rst(rd_rst),
        .rd_en(rd_en),
        .rd_data(rd_data),
        .rd_empty(rd_empty)
    );

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

    initial begin
        rd_clk = 1'b0;
        #4;
        forever #7 rd_clk = ~rd_clk;
    end

    task reset_model;
        begin
            actual_occupancy = 0;
            exp_head = 0;
            exp_tail = 0;
            wr_full_release_pending = 1'b0;
            rd_empty_release_pending = 1'b0;
            wr_full_release_deadline = 0;
            rd_empty_release_deadline = 0;
        end
    endtask

    task enqueue_expected;
        input [15:0] value;
        begin
            expected_fifo[exp_tail] = value;
            if (exp_tail == QUEUE_SIZE - 1)
                exp_tail = 0;
            else
                exp_tail = exp_tail + 1;
        end
    endtask

    task pop_and_compare;
        input [255:0] tag;
        begin
            if (exp_head == exp_tail) begin
                $display("ERROR [%s rd_cycle=%0d]: unexpected accepted read with empty expected queue",
                         tag, rd_cycle_count);
                errors = errors + 1;
            end else begin
                if (rd_data !== expected_fifo[exp_head]) begin
                    $display("ERROR [%s rd_cycle=%0d]: rd_data mismatch. Expected %h, got %h",
                             tag, rd_cycle_count, expected_fifo[exp_head], rd_data);
                    errors = errors + 1;
                end

                if (exp_head == QUEUE_SIZE - 1)
                    exp_head = 0;
                else
                    exp_head = exp_head + 1;
            end
        end
    endtask

    task apply_joint_reset;
        begin
            wr_en = 1'b0;
            rd_en = 1'b0;
            wr_data = 16'h0000;
            wr_rst = 1'b1;
            rd_rst = 1'b1;

            repeat (4) @(posedge wr_clk);
            repeat (4) @(posedge rd_clk);

            wr_rst = 1'b0;
            rd_rst = 1'b0;

            repeat (2) @(posedge wr_clk);
            repeat (2) @(posedge rd_clk);
            #1;

            if (wr_full !== 1'b0) begin
                $display("ERROR [reset_release]: wr_full should be low after reset release");
                errors = errors + 1;
            end

            if (rd_empty !== 1'b1) begin
                $display("ERROR [reset_release]: rd_empty should be high after reset release");
                errors = errors + 1;
            end
        end
    endtask

    task write_word_blocking;
        input [15:0] value;
        integer writes_before;
        begin
            writes_before = accepted_writes;
            @(negedge wr_clk);
            wr_data = value;
            wr_en = 1'b1;

            wait_loops = 0;
            while (accepted_writes == writes_before && wait_loops < 40) begin
                @(negedge wr_clk);
                wait_loops = wait_loops + 1;
            end

            if (accepted_writes == writes_before) begin
                $display("ERROR [write_word_blocking]: timed out waiting for accepted write of %h", value);
                errors = errors + 1;
            end

            wr_en = 1'b0;
            wr_data = 16'h0000;
        end
    endtask

    task pulse_write_once;
        input [15:0] value;
        begin
            @(negedge wr_clk);
            wr_data = value;
            wr_en = 1'b1;
            @(negedge wr_clk);
            wr_en = 1'b0;
            wr_data = 16'h0000;
        end
    endtask

    task read_word_blocking;
        integer reads_before;
        begin
            reads_before = accepted_reads;
            @(negedge rd_clk);
            rd_en = 1'b1;

            wait_loops = 0;
            while (accepted_reads == reads_before && wait_loops < 40) begin
                @(negedge rd_clk);
                wait_loops = wait_loops + 1;
            end

            if (accepted_reads == reads_before) begin
                $display("ERROR [read_word_blocking]: timed out waiting for accepted read");
                errors = errors + 1;
            end

            rd_en = 1'b0;
        end
    endtask

    task pulse_read_once;
        begin
            @(negedge rd_clk);
            rd_en = 1'b1;
            @(negedge rd_clk);
            rd_en = 1'b0;
        end
    endtask

    task writer_stream;
        input integer count;
        input [15:0] base;
        integer idx;
        integer gap;
        begin
            for (idx = 0; idx < count; idx = idx + 1) begin
                write_word_blocking(base + idx[15:0]);
                gap = idx % 3;
                repeat (gap)
                    @(negedge wr_clk);
            end
        end
    endtask

    task reader_stream;
        input integer count;
        integer idx;
        integer gap;
        begin
            repeat (3) @(negedge rd_clk);

            for (idx = 0; idx < count; idx = idx + 1) begin
                read_word_blocking;
                gap = (idx + 1) % 4;
                repeat (gap)
                    @(negedge rd_clk);
            end
        end
    endtask

    task ensure_model_empty;
        input [255:0] tag;
        begin
            if (actual_occupancy != 0) begin
                $display("ERROR [%s]: expected occupancy 0, got %0d", tag, actual_occupancy);
                errors = errors + 1;
            end

            if (exp_head != exp_tail) begin
                $display("ERROR [%s]: expected queue not empty at end of test", tag);
                errors = errors + 1;
            end
        end
    endtask

    always @(posedge wr_clk) begin
        pre_wr_en = wr_en;
        pre_wr_full = wr_full;
        pre_wr_data = wr_data;
        wr_cycle_count = wr_cycle_count + 1;
        #1;

        if (wr_rst) begin
            reset_model;

            if (wr_full !== 1'b0) begin
                $display("ERROR [wr_reset wr_cycle=%0d]: wr_full must be low during reset", wr_cycle_count);
                errors = errors + 1;
            end
        end else begin
            if (pre_wr_en && !pre_wr_full) begin
                if (actual_occupancy >= DEPTH) begin
                    $display("ERROR [wr_accept wr_cycle=%0d]: accepted write while FIFO already full",
                             wr_cycle_count);
                    errors = errors + 1;
                end else begin
                    enqueue_expected(pre_wr_data);
                    actual_occupancy = actual_occupancy + 1;
                    accepted_writes = accepted_writes + 1;

                    if (actual_occupancy == 1) begin
                        rd_empty_release_pending = 1'b1;
                        rd_empty_release_deadline = rd_cycle_count + 2;
                    end
                end
            end else if (pre_wr_en && pre_wr_full) begin
                blocked_writes = blocked_writes + 1;
            end

            checks_run = checks_run + 1;

            if (actual_occupancy == DEPTH) begin
                wr_full_release_pending = 1'b0;

                if (wr_full !== 1'b1) begin
                    $display("ERROR [wr_flag wr_cycle=%0d]: wr_full must assert when FIFO becomes full",
                             wr_cycle_count);
                    errors = errors + 1;
                end
            end else if (wr_full_release_pending) begin
                if (wr_full === 1'b0) begin
                    wr_full_release_pending = 1'b0;
                end else if (wr_cycle_count > wr_full_release_deadline) begin
                    $display("ERROR [wr_flag wr_cycle=%0d]: wr_full did not deassert within 2 wr_clk edges after read freed space",
                             wr_cycle_count);
                    errors = errors + 1;
                    wr_full_release_pending = 1'b0;
                end
            end
        end

        if (errors >= 25) begin
            $display("Too many write-side errors, stopping early");
            $finish;
        end
    end

    always @(posedge rd_clk) begin
        pre_rd_en = rd_en;
        pre_rd_empty = rd_empty;
        rd_cycle_count = rd_cycle_count + 1;
        #1;

        if (rd_rst) begin
            reset_model;

            if (rd_empty !== 1'b1) begin
                $display("ERROR [rd_reset rd_cycle=%0d]: rd_empty must be high during reset", rd_cycle_count);
                errors = errors + 1;
            end
        end else begin
            if (pre_rd_en && !pre_rd_empty) begin
                if (actual_occupancy <= 0) begin
                    $display("ERROR [rd_accept rd_cycle=%0d]: accepted read while FIFO is empty",
                             rd_cycle_count);
                    errors = errors + 1;
                end else begin
                    pop_and_compare("rd_accept");
                    actual_occupancy = actual_occupancy - 1;
                    accepted_reads = accepted_reads + 1;

                    if (actual_occupancy == DEPTH - 1) begin
                        wr_full_release_pending = 1'b1;
                        wr_full_release_deadline = wr_cycle_count + 2;
                    end

                    if (actual_occupancy == 0)
                        rd_empty_release_pending = 1'b0;
                end
            end else if (pre_rd_en && pre_rd_empty) begin
                blocked_reads = blocked_reads + 1;
            end

            checks_run = checks_run + 1;

            if (actual_occupancy == 0) begin
                rd_empty_release_pending = 1'b0;

                if (rd_empty !== 1'b1) begin
                    $display("ERROR [rd_flag rd_cycle=%0d]: rd_empty must assert when FIFO becomes empty",
                             rd_cycle_count);
                    errors = errors + 1;
                end
            end else if (rd_empty_release_pending) begin
                if (rd_empty === 1'b0) begin
                    rd_empty_release_pending = 1'b0;
                end else if (rd_cycle_count > rd_empty_release_deadline) begin
                    $display("ERROR [rd_flag rd_cycle=%0d]: rd_empty did not deassert within 2 rd_clk edges after write made data available",
                             rd_cycle_count);
                    errors = errors + 1;
                    rd_empty_release_pending = 1'b0;
                end
            end
        end

        if (errors >= 25) begin
            $display("Too many read-side errors, stopping early");
            $finish;
        end
    end

    initial begin
        wr_rst = 1'b0;
        rd_rst = 1'b0;
        wr_en = 1'b0;
        rd_en = 1'b0;
        wr_data = 16'h0000;

        $display("==============================================");
        $display("   Async Dual-Clock FIFO Self-Checking Test");
        $display("==============================================");

        apply_joint_reset;

        // Basic write then read ordering.
        $display("PHASE: basic_order");
        write_word_blocking(16'h1111);
        write_word_blocking(16'h2222);
        write_word_blocking(16'h3333);
        repeat (4) @(posedge rd_clk);
        read_word_blocking;
        read_word_blocking;
        read_word_blocking;
        ensure_model_empty("basic_order");

        // Fill to full and confirm extra writes are blocked.
        $display("PHASE: full_and_wrap");
        apply_joint_reset;
        for (i = 0; i < DEPTH; i = i + 1)
            write_word_blocking(16'h2000 + i[15:0]);

        before_count = accepted_writes;
        pulse_write_once(16'hDEAD);
        repeat (2) @(posedge wr_clk);

        if (accepted_writes != before_count) begin
            $display("ERROR [full_block]: write was accepted while FIFO should have been full");
            errors = errors + 1;
        end

        // Read one element to create free space, then write one more to force wrap-around.
        repeat (4) @(posedge rd_clk);
        read_word_blocking;
        write_word_blocking(16'h2BAD);

        // Drain fully and confirm extra reads are blocked.
        for (i = 0; i < DEPTH; i = i + 1)
            read_word_blocking;

        ensure_model_empty("drain_after_wrap");

        before_count = accepted_reads;
        pulse_read_once;
        repeat (2) @(posedge rd_clk);

        if (accepted_reads != before_count) begin
            $display("ERROR [empty_block]: read was accepted while FIFO should have been empty");
            errors = errors + 1;
        end

        // Reset during stored-data state must discard old contents.
        $display("PHASE: reset_flush");
        write_word_blocking(16'h3001);
        write_word_blocking(16'h3002);
        write_word_blocking(16'h3003);
        apply_joint_reset;
        before_count = accepted_reads;
        pulse_read_once;
        repeat (2) @(posedge rd_clk);

        if (accepted_reads != before_count) begin
            $display("ERROR [reset_flush]: stale data remained readable after reset");
            errors = errors + 1;
        end

        write_word_blocking(16'h30AA);
        read_word_blocking;
        ensure_model_empty("post_reset_reuse");

        // Concurrent traffic with different gaps to stress asynchronous behaviour.
        $display("PHASE: concurrent_stream");
        apply_joint_reset;
        fork
            writer_stream(20, 16'h4000);
            reader_stream(20);
        join

        repeat (4) @(posedge wr_clk);
        repeat (4) @(posedge rd_clk);
        ensure_model_empty("concurrent_stream");

        $display("");
        $display("==============================================");
        $display("  Checks Run     : %0d", checks_run);
        $display("  Accepted Writes: %0d", accepted_writes);
        $display("  Accepted Reads : %0d", accepted_reads);
        $display("  Blocked Writes : %0d", blocked_writes);
        $display("  Blocked Reads  : %0d", blocked_reads);
        $display("==============================================");

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

        $finish;
    end
endmodule
