// retroDE_ps2 — tb_vram_stub (Ch89 audit-medium)
//
// Focused unit TB for vram_stub bounds-check correctness. The
// Ch89 audit flagged the original `(addr + 32'd3) < BYTES` guard:
// a 32-bit addr near 0xFFFF_FFFF wraps on the +3 and the
// comparison passes, indexing out of range. The fix uses
// `addr <= MAX_BASE` where MAX_BASE = BYTES - 4 (a localparam).
// This TB exercises the boundary and a high-address wrap case.
//
// Scenarios:
//   1. Write at addr 0 → read back matches.
//   2. Write at addr MAX_BASE = BYTES-4 → read back matches.
//   3. Write at addr MAX_BASE+1 → rejected; read at that addr
//      returns zero.
//   4. Write at addr 0xFFFF_FFFE (near 32-bit wrap; +3 wraps to
//      0x0000_0001, which would have falsely passed the old
//      `(addr+3) < BYTES` check) → rejected; read at addr 0
//      stays zero (proving we did NOT alias-write into mem[0]).

`timescale 1ns/1ps

module tb_vram_stub;

    localparam int unsigned BYTES = 64;

    logic clk;
    logic rst_n;
    initial clk = 1'b0;
    always #5 clk = ~clk;

    logic        write_en;
    logic [31:0] write_addr;
    logic [31:0] write_data;
    logic [3:0]  write_be;
    logic [31:0] read_addr;
    logic [31:0] read_data;

    vram_stub #(.BYTES(BYTES)) u_vram (
        .clk(clk), .rst_n(rst_n),
        .write_en(write_en),
        .write_addr(write_addr),
        .write_data(write_data),
        .write_be(write_be),

        .write_mask(32'hFFFF_FFFF),
        .read_addr(read_addr),
        .read_data(read_data),
        .read2_addr(32'd0),
        .read2_data()
    );

    int errors;

    task automatic do_write(input logic [31:0] addr, input logic [31:0] data);
        @(negedge clk);
        write_en   = 1'b1;
        write_addr = addr;
        write_data = data;
        write_be   = 4'b1111;
        @(posedge clk);
        @(negedge clk);
        write_en   = 1'b0;
        write_addr = 32'd0;
        write_data = 32'd0;
        write_be   = 4'b0000;
    endtask

    // Halfword (PSMCT16-style) write: only the low 2 bytes of the
    // 32-bit slot starting at `addr` are committed.
    task automatic do_write_hw(input logic [31:0] addr, input logic [15:0] data);
        @(negedge clk);
        write_en   = 1'b1;
        write_addr = addr;
        write_data = {16'd0, data};
        write_be   = 4'b0011;
        @(posedge clk);
        @(negedge clk);
        write_en   = 1'b0;
        write_addr = 32'd0;
        write_data = 32'd0;
        write_be   = 4'b0000;
    endtask

    task automatic check_read_hw(input logic [31:0] addr,
                                 input logic [15:0] expected,
                                 input string       tag);
        // Use the existing 4-byte read at a 4-byte-aligned base
        // and mux the right halfword.
        logic [15:0] got;
        read_addr = addr & ~32'd3;
        #1;
        got = addr[1] ? read_data[31:16] : read_data[15:0];
        if (got !== expected) begin
            $error("[%s] hw read @0x%08x got 0x%04x expected 0x%04x",
                   tag, addr, got, expected);
            errors = errors + 1;
        end else begin
            $display("[tb_vram_stub] %s hw read @0x%08x = 0x%04x OK", tag, addr, got);
        end
    endtask

    task automatic check_read(input logic [31:0] addr,
                              input logic [31:0] expected,
                              input string       tag);
        read_addr = addr;
        #1;
        if (read_data !== expected) begin
            $error("[%s] read @0x%08x got 0x%08x expected 0x%08x",
                   tag, addr, read_data, expected);
            errors = errors + 1;
        end else begin
            $display("[tb_vram_stub] %s read @0x%08x = 0x%08x OK", tag, addr, read_data);
        end
    endtask

    initial begin
        rst_n      = 1'b0;
        write_en   = 1'b0;
        write_be   = 4'b0000;
        write_addr = 32'd0;
        write_data = 32'd0;
        read_addr  = 32'd0;
        errors     = 0;

        repeat (4) @(posedge clk);
        rst_n = 1'b1;
        repeat (2) @(posedge clk);

        // 1. In-range write at 0.
        do_write(32'd0, 32'hDEAD_BEEF);
        check_read(32'd0, 32'hDEAD_BEEF, "in-range-low");

        // 2. In-range write at MAX_BASE = BYTES-4 (boundary OK).
        do_write(32'(BYTES - 4), 32'hCAFE_F00D);
        check_read(32'(BYTES - 4), 32'hCAFE_F00D, "in-range-boundary");

        // 3. Out-of-range write at MAX_BASE+1: +3 stays in 32-bit
        //    space (no wrap), but addr > MAX_BASE → reject.
        do_write(32'(BYTES - 3), 32'h1111_2222);
        check_read(32'(BYTES - 3), 32'd0, "oor-just-past-boundary");

        // 4. High-address wrap case (the Ch89 audit-medium fix
        //    target): addr = 0xFFFF_FFFE; addr+3 wraps to
        //    0x0000_0001 which is < BYTES, so the OLD guard would
        //    have allowed indexing. We assert the write is
        //    rejected (no aliasing into mem[0]) AND that the
        //    high-addr read returns zero.
        do_write(32'hFFFF_FFFE, 32'h5555_AAAA);
        check_read(32'hFFFF_FFFE, 32'd0, "oor-32-bit-wrap-read");
        // mem[0] should still be the value from step 1.
        check_read(32'd0, 32'hDEAD_BEEF, "no-alias-into-low-addr");

        // 5. Ch95 audit-medium target — halfword write at
        //    BYTES-2 must be ACCEPTED (both bytes in range), and
        //    halfword write at BYTES-1 must be REJECTED (byte 1
        //    of the slot lands at BYTES, OOB).
        //    The pre-fix admission gate was the 4-byte
        //    `addr <= MAX_BASE` (= BYTES-4), which would have
        //    silently dropped the BYTES-2 halfword too.
        //    After step 2 the slot at BYTES-4 holds 0xCAFE_F00D
        //    (mem[60..63] = {0x0D, 0xF0, 0xFE, 0xCA}). After
        //    the BYTES-2 halfword write of 0x7788, mem[62..63]
        //    becomes {0x88, 0x77}, so the slot reads back as
        //    0x7788_F00D. The rejected write at BYTES-1 must
        //    leave that unchanged.
        do_write_hw(32'(BYTES - 2), 16'h7788);
        check_read(32'(BYTES - 4), 32'h7788_F00D, "hw-in-range-boundary-slot");
        do_write_hw(32'(BYTES - 1), 16'h99AA);
        check_read(32'(BYTES - 4), 32'h7788_F00D, "hw-oor-leaves-slot-untouched");

        if (errors == 0) $display("[tb_vram_stub] PASS");
        else             $display("[tb_vram_stub] FAIL");
        $finish;
    end

    initial begin
        #1000000;
        $error("[tb_vram_stub] timeout");
        $finish;
    end

endmodule : tb_vram_stub
