`timescale 1ns / 1ps

module tb_pwm_deadtime;
    reg clk;
    reg rst;
    reg enable;
    reg [7:0] period;
    reg [7:0] duty;
    reg [3:0] dead_time;
    wire pwm_high;
    wire pwm_low;

    integer errors = 0;
    integer tests_run = 0;
    integer i;
    integer mode_sel;
    integer low_margin;
    integer high_margin;
    integer chosen_dead;
    integer chosen_period;
    integer chosen_duty;
    reg random_rst;
    reg random_enable;

    reg [7:0] golden_counter;
    reg golden_raw_valid;
    reg golden_raw_state;
    reg golden_pending_raw;
    reg golden_dead_active;
    reg [3:0] golden_dead_count;

    pwm_deadtime uut (
        .clk(clk),
        .rst(rst),
        .enable(enable),
        .period(period),
        .duty(duty),
        .dead_time(dead_time),
        .pwm_high(pwm_high),
        .pwm_low(pwm_low)
    );

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

    function raw_high;
        input [7:0] counter_value;
        input [7:0] period_value;
        input [7:0] duty_value;
        begin
            raw_high = (duty_value != 8'd0) &&
                       ((duty_value >= period_value) || (counter_value < duty_value));
        end
    endfunction

    task clear_golden;
        begin
            golden_counter = 8'd0;
            golden_raw_valid = 1'b0;
            golden_raw_state = 1'b0;
            golden_pending_raw = 1'b0;
            golden_dead_active = 1'b0;
            golden_dead_count = 4'd0;
        end
    endtask

    task golden_step;
        input next_rst;
        input next_enable;
        input [7:0] next_period;
        input [7:0] next_duty;
        input [3:0] next_dead_time;
        output exp_pwm_high;
        output exp_pwm_low;
        reg current_raw;
        begin
            if (next_rst || !next_enable) begin
                clear_golden;
                exp_pwm_high = 1'b0;
                exp_pwm_low = 1'b0;
            end else begin
                current_raw = raw_high(golden_counter, next_period, next_duty);

                if (!golden_raw_valid) begin
                    golden_raw_valid = 1'b1;
                    golden_raw_state = current_raw;
                    golden_pending_raw = current_raw;
                    golden_dead_active = 1'b0;
                    golden_dead_count = 4'd0;
                    exp_pwm_high = current_raw;
                    exp_pwm_low = ~current_raw;
                end else if (current_raw != golden_raw_state) begin
                    golden_raw_state = current_raw;
                    golden_pending_raw = current_raw;
                    if (next_dead_time != 4'd0) begin
                        golden_dead_active = 1'b1;
                        golden_dead_count = next_dead_time - 4'd1;
                        exp_pwm_high = 1'b0;
                        exp_pwm_low = 1'b0;
                    end else begin
                        golden_dead_active = 1'b0;
                        golden_dead_count = 4'd0;
                        exp_pwm_high = current_raw;
                        exp_pwm_low = ~current_raw;
                    end
                end else if (golden_dead_active) begin
                    if (golden_dead_count != 4'd0) begin
                        golden_dead_count = golden_dead_count - 4'd1;
                        exp_pwm_high = 1'b0;
                        exp_pwm_low = 1'b0;
                    end else begin
                        golden_dead_active = 1'b0;
                        exp_pwm_high = golden_pending_raw;
                        exp_pwm_low = ~golden_pending_raw;
                    end
                end else begin
                    exp_pwm_high = current_raw;
                    exp_pwm_low = ~current_raw;
                end

                if (golden_counter == next_period - 8'd1)
                    golden_counter = 8'd0;
                else
                    golden_counter = golden_counter + 8'd1;
            end
        end
    endtask

    task apply_cycle;
        input [255:0] tag;
        input next_rst;
        input next_enable;
        input [7:0] next_period;
        input [7:0] next_duty;
        input [3:0] next_dead_time;
        reg exp_pwm_high;
        reg exp_pwm_low;
        begin
            golden_step(next_rst, next_enable, next_period, next_duty, next_dead_time,
                        exp_pwm_high, exp_pwm_low);

            rst = next_rst;
            enable = next_enable;
            period = next_period;
            duty = next_duty;
            dead_time = next_dead_time;

            @(posedge clk);
            #1;

            tests_run = tests_run + 1;

            if (pwm_high !== exp_pwm_high) begin
                $display("ERROR [%s]: pwm_high mismatch. Expected %b, got %b (period=%0d duty=%0d dead_time=%0d)",
                         tag, exp_pwm_high, pwm_high, next_period, next_duty, next_dead_time);
                errors = errors + 1;
            end

            if (pwm_low !== exp_pwm_low) begin
                $display("ERROR [%s]: pwm_low mismatch. Expected %b, got %b (period=%0d duty=%0d dead_time=%0d)",
                         tag, exp_pwm_low, pwm_low, next_period, next_duty, next_dead_time);
                errors = errors + 1;
            end

            if ((pwm_high === 1'b1) && (pwm_low === 1'b1)) begin
                $display("ERROR [%s]: complementary outputs must never both be high", tag);
                errors = errors + 1;
            end
        end
    endtask

    task run_pattern;
        input [255:0] tag;
        input integer cycle_count;
        input [7:0] pattern_period;
        input [7:0] pattern_duty;
        input [3:0] pattern_dead_time;
        integer j;
        begin
            for (j = 0; j < cycle_count; j = j + 1)
                apply_cycle(tag, 1'b0, 1'b1, pattern_period, pattern_duty, pattern_dead_time);
        end
    endtask

    initial begin
        rst = 1'b0;
        enable = 1'b0;
        period = 8'd8;
        duty = 8'd0;
        dead_time = 4'd0;
        clear_golden;

        $display("=======================================");
        $display("  PWM Generator with Dead-Time Testbench");
        $display("=======================================");

        // Reset and disabled behavior.
        apply_cycle("reset_idle",            1'b1, 1'b0, 8'd8,  8'd3, 4'd2);
        apply_cycle("reset_enabled",         1'b1, 1'b1, 8'd10, 8'd4, 4'd1);
        apply_cycle("disabled_after_reset",  1'b0, 1'b0, 8'd10, 8'd4, 4'd1);
        apply_cycle("disabled_hold_1",       1'b0, 1'b0, 8'd6,  8'd2, 4'd3);
        apply_cycle("disabled_hold_2",       1'b0, 1'b0, 8'd6,  8'd5, 4'd0);

        // Startup from enable should not insert dead-time by itself.
        apply_cycle("startup_1",             1'b0, 1'b1, 8'd8,  8'd3, 4'd3);
        apply_cycle("startup_2",             1'b0, 1'b1, 8'd8,  8'd3, 4'd3);
        apply_cycle("startup_disable",       1'b0, 1'b0, 8'd8,  8'd3, 4'd3);
        apply_cycle("startup_lowside",       1'b0, 1'b1, 8'd8,  8'd0, 4'd3);

        // Corner cases for duty at the extremes.
        run_pattern("always_low_raw",  12, 8'd9, 8'd0, 4'd2);
        apply_cycle("disable_between_extremes", 1'b0, 1'b0, 8'd9, 8'd0, 4'd2);
        run_pattern("always_high_raw", 12, 8'd9, 8'd12, 4'd2);
        apply_cycle("disable_after_high", 1'b0, 1'b0, 8'd9, 8'd12, 4'd2);

        // Normal PWM without dead-time.
        run_pattern("no_deadtime_pwm", 24, 8'd8, 8'd3, 4'd0);

        // PWM with dead-time around both edges.
        apply_cycle("deadtime_reset_point", 1'b0, 1'b0, 8'd10, 8'd4, 4'd2);
        run_pattern("deadtime_two", 28, 8'd10, 8'd4, 4'd2);
        apply_cycle("deadtime_one_reset", 1'b0, 1'b0, 8'd7, 8'd2, 4'd1);
        run_pattern("deadtime_one", 18, 8'd7, 8'd2, 4'd1);

        // Disable must clear state and re-enable must restart from counter zero.
        apply_cycle("reenable_run_1",       1'b0, 1'b1, 8'd11, 8'd5, 4'd2);
        apply_cycle("reenable_run_2",       1'b0, 1'b1, 8'd11, 8'd5, 4'd2);
        apply_cycle("reenable_disable_1",   1'b0, 1'b0, 8'd11, 8'd5, 4'd2);
        apply_cycle("reenable_disable_2",   1'b0, 1'b0, 8'd11, 8'd5, 4'd2);
        apply_cycle("reenable_restart_1",   1'b0, 1'b1, 8'd11, 8'd5, 4'd2);
        apply_cycle("reenable_restart_2",   1'b0, 1'b1, 8'd11, 8'd5, 4'd2);

        // Randomized regression. Hold parameters stable while enabled so the
        // tests match the problem statement exactly.
        chosen_period = 8;
        chosen_duty = 0;
        chosen_dead = 0;
        for (i = 0; i < 250; i = i + 1) begin
            if ($urandom_range(0, 31) == 0)
                random_rst = 1'b1;
            else
                random_rst = 1'b0;

            if (random_rst)
                random_enable = 1'b0;
            else if ($urandom_range(0, 7) != 0)
                random_enable = 1'b1;
            else
                random_enable = 1'b0;

            if (random_rst || !random_enable || (enable == 1'b0)) begin
                chosen_period = $urandom_range(4, 20);
                mode_sel = $urandom_range(0, 2);

                if (mode_sel == 0) begin
                    chosen_duty = 0;
                    chosen_dead = $urandom_range(0, 3);
                end else if (mode_sel == 1) begin
                    chosen_duty = chosen_period + $urandom_range(0, 6);
                    chosen_dead = $urandom_range(0, 3);
                end else begin
                    chosen_dead = $urandom_range(0, 2);
                    low_margin = chosen_dead + 1;
                    high_margin = chosen_period - chosen_dead - 1;
                    if (low_margin > high_margin) begin
                        chosen_dead = 0;
                        low_margin = 1;
                        high_margin = chosen_period - 1;
                    end
                    chosen_duty = $urandom_range(low_margin, high_margin);
                end
            end

            apply_cycle("random_regression", random_rst, random_enable,
                        chosen_period, chosen_duty, chosen_dead);
        end

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

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

        $finish;
    end
endmodule
