module axi_fifo_tb();

  /*********************************************
  ** User variables
  *********************************************/
  localparam FIFO_SIZE = 1;
  localparam TEST_VECTOR_SIZE = 10;

  /*********************************************
  ** Clocks & Reset
  *********************************************/
  `define CLOCK_FREQ    200e6
  `define RESET_TIME    100

  reg clk;
  initial clk = 1'b0;
  localparam CLOCK_PERIOD = 1e9/`CLOCK_FREQ;
  always
    #(CLOCK_PERIOD) clk = ~clk;

  reg reset;
  initial begin
    reset = 1'b1;
    #(`RESET_TIME);
    @(posedge clk);
    reset = 1'b0;
  end

  /*********************************************
  ** DUT
  *********************************************/
  reg [31:0] i_tdata;
  reg i_tvalid, o_tready;
  wire i_tready, o_tvalid;
  wire [31:0] o_tdata;
  reg clear;

  axi_fifo #(
    .SIZE(FIFO_SIZE),
    .WIDTH(32))
  dut_axi_fifo (
    .clk(clk), .reset(reset), .clear(clear),
    .i_tdata(i_tdata), .i_tvalid(i_tvalid), .i_tready(i_tready),
    .o_tdata(o_tdata), .o_tvalid(o_tvalid), .o_tready(o_tready),
    .space(), .occupied());

  /*********************************************
  ** Testbench
  *********************************************/
  reg [TEST_VECTOR_SIZE-1:0] i_tvalid_sequence;
  reg [TEST_VECTOR_SIZE-1:0] o_tready_sequence;
  integer i,k,n,i_tready_timeout;
  reg [31:0] o_tdata_check;

  initial begin
    i_tdata = {32{1'b1}};
    i_tvalid = 1'b0;
    o_tready = 1'b0;
    i_tready_timeout = 0;
    clear = 1'b0;
    @(negedge reset);
    #(10*CLOCK_PERIOD)
    @(posedge clk);
    $display("*****************************************************");
    $display("**              Begin Assertion Tests              **");
    $display("*****************************************************");
    $display("Test 1 -- Check filling FIFO");
    // Note, if REG_OUTPUT is enabled, the FIFO has space for 1 extra entry
    for (i = 0; i < 2**FIFO_SIZE; i = i + 1) begin
      if (~i_tready) begin
          $display("Test 1 FAILED!");
          $error("FIFO size should be %d entries, but detected %d!",2**FIFO_SIZE,i);
          $stop;
      end
      i_tvalid = 1'b1;
      i_tdata = i_tdata + 32'd1;
      @(posedge clk);
    end
    i_tvalid = 1'b0;
    @(posedge clk);
    if (i_tready) begin
      $display("Test 1 warning!");
      $warning("i_tready still asserted after filling FIFO with %d entries! Might be due to output registering.",i);
      //$stop;
    end
    $display("Test 1 Passed!");
    //////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////
    $display("Test 2 -- Check emptying FIFO");
    for (i = 0; i < 2**FIFO_SIZE; i = i + 1) begin
      if (~o_tvalid) begin
          $display("Test 2 FAILED!");
          $error("FIFO o_tvalid not asserted! Occured at entry %d",2**FIFO_SIZE-i+1);
          $stop;
      end
      o_tready = 1'b1;
      @(posedge clk);
    end
    o_tready = 1'b0;
    @(posedge clk);
    if (o_tvalid) begin
      $display("Test 1 FAILED!");
      $error("o_tvalid still asserted after emptying FIFO!");
      $stop;
    end
    $display("Test 2 Passed!");
    //////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////
    $display("Test 3 -- Check for o_tready / i_tready dropping unexpectantly");
    clear = 1'b1;
    i_tdata = {32{1'b1}};
    i_tvalid = 1'b0;
    o_tready = 1'b0;
    @(posedge clk);
    clear = 1'b0;
    @(posedge clk);
    for (i = 0; i < 2**FIFO_SIZE-1; i = i + 1) begin
      i_tvalid = 1'b1;
      i_tdata = i_tdata + 32'd1;
      @(posedge clk);
      i_tvalid = 1'b0;
      // Give some time to propogate
      @(posedge clk);
      @(posedge clk);
      @(posedge clk);
      if (~i_tready) begin
          $display("Test 3 FAILED!");
          $error("i_tready deasserted unexpectantly after writing %d entries!",i+1);
          $stop;
      end
      if (~o_tvalid) begin
          $display("Test 3 FAILED!");
          $error("o_tvalid deasserted unexpectantly after writing %d entries!",i+1);
          $stop;
      end
    end
    // Write final entry
    i_tvalid = 1'b1;
    i_tdata = i_tdata + 32'd1;
    @(posedge clk);
    i_tvalid = 1'b0;
    @(posedge clk);
    if (i_tready) begin
        $display("Test 3 warning!");
        $warning("i_tready still asserted after writing %d entries! Might be due to output registering.",i+1);
        //$stop;
    end
    @(posedge clk);
    for (i = 0; i < 2**FIFO_SIZE-1; i = i + 1) begin
      o_tready = 1'b1;
      @(posedge clk);
      o_tready = 1'b0;
      // Give some time to propogate
      @(posedge clk);
      @(posedge clk);
      @(posedge clk);
      if (~i_tready) begin
          $display("Test 3 FAILED!");
          $error("i_tready deasserted unexpectantly after reading %d entries!",i+1);
          $stop;
      end
      if (~o_tvalid) begin
          $display("Test 3 FAILED!");
          $error("o_tvalid deasserted unexpectantly after reading %d entries!",i+1);
          $stop;
      end
    end
    // Read final entry
    o_tready = 1'b1;
    @(posedge clk);
    o_tready = 1'b0;
    @(posedge clk);
    if (o_tvalid) begin
        $display("Test 3 FAILED!");
        $error("o_tvalid still asserted after reading %d entries!",i+1);
        $stop;
    end
    @(posedge clk);
    //////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////
    $display("Test 4 -- Check for bubble states");
    clear = 1'b1;
    i_tdata = {32{1'b1}};
    i_tvalid = 1'b0;
    o_tready = 1'b0;
    @(posedge clk);
    clear = 1'b0;
    @(posedge clk);
    // Fill up half way
    for (i = 0; i < (2**FIFO_SIZE + 1)/2; i = i + 1) begin
      i_tvalid = 1'b1;
      i_tdata = i_tdata + 32'd1;
      @(posedge clk);
    end
    // Start reading
    o_tready = 1'b1;
    i_tdata = i_tdata + 32'd1;
    @(posedge clk);
    // Give a clock cycle for latency, but no bubbles should occur after this
    i_tdata = i_tdata + 32'd1;
    @(posedge clk);
    // Continue to write and read at full rate
    for (i = 0; i < (2**FIFO_SIZE + 1)/2; i = i + 1) begin
      if (~i_tready) begin
          $display("Test 4 FAILED!");
          $error("FIFO bubble state detected when writing & reading at full rate!");
          $stop;
      end
      i_tvalid = 1'b1;
      i_tdata = i_tdata + 32'd1;
      @(posedge clk);
    end
    // Read at full, write at half rate
    for (i = 0; i < (2**FIFO_SIZE + 1)/2; i = i + 1) begin
      if (~i_tready | ~o_tvalid) begin
          $display("Test 4 FAILED!");
          $error("FIFO bubble state detected when write at half rate, reading at full rate!");
          $stop;
      end
      i_tvalid = ~i_tvalid;
      if (i_tvalid) i_tdata = i_tdata + 32'd1;
      @(posedge clk);
    end
    // Read at half rate, write at full rate
    for (i = 0; i < (2**FIFO_SIZE + 1)/2; i = i + 1) begin
      if (~i_tready | ~o_tvalid) begin
          $display("Test 4 FAILED!");
          $error("FIFO bubble state detected when write at half rate, reading at full rate!");
          $stop;
      end
      o_tready = ~o_tready;
      i_tvalid = 1'b1;
      i_tdata = i_tdata + 32'd1;
      @(posedge clk);
    end
    $display("Test 4 Passed!");
    //////////////////////////////////////////////////////////////////////////////
    // Tests combinations of i_tvalid / o_tready sequences.
    // Test space depends on TEST_VECTOR_SIZE.
    // Example: TEST_VECTOR_SIZE = 10 => 1024*1024 number of test sequences,
    //          which is every possible 10 bit sequence of i_tvalid / o_tready.
    $display("Test 5 -- Check combinations of i_tvalid / o_tready");
    clear = 1'b1;
    i_tdata = {32{1'b1}};
    i_tvalid = 1'b0;
    o_tready = 1'b0;
    i_tready_timeout = 0;
    i_tvalid_sequence = {TEST_VECTOR_SIZE{1'd0}};
    o_tready_sequence = {TEST_VECTOR_SIZE{1'd0}};
    @(posedge clk);
    clear = 1'b0;
    @(posedge clk);
    for (i = 0; i < 2**TEST_VECTOR_SIZE; i = i + 1) begin
      i_tvalid_sequence = i_tvalid_sequence + 1;
      for (k = 0; k < 2**TEST_VECTOR_SIZE; k = k + 1) begin
        o_tready_sequence = o_tready_sequence + 1;
        for (n = 0; n < TEST_VECTOR_SIZE; n = n + 1) begin
          if (o_tready_sequence[n]) begin
            o_tready = 1'b1;
          end else begin
            o_tready = 1'b0;
          end
          // Special Case: If i_tready timed out, then i_tvalid is still asserted and we cannot
          //               deassert i_tvalid until we see a corresponding i_tready. This is a basic
          //               AXI stream requirement, so we will continue to assert i_tvalid regardless
          //               of what i_tvalid_sequence would have set i_tvalid for this loop.
          if (i_tvalid_sequence[n] | (i_tready_timeout == TEST_VECTOR_SIZE)) begin
            i_tvalid = 1'b1;
            if (i_tready_timeout < TEST_VECTOR_SIZE) begin
              i_tdata = i_tdata + 32'd1;
            end
            @(posedge clk);
            i_tready_timeout = 0;
            // Wait for i_tready until timeout. Timeouts may occur when o_tready_sequence
            // has o_tready not asserted for several clock cycles.
            while(~i_tready & (i_tready_timeout < TEST_VECTOR_SIZE)) begin
              @(posedge clk)
              i_tready_timeout = i_tready_timeout + 1;
            end
          end else begin
            i_tvalid = 1'b0;
            @(posedge clk);
          end
        end
      end
      // Reset starting conditions for the test sequences
      clear = 1'b1;
      i_tdata = {32{1'b1}};
      i_tvalid = 1'b0;
      o_tready = 1'b0;
      i_tready_timeout = 0;
      @(posedge clk);
      clear = 1'b0;
      @(posedge clk);
    end
    $display("Test 5 Passed!");
    $display("All tests PASSED!");
    $stop;
  end

  // Check the input counting sequence independent of
  // i_tvalid / o_tready sequences.
  always @(posedge clk) begin
    if (reset) begin
      o_tdata_check <= 32'd0;
    end else begin
      if (clear) begin
        o_tdata_check <= 32'd0;
      end
      if (o_tready & o_tvalid) begin
        o_tdata_check <= o_tdata_check + 32'd1;
        if (o_tdata != o_tdata_check) begin
          $display("Test FAILED!");
          $error("Incorrect output!");
          $stop;
        end
      end
    end
  end

endmodule