// Copyright 2018 ETH Zurich and University of Bologna.
// Copyright and related rights are licensed under the Solderpad Hardware
// License, Version 0.51 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at
// http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law
// or agreed to in writing, software, hardware and materials distributed under
// this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.

module id_queue_tb #(
    parameter int ID_WIDTH = 10,
    parameter int CAPACITY = 30,
    parameter int unsigned INP_MIN_WAIT_CYCLES = 0,
    parameter int unsigned INP_MAX_WAIT_CYCLES = 40,
    parameter int unsigned OUP_MIN_WAIT_CYCLES = 0,
    parameter int unsigned OUP_MAX_WAIT_CYCLES = INP_MAX_WAIT_CYCLES/2,
    parameter int unsigned N_CHECKS = 10000,
    parameter bit VERBOSE = 1'b0,
    parameter type data_t = logic[3:0]
);

    localparam time TCLK = 10ns;
    localparam time TA = TCLK * 1/4;
    localparam time TT = TCLK * 3/4;

    typedef logic [ID_WIDTH-1:0] id_t;
    typedef logic [$bits(data_t)-1:0] mask_t;

    typedef struct packed {
        id_t    id;
        data_t  data;
    } queue_t;

    typedef struct packed {
        data_t  data;
        mask_t  mask;
    } exists_t;

    logic       clk,
                rst_n;

    data_t      inp_data,
                oup_data;

    exists_t    exists_inp;

    id_t        inp_id,
                oup_id;

    queue_t     queue_inp;

    logic       exists,
                exists_req,         exists_gnt,
                inp_req,            inp_gnt,
                oup_req,            oup_gnt,
                oup_pop,
                oup_data_valid;

    clk_rst_gen #(
        .ClkPeriod    (TCLK),
        .RstClkCycles (5)
    ) i_clk_rst_gen (
        .clk_o  (clk),
        .rst_no (rst_n)
    );

    id_queue #(
        .ID_WIDTH   (ID_WIDTH),
        .CAPACITY   (CAPACITY),
        .data_t     (data_t)
    ) dut (
        .clk_i              (clk),
        .rst_ni             (rst_n),

        .inp_id_i           (inp_id),
        .inp_data_i         (inp_data),
        .inp_req_i          (inp_req),
        .inp_gnt_o          (inp_gnt),

        .exists_data_i      (exists_inp.data),
        .exists_mask_i      (exists_inp.mask),
        .exists_req_i       (exists_req),
        .exists_o           (exists),
        .exists_gnt_o       (exists_gnt),

        .oup_id_i           (oup_id),
        .oup_pop_i          (oup_pop),
        .oup_req_i          (oup_req),
        .oup_data_o         (oup_data),
        .oup_data_valid_o   (oup_data_valid),
        .oup_gnt_o          (oup_gnt)
    );

    // Random Input Driver
    rand_stream_mst #(
        .data_t             (queue_t),
        .MinWaitCycles      (INP_MIN_WAIT_CYCLES),
        .MaxWaitCycles      (INP_MAX_WAIT_CYCLES),
        .ApplDelay          (TA),
        .AcqDelay           (TT)
    ) i_inp_mst (
        .clk_i      (clk),
        .rst_ni     (rst_n),

        .data_o     (queue_inp),
        .valid_o    (inp_req),
        .ready_i    (inp_gnt)
    );
    assign inp_id   = queue_inp.id;
    assign inp_data = queue_inp.data;

    // Random Output Driver
    rand_stream_mst #(
        .data_t             (logic),
        .MinWaitCycles      (OUP_MIN_WAIT_CYCLES),
        .MaxWaitCycles      (OUP_MAX_WAIT_CYCLES),
        .ApplDelay          (TA),
        .AcqDelay           (TT)
    ) i_oup_mst (
        .clk_i      (clk),
        .rst_ni     (rst_n),

        .data_o     (),
        .valid_o    (oup_req),
        .ready_i    (oup_gnt)
    );

    // Random Exists Driver
    rand_stream_mst #(
        .data_t             (exists_t),
        .MinWaitCycles      (OUP_MIN_WAIT_CYCLES),
        .MaxWaitCycles      (OUP_MAX_WAIT_CYCLES),
        .ApplDelay          (TA),
        .AcqDelay           (TT)
    ) i_exists_mst (
        .clk_i      (clk),
        .rst_ni     (rst_n),

        .data_o     (exists_inp),
        .valid_o    (exists_req),
        .ready_i    (exists_gnt)
    );

    // Model expected state of queue.
    import rand_id_queue_pkg::*;
    rand_id_queue #(
        .data_t     (data_t),
        .ID_WIDTH   (ID_WIDTH)
    ) exp_queue = new;
    initial begin
        wait (rst_n);
        forever begin
            @(posedge clk);
            if (inp_req && inp_gnt) begin
                exp_queue.push(inp_id, inp_data);
                if (VERBOSE) begin
                    $display("%0t: new entry %0x at ID %0x", $time, inp_data, inp_id);
                end
            end
            if (oup_req && oup_gnt && oup_pop) begin
                if (VERBOSE) begin
                    $display("%0t: removed entry from ID %0x", $time, oup_id);
                end
                void'(exp_queue.pop_id(oup_id));
            end
        end
    end

    // Check the output of the ID queues.
    initial begin
        wait (rst_n);
        forever begin
            @(posedge clk);
            #(TT);
            if (exists_req && exists_gnt) begin
                automatic logic match = 1'b0;
                automatic mask_t mask = exists_inp.mask;
                automatic data_t masked_exists = exists_inp.data & mask;
                for (int unsigned id = 0; id < 2**ID_WIDTH; id++) begin
                    for (int unsigned idx = 0; idx < exp_queue.queues[id].size(); idx++) begin
                        match = ((exp_queue.queues[id][idx] & mask) == masked_exists);
                        if (match) begin
                            break;
                        end
                    end
                    if (match) begin
                        break;
                    end
                end
                assert (exists == match) else begin
                    if (match) begin
                        $error("Entry with value %0x and mask %0b should exist but ID queue did not find it!",
                            exists_inp.data, exists_inp.mask);
                    end else begin
                        $error("Entry with value %0x and mask %0b should NOT exist but ID queue found it!",
                            exists_inp.data, exists_inp.mask);
                    end
                end
            end
            if (oup_req && oup_gnt) begin
                if (oup_data_valid) begin
                    automatic data_t exp_data = exp_queue.get(oup_id);
                    assert (exp_data == oup_data)
                        else $error("Expected to read %0x from ID %0x but got %0x!",
                            exp_data, oup_id, oup_data);
                    if (VERBOSE) begin
                        $display("%0t: read %0x at ID %0x!", $time, oup_data, oup_id);
                    end
                end else begin
                    assert (exp_queue.queues[oup_id].size() == 0)
                        else $error("Expected to get valid output from ID %0x but did not!",
                            oup_id);
                end
            end
        end
    end

    // Model slave at output and consume values.
    initial begin
        void'(std::randomize(oup_id));
        oup_pop = 1'b0;
        wait (rst_n);
        @(posedge clk);
        #(TT);
        forever begin
            @(posedge clk);
            #(TA);
            if (exp_queue.empty()) begin
                oup_pop = 1'b0;
            end else begin
                void'(std::randomize(oup_pop));
                oup_id = exp_queue.rand_id();
            end
            #(TT-TA);
            while (oup_req && !oup_gnt) begin
                @(posedge clk);
                #(TT);
            end
        end
    end

    // Terminate test after specified number of checks is reached.
    initial begin
        static int unsigned n_pops = 0;
        wait (rst_n);
        forever begin
            @(posedge clk);
            #(TT);
            if (oup_req && oup_gnt && oup_pop && oup_data_valid) begin
                n_pops++;
            end
            #(TCLK-TT);
            if (n_pops >= N_CHECKS) begin
                $display("Finished with a total of %0d random entries fed through the ID queue.",
                    n_pops);
                $finish(0);
            end
        end
    end

endmodule