// retroDE_ps2 — tb_sio2_input_stub (Ch234)
// ============================================================================
// Focused unit-test TB for sio2_input_stub. Instantiates the stub
// directly (no IOP map) and drives synthetic input_p1/input_p2 bitmaps,
// then reads back PAD_P1_STATE / PAD_P2_STATE / PAD_STATUS through the
// stub's IOP-map-style read port.
//
// Verifies:
//   1. Reset: stub responds 32'd0 for any read.
//   2. No buttons (INPUT_P1 = INPUT_P2 = 0)        → Sony word = 0xFFFF.
//   3. Single buttons (each of the 16 retroDE bits) map to the right
//      Sony pad bit (active-low: pressed = bit cleared).
//   4. Combos (multiple buttons)                   → combined active-low
//      pattern.
//   5. P1 and P2 are independent (writing P1 doesn't change P2 readback
//      and vice versa).
//   6. PAD_STATUS reads `32'h0000_0001` (bit 0 = present/valid).
//   7. Writes accepted-and-ignored (no mutation of any state).
//   8. Out-of-range word offsets inside the region read 0.
//
// CDC note: this TB drives a single clock, so the stub's 2-FF
// synchronizer is functionally a 2-cycle delay. The TB inserts
// `repeat (3) @(posedge clk)` after every INPUT_P1/P2 change to let
// the sync settle + the read pipeline produce valid data.
// ============================================================================

