// retroDE_ps2 — tb_gs_scanout_diag (Ch442 A+B scanout diagnostic)
//
// Integration test for the READ-ONLY A+B diagnostic that splits the single
// LPDDR_STATUS[5] scan-error bit into independently observable causes and
// captures the FIRST raw-underflow event of each video-source-enabled session.
//
// DUT-A: real gs_lpddr_scanout_lb -> real ps2_hps_bridge, read back over AXI.
//   P1 starvation: arready held low + active row 0    -> underflow, first-failure snapshot
//   P2 clear     : drop video source (enable=0)       -> valid + underflow clear
//   P3 baseline  : healthy reads, no active pixels     -> no false positive
//   P4 rrerror   : SLVERR reads, no active pixels      -> read-error WITHOUT underflow
//   + concurrent monitors (Codex review):
//       M1 fs_edge suppression : raw_uf_cond must never assert on an fs_edge_v cycle
//       M2 valid ordering      : dest snap_valid rises ONLY on the payload-capture edge
//   + P1 snapshot stability    : later misses do NOT overwrite the first snapshot
//
// DUT-B: production-like gs_lpddr_scanout_lb (V_SOURCE_START=32, V_STRETCH_15_TO_14,
//   V_LINEAR_FILTER) checked at its outputs directly:
//       nonzero V_SOURCE_START -> cold-start snapshot scan_y=32 (NOT 0)
//       cause-bit / filter-phase packing of diag_stat / diag_first
`timescale 1ns/1ps

module tb_gs_scanout_diag;
    // ---- three independent clock domains ----
    logic clk       = 1'b0; always #10  clk       = ~clk;   // bridge  (50 MHz, 20 ns)
    logic emif_clk  = 1'b0; always #2.5 emif_clk  = ~emif_clk; // EMIF  (200 MHz)
    logic video_clk = 1'b0; always #4   video_clk = ~video_clk; // video (125 MHz)
    logic reset_n = 1'b0, emif_rst_n = 1'b0;

    localparam int N_ROWS    = 8;
    localparam int ROW_BEATS = 2;
    localparam int STRIDE    = 64;   // PSMCT32: 16 px/row, 2 beats

    int errors = 0;
    task automatic chk(input string label, input logic cond);
        if (!cond) begin $error("[scanout_diag] FAIL: %s", label); errors++; end
        else         $display("[scanout_diag]  ok : %s", label);
    endtask

    // ================= DUT-A: scanout_lb + bridge =================
    logic         so_enable, frame_start, in_window;
    logic [11:0]  pixel_x, pixel_y;
    logic [7:0]   so_r, so_g, so_b;
    logic         so_line_valid, so_underflow;
    logic [31:0]  so_rd_errs;
    logic         so_diag_rderr_nz, so_diag_valid;
    logic [29:0]  so_diag_first;
    logic [6:0]   so_diag_stat;
    logic [29:0]  araddr; logic [1:0] arburst; logic [6:0] arid;
    logic [7:0]   arlen;  logic [2:0] arsize;  logic arvalid, arready;
    logic [255:0] rdata;  logic [1:0] rresp;   logic rlast, rvalid, rready;

    gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
                          .N_ROWS(N_ROWS), .PSMCT32(1'b1)) dut_scan (
        .axi_clk(emif_clk), .axi_rst_n(emif_rst_n), .enable(so_enable),
        .video_clk(video_clk), .frame_start(frame_start),
        .pixel_x(pixel_x), .pixel_y(pixel_y), .in_window(in_window),
        .r(so_r), .g(so_g), .b(so_b),
        .line_valid(so_line_valid), .underflow(so_underflow), .rd_errs(so_rd_errs),
        .diag_rderr_nz(so_diag_rderr_nz), .diag_valid(so_diag_valid),
        .diag_first(so_diag_first), .diag_stat(so_diag_stat),
        .araddr(araddr), .arburst(arburst), .arid(arid),
        .arlen(arlen), .arsize(arsize), .arvalid(arvalid), .arready(arready),
        .rdata(rdata), .rresp(rresp), .rlast(rlast), .rvalid(rvalid), .rready(rready)
    );

    // behavioral EMIF read responder (emif_clk) — normal / starve / SLVERR
    localparam logic [1:0] M_NORMAL = 2'd0, M_STARVE = 2'd1, M_ERR = 2'd2;
    logic [1:0]  resp_mode = M_NORMAL;
    logic        pend; logic [1:0] pend_resp; logic [29:0] pend_addr;
    assign arready = (resp_mode != M_STARVE) && arvalid && !pend && !rvalid;
    always_ff @(posedge emif_clk or negedge emif_rst_n) begin
        if (!emif_rst_n) begin
            pend <= 1'b0; rvalid <= 1'b0; rlast <= 1'b0; rresp <= 2'b00; rdata <= 256'd0;
        end else begin
            if (arready) begin
                pend      <= 1'b1;
                pend_addr <= araddr;
                pend_resp <= (resp_mode == M_ERR) ? 2'b10 : 2'b00;
            end
            if (pend && !rvalid) begin
                rvalid <= 1'b1; rlast <= 1'b1; rresp <= pend_resp;
                rdata  <= {8{2'b01, pend_addr}};
                pend   <= 1'b0;
            end else if (rvalid && rready) begin
                rvalid <= 1'b0; rlast <= 1'b0;
            end
        end
    end

    // bridge diag inputs, driven from scanout_lb (top wiring mirror)
    wire        scan_diag_uf_i       = so_underflow;
    wire        scan_diag_rderr_nz_i = so_diag_rderr_nz;
    wire        scan_diag_valid_i    = so_diag_valid;
    wire [29:0] scan_diag_first_i    = so_diag_first;
    wire [6:0]  scan_diag_stat_i     = so_diag_stat;

    // bridge AXI4-lite slave master signals
    logic [3:0]   s_axi_awid=0;  logic [37:0] s_axi_awaddr=0; logic [7:0] s_axi_awlen=0;
    logic [2:0]   s_axi_awsize=0; logic [1:0] s_axi_awburst=0; logic s_axi_awlock=0;
    logic [3:0]   s_axi_awcache=0; logic [2:0] s_axi_awprot=0; logic s_axi_awvalid=0, s_axi_awready;
    logic [127:0] s_axi_wdata=0; logic [15:0] s_axi_wstrb=0; logic s_axi_wlast=0, s_axi_wvalid=0, s_axi_wready;
    logic [3:0]   s_axi_bid; logic [1:0] s_axi_bresp; logic s_axi_bvalid; logic s_axi_bready=1;
    logic [3:0]   s_axi_arid=0; logic [37:0] s_axi_araddr=0; logic [7:0] s_axi_arlen=0;
    logic [2:0]   s_axi_arsize=0; logic [1:0] s_axi_arburst=0; logic s_axi_arlock=0;
    logic [3:0]   s_axi_arcache=0; logic [2:0] s_axi_arprot=0; logic s_axi_arvalid=0, s_axi_arready;
    logic [3:0]   s_axi_rid; logic [127:0] s_axi_rdata; logic [1:0] s_axi_rresp; logic s_axi_rlast, s_axi_rvalid; logic s_axi_rready=0;

    ps2_hps_bridge u_bridge (
        .clk(clk), .reset_n(reset_n),
        .s_axi_awid(s_axi_awid), .s_axi_awaddr(s_axi_awaddr), .s_axi_awlen(s_axi_awlen),
        .s_axi_awsize(s_axi_awsize), .s_axi_awburst(s_axi_awburst), .s_axi_awlock(s_axi_awlock),
        .s_axi_awcache(s_axi_awcache), .s_axi_awprot(s_axi_awprot), .s_axi_awvalid(s_axi_awvalid),
        .s_axi_awready(s_axi_awready),
        .s_axi_wdata(s_axi_wdata), .s_axi_wstrb(s_axi_wstrb), .s_axi_wlast(s_axi_wlast),
        .s_axi_wvalid(s_axi_wvalid), .s_axi_wready(s_axi_wready),
        .s_axi_bid(s_axi_bid), .s_axi_bresp(s_axi_bresp), .s_axi_bvalid(s_axi_bvalid), .s_axi_bready(s_axi_bready),
        .s_axi_arid(s_axi_arid), .s_axi_araddr(s_axi_araddr), .s_axi_arlen(s_axi_arlen),
        .s_axi_arsize(s_axi_arsize), .s_axi_arburst(s_axi_arburst), .s_axi_arlock(s_axi_arlock),
        .s_axi_arcache(s_axi_arcache), .s_axi_arprot(s_axi_arprot), .s_axi_arvalid(s_axi_arvalid),
        .s_axi_arready(s_axi_arready),
        .s_axi_rid(s_axi_rid), .s_axi_rdata(s_axi_rdata), .s_axi_rresp(s_axi_rresp),
        .s_axi_rlast(s_axi_rlast), .s_axi_rvalid(s_axi_rvalid), .s_axi_rready(s_axi_rready),
        .scan_diag_uf_i(scan_diag_uf_i), .scan_diag_rderr_nz_i(scan_diag_rderr_nz_i),
        .scan_diag_valid_i(scan_diag_valid_i), .scan_diag_first_i(scan_diag_first_i),
        .scan_diag_stat_i(scan_diag_stat_i)
    );

    // ---- concurrent monitor M1: capture must never be enabled on an fs_edge cycle ----
    logic cov_fsedge_miss = 1'b0;   // coverage: we actually hit fs_edge coincident with a would-be miss
    always @(posedge video_clk) if (emif_rst_n) begin
        if (dut_scan.enable && dut_scan.fs_edge_v && dut_scan.raw_uf_cond) begin
            $error("[scanout_diag] M1 FAIL: raw_uf_cond asserted on fs_edge_v cycle"); errors++;
        end
        if (dut_scan.enable && dut_scan.fs_edge_v && dut_scan.in_window &&
            (dut_scan.scan_y >= dut_scan.nf_v) && (dut_scan.scan_y < ($clog2(N_ROWS)+1)'(N_ROWS)))
            cov_fsedge_miss <= 1'b1;
    end

    // ---- concurrent monitor M2: dest snap_valid rises ONLY on the payload-capture edge ----
    // snap_valid_q is registered, so its observed rise (T+1) reflects the capture condition
    // sampled on the PREVIOUS edge (T). Compare against that delayed condition, else the
    // check mis-times the legitimate capture. Proves 0x120[0] never leads payload capture.
    logic snapv_prev = 1'b0, cap_edge_prev = 1'b0;
    always @(posedge clk) if (reset_n) begin
        if (u_bridge.scan_diag_snap_valid_q && !snapv_prev) begin  // rising edge of exposed 0x120[0]
            if (!cap_edge_prev) begin
                $error("[scanout_diag] M2 FAIL: snap_valid rose without the payload-capture edge"); errors++;
            end
        end
        snapv_prev    <= u_bridge.scan_diag_snap_valid_q;
        cap_edge_prev <= u_bridge.scan_diag_valid_sync[1] && !u_bridge.scan_diag_valid_sync[2];
    end

    // ---- AXI read (single-beat, lane by addr[3:2]) ----
    task automatic axi_read32(input logic [37:0] addr, output logic [31:0] data);
        @(posedge clk);
        s_axi_arid<=0; s_axi_araddr<=addr; s_axi_arlen<=0; s_axi_arsize<=3'd2;
        s_axi_arburst<=2'b01; s_axi_arvalid<=1'b1; s_axi_rready<=1'b1;
        wait (s_axi_arready); @(posedge clk); s_axi_arvalid<=1'b0;
        wait (s_axi_rvalid);
        case (addr[3:2])
            2'b00:   data = s_axi_rdata[31:0];
            2'b01:   data = s_axi_rdata[63:32];
            2'b10:   data = s_axi_rdata[95:64];
            default: data = s_axi_rdata[127:96];
        endcase
        @(posedge clk); s_axi_rready<=1'b0;
    endtask

    task automatic sweep_rows(input int hold_cycles);
        for (int y = 0; y < N_ROWS; y++) begin
            @(posedge video_clk); pixel_y <= y[11:0]; pixel_x <= 12'd0;
            repeat (hold_cycles) @(posedge video_clk);
        end
    endtask
    task automatic pulse_frame_start();
        @(posedge video_clk); frame_start <= 1'b1;
        repeat (3) @(posedge video_clk); frame_start <= 1'b0;
        repeat (3) @(posedge video_clk);
    endtask

    // ================= DUT-B: production-like (V_SOURCE_START=32, stretch, linear) =================
    localparam int NB = 48, VSS = 32;
    logic         b_enable, b_frame_start, b_in_window;
    logic [11:0]  b_px, b_py;
    logic [7:0]   b_r, b_g, b_bb;
    logic         b_line_valid, b_underflow; logic [31:0] b_rd_errs;
    logic         b_diag_rderr_nz, b_diag_valid;
    logic [29:0]  b_diag_first; logic [6:0] b_diag_stat;
    logic [29:0]  b_araddr; logic [1:0] b_arburst; logic [6:0] b_arid;
    logic [7:0]   b_arlen; logic [2:0] b_arsize; logic b_arvalid;
    gs_lpddr_scanout_lb #(.FB_BASE(30'd0), .STRIDE_BYTES(STRIDE), .ROW_BEATS(ROW_BEATS),
                          .N_ROWS(NB), .PSMCT32(1'b1),
                          .V_SOURCE_START(VSS), .V_STRETCH_15_TO_14(1'b1),
                          .V_LINEAR_FILTER(1'b1)) dut_b (
        .axi_clk(emif_clk), .axi_rst_n(emif_rst_n), .enable(b_enable),
        .video_clk(video_clk), .frame_start(b_frame_start),
        .pixel_x(b_px), .pixel_y(b_py), .in_window(b_in_window),
        .r(b_r), .g(b_g), .b(b_bb),
        .line_valid(b_line_valid), .underflow(b_underflow), .rd_errs(b_rd_errs),
        .diag_rderr_nz(b_diag_rderr_nz), .diag_valid(b_diag_valid),
        .diag_first(b_diag_first), .diag_stat(b_diag_stat),
        .araddr(b_araddr), .arburst(b_arburst), .arid(b_arid),
        .arlen(b_arlen), .arsize(b_arsize), .arvalid(b_arvalid), .arready(1'b0), // permanently starved
        .rdata(256'd0), .rresp(2'b00), .rlast(1'b0), .rvalid(1'b0), .rready()
    );

    logic [31:0] st, fst;
    initial begin
        so_enable=0; frame_start=0; in_window=0; pixel_x=0; pixel_y=0; resp_mode=M_NORMAL;
        b_enable=0; b_frame_start=0; b_in_window=0; b_px=0; b_py=0;
        repeat (6) @(posedge clk); reset_n=1; emif_rst_n=1;
        repeat (6) @(posedge clk);

        // -------- P1: cold-start row-zero starvation -> first-failure snapshot --------
        resp_mode=M_STARVE; so_enable=1;
        pulse_frame_start();
        @(posedge video_clk) begin in_window<=1'b1; pixel_x<=12'd0; pixel_y<=12'd0; end
        repeat (60) @(posedge video_clk);
        repeat (60) @(posedge clk);
        axi_read32(38'h120, st);
        axi_read32(38'h124, fst);
        chk("P1 valid=1",         st[0]===1'b1);
        chk("P1 underflow=1",     st[1]===1'b1);
        chk("P1 rderr_nz=0",      st[2]===1'b0);
        chk("P1 cause_base=1",    st[3]===1'b1);
        chk("P1 cause_lookah=0",  st[4]===1'b0);
        chk("P1 line_valid=0",    st[5]===1'b0);
        chk("P1 snap scan_y=0",   fst[9:0]===10'd0);
        chk("P1 snap nf_v=0",     fst[19:10]===10'd0);
        chk("P1 snap pmax qualified (>=QUAL)", fst[29:20] >= 10'd4);

        // snapshot stability: later misses on a different row must NOT overwrite the first snapshot
        @(posedge video_clk) begin pixel_y<=12'd2; end
        repeat (40) @(posedge video_clk);
        repeat (20) @(posedge clk);
        axi_read32(38'h124, fst);
        chk("P1 snapshot stable (scan_y still 0)", fst[9:0]===10'd0);
        // fs_edge coincident with an active miss: pulse frame_start while in_window+miss hold.
        // Monitor M1 asserts capture stays suppressed on the fs_edge cycle.
        @(posedge video_clk) begin pixel_y<=12'd0; end
        pulse_frame_start();
        repeat (10) @(posedge video_clk);

        // -------- P2: clear via video-source disable --------
        so_enable=0; in_window=0;
        repeat (40) @(posedge clk);
        axi_read32(38'h120, st);
        chk("P2 valid cleared",     st[0]===1'b0);
        chk("P2 underflow cleared", st[1]===1'b0);

        // -------- P3: healthy baseline (rows load, no active pixels) -> no false positive --
        resp_mode=M_NORMAL; so_enable=1; in_window=0;
        pulse_frame_start(); sweep_rows(24);
        repeat (40) @(posedge clk);
        axi_read32(38'h120, st);
        chk("P3 valid=0",      st[0]===1'b0);
        chk("P3 underflow=0",  st[1]===1'b0);
        chk("P3 rderr_nz=0",   st[2]===1'b0);

        // -------- P4: SLVERR reads, no active pixels -> read-error WITHOUT underflow --
        resp_mode=M_ERR; in_window=0;
        pulse_frame_start(); sweep_rows(24);
        repeat (60) @(posedge clk);
        axi_read32(38'h120, st);
        chk("P4 rderr_nz=1",   st[2]===1'b1);
        chk("P4 valid=0",      st[0]===1'b0);
        chk("P4 underflow=0",  st[1]===1'b0);

        // -------- P5 (DUT-B): nonzero V_SOURCE_START cold-start + packing --------
        b_enable=1;
        @(posedge video_clk) b_frame_start<=1'b1;
        repeat (3) @(posedge video_clk) b_frame_start<=1'b0;
        repeat (3) @(posedge video_clk);
        @(posedge video_clk) begin b_in_window<=1'b1; b_px<=12'd0; b_py<=12'd0; end
        repeat (80) @(posedge video_clk);
        chk("P5 DUT-B captured",        b_diag_valid===1'b1);
        chk("P5 scan_y=VSS(32)",        b_diag_first[9:0]===10'd32);   // NOT 0: source row 32
        chk("P5 nf_v=32",               b_diag_first[19:10]===10'd32);
        chk("P5 pmax qualified (>=QUAL)",   b_diag_first[29:20] >= 10'd4);
        chk("P5 base=1",                b_diag_stat[0]===1'b1);
        chk("P5 line_valid=0",          b_diag_stat[2]===1'b0);
        // cause/phase packing: outputs must byte-map the internal capture regs
        chk("P5 pack base bit",     b_diag_stat[0]   === dut_b.diag_base_q);
        chk("P5 pack lookahead bit",b_diag_stat[1]   === dut_b.diag_look_q);
        chk("P5 pack line_valid bit",b_diag_stat[2]  === dut_b.diag_lv_q);
        chk("P5 pack vphase field", b_diag_stat[6:3] === dut_b.diag_vphase_q);
        chk("P5 pack scan_y field", b_diag_first[9:0]   === 10'(dut_b.diag_scan_y_q));
        chk("P5 pack nf_v field",   b_diag_first[19:10] === 10'(dut_b.diag_nf_v_q));
        chk("P5 pack pmax field",   b_diag_first[29:20] === 10'(dut_b.diag_pmax_q));

        // -------- coverage: the fs_edge-coincident-miss case was actually exercised --------
        chk("M1 coverage: fs_edge coincident with miss observed", cov_fsedge_miss===1'b1);

        if (errors==0) $display("[tb_gs_scanout_diag] PASS");
        else           $display("[tb_gs_scanout_diag] FAIL (%0d errors)", errors);
        $finish;
    end

    initial begin #800000; $error("[tb_gs_scanout_diag] TIMEOUT"); $finish; end
endmodule : tb_gs_scanout_diag
