// Файл: zx_cartridge_tb.v
`timescale 1ns / 1ps

module zx_cartridge_tb();
    // Управляющие сигналы
    reg reset_n;
    reg iorq_n;
    reg rd_n;
    reg mreq_n;
    
    // Полная адресная шина (16 бит)
    reg [15:0] address;
    
    // Подключение отдельных бит к тестируемому модулю
    wire A7   = address[7];
    wire A13  = address[13];
    wire A14  = address[14];
    wire A15  = address[15];
    
    // Выходы тестируемого модуля
    wire ZX_ROM_blk;
    wire CR_ROM_oe_n;
    wire [5:0] CR_ROM_A;
    
    // Тестируемый модуль с параметром SELF_LOCK_VAL = 15
    zx_cartridge #(
        .SELF_LOCK_VAL(15)
    ) uut (
        .reset_n(reset_n),
        .iorq_n(iorq_n),
        .rd_n(rd_n),
        .mreq_n(mreq_n),
        .A7(A7),
        .A13(A13),
        .A14(A14),
        .A15(A15),
        .ZX_ROM_blk(ZX_ROM_blk),
        .CR_ROM_oe_n(CR_ROM_oe_n),
        .CR_ROM_A(CR_ROM_A)
    );
    
    // Задачи для моделирования циклов Z80
    // Запись в порт (активируется iorq_n, для инкремента важен его спад)
    task write_port(input [15:0] addr);
        begin
            address = addr;
            #10;
            iorq_n = 0;             // начало цикла IN/OUT
            #10;
            iorq_n = 1;             // завершение цикла – отрицательный фронт
            #10;
        end
    endtask
    
    // Чтение из памяти с проверкой прямо во время цикла
    task read_mem_check(input [15:0] addr, input exp_oe_n, input exp_blk);
        begin
            address = addr;
            #10;
            mreq_n = 0;             // запрос памяти
            rd_n   = 0;             // чтение
            #10;                    // даём сигналам установиться
            // проверяем прямо во время активного цикла
            if (CR_ROM_oe_n !== exp_oe_n) begin
                $display("ОШИБКА: read_mem_check по адресу %h: CR_ROM_oe_n ожидалось %b, получено %b", addr, exp_oe_n, CR_ROM_oe_n);
            end
            if (ZX_ROM_blk !== exp_blk) begin
                $display("ОШИБКА: read_mem_check по адресу %h: ZX_ROM_blk ожидалось %b, получено %b", addr, exp_blk, ZX_ROM_blk);
            end
            #10;
            mreq_n = 1;
            rd_n   = 1;
            #10;
        end
    endtask
    
    // Простое чтение без проверки (для создания циклов)
    task read_mem(input [15:0] addr);
        begin
            address = addr;
            #10;
            mreq_n = 0;
            rd_n   = 0;
            #20;
            mreq_n = 1;
            rd_n   = 1;
            #10;
        end
    endtask
    
    // Проверка с выводом сообщения (для простых случаев)
    task check_equal(input [31:0] expected, input [31:0] actual, input [80*8:0] msg);
        if (expected !== actual) begin
            $display("ОШИБКА: %s. Ожидалось %d, получено %d", msg, expected, actual);
        end
    endtask
    
    integer i;
    
    initial begin
        $dumpfile("zx_cartridge.vcd");
        $dumpvars(0, zx_cartridge_tb);
        
        // Исходное состояние: сброс активен, все сигналы неактивны
        reset_n = 0;
        iorq_n  = 1;
        rd_n    = 1;
        mreq_n  = 1;
        address = 16'h0000;
        #100;
        reset_n = 1;
        #10;
        
        // ------------------------------------------------------------
        // Тест 1: Инкремент происходит только при A7=0 и спаде iorq_n
        // ------------------------------------------------------------
        $display("=== Тест 1: Условие инкремента (A7=0 и спад iorq_n) ===");
        check_equal(0, CR_ROM_A, "Начальное значение CR_ROM_A");
        
        // Попытка с A7=1 – не должен инкрементироваться
        write_port(16'h0080);       // A7=1 (адрес 0x80)
        #10;
        check_equal(0, CR_ROM_A, "После записи в порт 0x80 (A7=1)");
        
        // Корректный инкремент с A7=0
        write_port(16'h007F);       // A7=0
        #10;
        check_equal(1, CR_ROM_A, "После первой записи в 0x7F");
        
        write_port(16'h007F);       // второй раз
        #10;
        check_equal(2, CR_ROM_A, "После второй записи в 0x7F");
        
        // ------------------------------------------------------------
        // Тест 2: Достижение SELF_LOCK_VAL (15) блокирует дальнейшие инкременты
        // ------------------------------------------------------------
        $display("=== Тест 2: Самоблокировка при значении 15 ===");
        // Инкрементируем до 14 (с 3 до 14)
        for (i = 3; i <= 14; i = i + 1) begin
            write_port(16'h007F);
            #10;
            // Просто выводим текущее значение для информации
            $display("После записи #%0d, CR_ROM_A = %0d", i, CR_ROM_A);
        end
        
        // Пятнадцатая запись – должна активировать блокировку
        write_port(16'h007F);
        #10;
        $display("После пятнадцатой записи, CR_ROM_A = %0d (самоблокировка активирована)", CR_ROM_A);
        
        // Ещё одна запись – после блокировки счётчик может измениться или нет,
        // но картридж уже заблокирован, поэтому не проверяем
        write_port(16'h007F);
        #10;
        $display("Запись после блокировки, CR_ROM_A = %0d (значение не важно)", CR_ROM_A);
        
        // ------------------------------------------------------------
        // Тест 3: При self_lock=1 CR_ROM_oe_n не активируется даже в нижней области ПЗУ
        // ------------------------------------------------------------
        $display("=== Тест 3: CR_ROM_oe_n неактивен во время блокировки ===");
        read_mem_check(16'h0100, 1'b1, 1'b0);  // ожидаем oe_n=1 (неактивен), blk=0
        
        // ------------------------------------------------------------
        // Тест 4: Сброс обнуляет счётчик и снимает блокировку
        // ------------------------------------------------------------
        $display("=== Тест 4: Сброс ===");
        reset_n = 0;
        #20;
        reset_n = 1;
        #10;
        check_equal(0, CR_ROM_A, "После сброса");
        check_equal(1, CR_ROM_oe_n, "CR_ROM_oe_n после сброса");
        
        // ------------------------------------------------------------
        // Тест 5: Активация CR_ROM_oe_n при чтении нижних 8 кБ (self_lock=0)
        // ------------------------------------------------------------
        $display("=== Тест 5: Активация CR_ROM_oe_n в нижней области ПЗУ (0x0000-0x1FFF) ===");
        
        // Чтение внутри нижней области
        read_mem_check(16'h0100, 1'b0, 1'b1);  // ожидаем oe_n=0 (активен), blk=1
        read_mem_check(16'h1FFF, 1'b0, 1'b1);  // граница нижней области
        
        // Чтение вне нижней области
        read_mem_check(16'h2000, 1'b1, 1'b0);  // A13=1
        read_mem_check(16'h4001, 1'b1, 1'b0);  // A14=1
        
        // ------------------------------------------------------------
        // Тест 6: Проверка влияния mreq_n и rd_n
        // ------------------------------------------------------------
        $display("=== Тест 6: Управляющие сигналы mreq_n и rd_n ===");
        address = 16'h0100;
        #10;
        
        // mreq_n=0, rd_n=1 – чтение не активно
        mreq_n = 0; rd_n = 1;
        #10;
        check_equal(1, CR_ROM_oe_n, "CR_ROM_oe_n при rd_n=1");
        
        // mreq_n=1, rd_n=0 – нет запроса памяти
        mreq_n = 1; rd_n = 0;
        #10;
        check_equal(1, CR_ROM_oe_n, "CR_ROM_oe_n при mreq_n=1");
        
        // Оба активны – должно включиться
        mreq_n = 0; rd_n = 0;
        #10;
        check_equal(0, CR_ROM_oe_n, "CR_ROM_oe_n при обоих активных");
        
        // Возврат в исходное
        mreq_n = 1; rd_n = 1;
        #10;
        
        $display("=== Все тесты завершены ===");
        $finish;
    end
    
endmodule