`timescale 1ns / 1ps

module tb_uart_receiver;
    reg clk;
    reg rst;
    reg rx;

    wire [7:0] data_out;
    wire data_valid;
    wire framing_error;

    integer errors = 0;
    integer tests_run = 0;
    integer cycle_count = 0;

    localparam integer MAX_EVENTS = 16;
    localparam [0:0] EVENT_VALID = 1'b0;
    localparam [0:0] EVENT_ERROR = 1'b1;
    localparam integer FRAME_EVENT_OFFSET = 153;

    integer expected_cycle [0:MAX_EVENTS-1];
    reg expected_kind [0:MAX_EVENTS-1];
    reg [7:0] expected_data [0:MAX_EVENTS-1];
    reg [255:0] expected_tag [0:MAX_EVENTS-1];
    integer event_count = 0;
    integer event_index = 0;

    uart_receiver uut (
        .clk(clk),
        .rst(rst),
        .rx(rx),
        .data_out(data_out),
        .data_valid(data_valid),
        .framing_error(framing_error)
    );

    initial begin
        clk = 1'b0;
        forever #5 clk = ~clk;
    end

    task expect_valid_after_start;
        input [255:0] tag;
        input [7:0] expected_byte;
        begin
            if (event_count >= MAX_EVENTS) begin
                $display("ERROR: expected event queue overflow");
                errors = errors + 1;
            end else begin
                expected_cycle[event_count] = cycle_count + FRAME_EVENT_OFFSET;
                expected_kind[event_count] = EVENT_VALID;
                expected_data[event_count] = expected_byte;
                expected_tag[event_count] = tag;
                event_count = event_count + 1;
            end
        end
    endtask

    task expect_error_after_start;
        input [255:0] tag;
        begin
            if (event_count >= MAX_EVENTS) begin
                $display("ERROR: expected event queue overflow");
                errors = errors + 1;
            end else begin
                expected_cycle[event_count] = cycle_count + FRAME_EVENT_OFFSET;
                expected_kind[event_count] = EVENT_ERROR;
                expected_data[event_count] = 8'h00;
                expected_tag[event_count] = tag;
                event_count = event_count + 1;
            end
        end
    endtask

    task check_outputs;
        reg expect_valid;
        reg expect_error;
        reg [7:0] expect_data;
        reg [255:0] current_tag;
        begin
            expect_valid = 1'b0;
            expect_error = 1'b0;
            expect_data = 8'h00;
            current_tag = "idle";

            if (event_index < event_count && expected_cycle[event_index] == cycle_count) begin
                current_tag = expected_tag[event_index];
                if (expected_kind[event_index] == EVENT_VALID) begin
                    expect_valid = 1'b1;
                    expect_data = expected_data[event_index];
                end else begin
                    expect_error = 1'b1;
                end
                event_index = event_index + 1;
            end

            if (data_valid !== expect_valid) begin
                $display("ERROR [%s cycle %0d]: data_valid mismatch. expected=%b got=%b",
                         current_tag, cycle_count, expect_valid, data_valid);
                errors = errors + 1;
            end

            if (framing_error !== expect_error) begin
                $display("ERROR [%s cycle %0d]: framing_error mismatch. expected=%b got=%b",
                         current_tag, cycle_count, expect_error, framing_error);
                errors = errors + 1;
            end

            if (data_valid && framing_error) begin
                $display("ERROR [cycle %0d]: data_valid and framing_error asserted together", cycle_count);
                errors = errors + 1;
            end

            if (expect_valid && (data_out !== expect_data)) begin
                $display("ERROR [%s cycle %0d]: data_out mismatch. expected=0x%02h got=0x%02h",
                         current_tag, cycle_count, expect_data, data_out);
                errors = errors + 1;
            end
        end
    endtask

    task advance_cycle;
        begin
            @(posedge clk);
            #1;
            cycle_count = cycle_count + 1;
            tests_run = tests_run + 1;
            check_outputs;
        end
    endtask

    task drive_level_cycles;
        input level;
        input integer cycles;
        integer i;
        begin
            rx = level;
            for (i = 0; i < cycles; i = i + 1)
                advance_cycle;
        end
    endtask

    task send_valid_frame;
        input [7:0] byte_value;
        input [255:0] tag;
        integer bit_idx;
        begin
            expect_valid_after_start(tag, byte_value);

            drive_level_cycles(1'b0, 16);
            for (bit_idx = 0; bit_idx < 8; bit_idx = bit_idx + 1)
                drive_level_cycles(byte_value[bit_idx], 16);
            drive_level_cycles(1'b1, 16);
        end
    endtask

    task send_framing_error_frame;
        input [7:0] byte_value;
        input [255:0] tag;
        integer bit_idx;
        begin
            expect_error_after_start(tag);

            drive_level_cycles(1'b0, 16);
            for (bit_idx = 0; bit_idx < 8; bit_idx = bit_idx + 1)
                drive_level_cycles(byte_value[bit_idx], 16);
            drive_level_cycles(1'b0, 16);
        end
    endtask

    initial begin
        rst = 1'b0;
        rx = 1'b1;

        $display("===========================================");
        $display("           UART Receiver Testbench");
        $display("===========================================");

        // Reset and idle behavior.
        rst = 1'b1;
        drive_level_cycles(1'b1, 2);
        rst = 1'b0;
        drive_level_cycles(1'b1, 2);

        // False start: low pulse disappears before the midpoint sample.
        drive_level_cycles(1'b0, 4);
        drive_level_cycles(1'b1, 20);

        // Directed valid frames.
        send_valid_frame(8'h55, "valid_55");
        drive_level_cycles(1'b1, 8);

        send_valid_frame(8'h00, "valid_00");
        send_valid_frame(8'hFF, "valid_ff");
        send_valid_frame(8'hA3, "valid_a3");

        // Back-to-back frames with no extra idle gap beyond the stop bit.
        send_valid_frame(8'h3C, "back_to_back_3c");
        send_valid_frame(8'hC7, "back_to_back_c7");
        drive_level_cycles(1'b1, 16);

        // Stop-bit failure.
        send_framing_error_frame(8'h96, "framing_error");
        drive_level_cycles(1'b1, 16);

        // Mid-frame reset should abandon the partial reception.
        drive_level_cycles(1'b0, 16);
        drive_level_cycles(1'b1, 16);
        drive_level_cycles(1'b0, 16);
        drive_level_cycles(1'b1, 16);
        drive_level_cycles(1'b0, 16);
        rst = 1'b1;
        drive_level_cycles(1'b1, 1);
        rst = 1'b0;
        drive_level_cycles(1'b1, 40);

        if (event_index !== event_count) begin
            $display("ERROR: %0d expected event(s) were never observed", event_count - event_index);
            errors = errors + 1;
        end

        $display("");
        $display("===========================================");
        $display("  Tests Run: %0d", tests_run);
        $display("===========================================");

        if (errors == 0) begin
            $display("TEST_RESULT: PASS");
        end else begin
            $display("TEST_RESULT: FAIL (%0d errors)", errors);
        end

        $finish;
    end
endmodule