`timescale 1ns/1ps

module tb_sio2_input_stub;

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

    logic [31:0] input_p1;
    logic [31:0] input_p2;

    logic        rd_en;
    logic [3:0]  rd_addr;
    wire  [31:0] rd_data;
    wire         rd_valid;

    logic        wr_en;
    logic [3:0]  wr_addr;
    logic [31:0] wr_data;

    sio2_input_stub u_dut (
        .clk      (clk),
        .rst_n    (rst_n),
        .input_p1 (input_p1),
        .input_p2 (input_p2),
        .rd_en    (rd_en),
        .rd_addr  (rd_addr),
        .rd_data  (rd_data),
        .rd_valid (rd_valid),
        .wr_en    (wr_en),
        .wr_addr  (wr_addr),
        .wr_data  (wr_data)
    );

    int errors;

    task automatic check_eq(input string label,
                            input logic [31:0] got,
                            input logic [31:0] expected);
        if (got !== expected) begin
            $error("[%s] got 0x%08x expected 0x%08x", label, got, expected);
            errors = errors + 1;
        end
    endtask

    // Issue a read at the given word offset and sample rd_data the
    // cycle rd_valid asserts.
    task automatic do_read(input logic [3:0] addr, output logic [31:0] data);
        @(posedge clk);
        rd_en   <= 1'b1;
        rd_addr <= addr;
        @(posedge clk);
        rd_en   <= 1'b0;
        wait (rd_valid);
        data = rd_data;
        @(posedge clk);
    endtask

    // Apply new pad state and let the 2-FF sync settle.
    task automatic apply_pads(input logic [31:0] new_p1, input logic [31:0] new_p2);
        @(posedge clk);
        input_p1 <= new_p1;
        input_p2 <= new_p2;
        repeat (4) @(posedge clk);  // 2-FF sync + slack
    endtask

    // Sony pad-word expectation helper. Mirrors the stub's sony_word()
    // function — bit-for-bit copy from `docs/contracts/sio2_pad.md`.
    function automatic logic [15:0] expected_sony(input logic [31:0] joy);
        logic [7:0] b3, b4;
        b3 = ~{joy[1], joy[2], joy[0], joy[3], joy[4], joy[15], joy[14], joy[5]};
        b4 = ~{joy[8], joy[7], joy[9], joy[6], joy[11], joy[10], joy[13], joy[12]};
        expected_sony = {b4, b3};
    endfunction

    logic [31:0] rd;
    logic [31:0] pat;

    initial begin
        errors    = 0;
        rst_n     = 1'b0;
        rd_en     = 1'b0;
        rd_addr   = 4'd0;
        wr_en     = 1'b0;
        wr_addr   = 4'd0;
        wr_data   = 32'd0;
        input_p1  = 32'd0;
        input_p2  = 32'd0;

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

        // ----------------------------------------------------------
        // §1. Reset state — all pad bits LOW → Sony word 0xFFFF.
        //     STATUS = 1.
        // ----------------------------------------------------------
        do_read(4'h0, rd); check_eq("rst_P1_word",   rd, 32'h0000_FFFF);
        do_read(4'h1, rd); check_eq("rst_P2_word",   rd, 32'h0000_FFFF);
        do_read(4'h2, rd); check_eq("rst_STATUS",    rd, 32'h0000_0001);

        // Out-of-range reads inside the 4-bit addr field → 0.
        do_read(4'h3, rd); check_eq("rst_oob_3",     rd, 32'd0);
        do_read(4'hF, rd); check_eq("rst_oob_F",     rd, 32'd0);

        // ----------------------------------------------------------
        // §2. Single-button mapping — for each of the 16 retroDE
        //     bits, press only that bit and verify the Sony word
        //     has exactly the corresponding bit cleared.
        // ----------------------------------------------------------
        for (int i = 0; i < 16; i = i + 1) begin
            pat = (32'd1 << i);
            apply_pads(pat, 32'd0);
            do_read(4'h0, rd);
            check_eq($sformatf("single_bit_%0d_P1", i),
                     rd, {16'd0, expected_sony(pat)});
            do_read(4'h1, rd);
            check_eq($sformatf("single_bit_%0d_P2_unaffected", i),
                     rd, 32'h0000_FFFF);
        end

        // ----------------------------------------------------------
        // §3. JOY_OSD (bit 16) is intentionally NOT forwarded —
        //     retrodesd consumes it before the bridge. Pressing it
        //     should leave the Sony word at 0xFFFF.
        // ----------------------------------------------------------
        apply_pads(32'h0001_0000, 32'd0);
        do_read(4'h0, rd); check_eq("OSD_bit_not_forwarded",
                                    rd, 32'h0000_FFFF);

        // ----------------------------------------------------------
        // §4. Combos. The classic "Konami code" final two — Start +
        //     Select pressed together → byte3 bits 0 and 3 both
        //     cleared, all other bits HIGH (released).
        // ----------------------------------------------------------
        pat = (32'd1 << 4) | (32'd1 << 5);  // START | SELECT
        apply_pads(pat, 32'd0);
        do_read(4'h0, rd);
        check_eq("combo_START_SELECT", rd, {16'd0, expected_sony(pat)});

        // All face + D-pad pressed at once. Both bytes have visible
        // clears.
        pat = (32'd1 << 6) | (32'd1 << 7) | (32'd1 << 8) | (32'd1 << 9)
            | (32'd1 << 0) | (32'd1 << 1) | (32'd1 << 2) | (32'd1 << 3);
        apply_pads(pat, 32'd0);
        do_read(4'h0, rd);
        check_eq("combo_face_plus_dpad", rd, {16'd0, expected_sony(pat)});

        // ----------------------------------------------------------
        // §5. P1/P2 independence. Different patterns on each
        //     channel, verify reads track each port independently.
        // ----------------------------------------------------------
        apply_pads(32'hAAAA_5555, 32'h5555_AAAA);
        do_read(4'h0, rd);
        check_eq("indep_P1_word",
                 rd, {16'd0, expected_sony(32'hAAAA_5555)});
        do_read(4'h1, rd);
        check_eq("indep_P2_word",
                 rd, {16'd0, expected_sony(32'h5555_AAAA)});

        // ----------------------------------------------------------
        // §6. Writes accepted-and-ignored. Drive write strobes at
        //     each of the three mapped addresses, then verify the
        //     readback values are unchanged.
        // ----------------------------------------------------------
        @(posedge clk);
        wr_en   <= 1'b1;
        wr_addr <= 4'h0;
        wr_data <= 32'hF00DBABE;
        @(posedge clk);
        wr_addr <= 4'h1;
        wr_data <= 32'hCAFEF00D;
        @(posedge clk);
        wr_addr <= 4'h2;
        wr_data <= 32'h12345678;
        @(posedge clk);
        wr_en   <= 1'b0;
        repeat (2) @(posedge clk);

        do_read(4'h0, rd);
        check_eq("wr_ignored_P1", rd, {16'd0, expected_sony(32'hAAAA_5555)});
        do_read(4'h1, rd);
        check_eq("wr_ignored_P2", rd, {16'd0, expected_sony(32'h5555_AAAA)});
        do_read(4'h2, rd);
        check_eq("wr_ignored_STATUS", rd, 32'h0000_0001);

        // ----------------------------------------------------------
        // §7. Clearing both pads returns to all-released = 0xFFFF.
        // ----------------------------------------------------------
        apply_pads(32'd0, 32'd0);
        do_read(4'h0, rd); check_eq("clr_P1", rd, 32'h0000_FFFF);
        do_read(4'h1, rd); check_eq("clr_P2", rd, 32'h0000_FFFF);
        do_read(4'h2, rd); check_eq("clr_STATUS", rd, 32'h0000_0001);

        // ----------------------------------------------------------
        // Done.
        // ----------------------------------------------------------
        $display("[tb_sio2_input_stub] errors=%0d", errors);
        if (errors == 0) $display("[tb_sio2_input_stub] PASS");
        else             $display("[tb_sio2_input_stub] FAIL");
        $finish;
    end

    initial begin
        #5_000_000;
        $error("[tb_sio2_input_stub] TIMEOUT");
        $finish;
    end

endmodule : tb_sio2_input_stub
