/**
 * ESP32-C3 Browser Emulation Tests
 *
 * Tests the RV32IMC (Integer + Multiply + Compressed) emulator used
 * for browser-side ESP32-C3 simulation without a QEMU backend.
 *
 * Test groups:
 *  1. RV32M — multiply/divide instructions (via RiscVCore directly)
 *  2. RV32C — 16-bit compressed instructions (via RiscVCore directly)
 *  3. Esp32C3Simulator — UART0 serial output
 *  4. Esp32C3Simulator — GPIO pin toggling
 *  5. Lifecycle — start/stop/reset
 */

import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { RiscVCore } from '../simulation/RiscVCore';
import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator';
import type { PinManager } from '../simulation/PinManager';

// ── Node environment stubs ───────────────────────────────────────────────────

let rafDepth = 0;
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
  // Allow a bounded number of recursive RAF calls to test the loop
  if (rafDepth < 2) { rafDepth++; cb(0); rafDepth--; }
  return 1;
});
vi.stubGlobal('cancelAnimationFrame', () => {});

// ── Helpers ──────────────────────────────────────────────────────────────────

function writeWord(mem: Uint8Array, offset: number, val: number): void {
  mem[offset]     =  val        & 0xFF;
  mem[offset + 1] = (val >>  8) & 0xFF;
  mem[offset + 2] = (val >> 16) & 0xFF;
  mem[offset + 3] = (val >> 24) & 0xFF;
}

function writeHalf(mem: Uint8Array, offset: number, val: number): void {
  mem[offset]     =  val       & 0xFF;
  mem[offset + 1] = (val >> 8) & 0xFF;
}

/** Run exactly n steps on a core */
function runSteps(core: RiscVCore, n: number): void {
  for (let i = 0; i < n; i++) core.step();
}

/** Create a minimal mock PinManager */
function mockPinManager(): PinManager {
  return {
    setPinState:  vi.fn(),
    getPinState:  vi.fn(() => false),
    registerPin:  vi.fn(),
    unregisterPin: vi.fn(),
  } as unknown as PinManager;
}

/** Access the simulator's internal RiscVCore for direct testing */
function getCore(sim: Esp32C3Simulator): RiscVCore {
  return (sim as unknown as { core: RiscVCore }).core;
}

/** Access the simulator's internal flash buffer for direct programming */
function getFlash(sim: Esp32C3Simulator): Uint8Array {
  return (sim as unknown as { flash: Uint8Array }).flash;
}

// ── Test Group 1: RV32M (multiply/divide) ────────────────────────────────────

describe('RV32M — multiply/divide extension', () => {
  let mem: Uint8Array;
  let core: RiscVCore;

  beforeEach(() => {
    mem  = new Uint8Array(64);
    core = new RiscVCore(mem, 0);
    core.reset(0);
  });

  it('MUL: 6 × 7 = 42', () => {
    writeWord(mem, 0, 0x00600093);  // ADDI x1, x0, 6
    writeWord(mem, 4, 0x00700113);  // ADDI x2, x0, 7
    writeWord(mem, 8, 0x022081B3);  // MUL  x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(42);
  });

  it('MUL: negative × positive = negative', () => {
    writeWord(mem, 0, 0xFFF00093);  // ADDI x1, x0, -1
    writeWord(mem, 4, 0x00300113);  // ADDI x2, x0, 3
    writeWord(mem, 8, 0x022081B3);  // MUL  x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(-3);
  });

  it('MULH: signed upper — (-1) × (-1) upper 32 bits = 0', () => {
    // (-1) * (-1) = 1; upper 32 bits of 64-bit result = 0
    writeWord(mem, 0, 0xFFF00093);  // ADDI x1, x0, -1
    writeWord(mem, 4, 0xFFF00113);  // ADDI x2, x0, -1
    writeWord(mem, 8, 0x022091B3);  // MULH x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(0);
  });

  it('MULHU: unsigned upper — 0xFFFFFFFF × 0xFFFFFFFF upper 32 bits', () => {
    // 0xFFFFFFFF * 0xFFFFFFFF = 0xFFFFFFFE_00000001; upper = 0xFFFFFFFE
    writeWord(mem, 0, 0xFFF00093);  // ADDI x1, x0, -1  (= 0xFFFFFFFF unsigned)
    writeWord(mem, 4, 0xFFF00113);  // ADDI x2, x0, -1
    writeWord(mem, 8, 0x022081B3 | (3 << 12));  // MULHU x3, x1, x2  (funct3=3)
    runSteps(core, 3);
    expect(core.regs[3] >>> 0).toBe(0xFFFFFFFE);
  });

  it('DIV: 42 / 7 = 6', () => {
    writeWord(mem, 0, 0x02A00093);  // ADDI x1, x0, 42
    writeWord(mem, 4, 0x00700113);  // ADDI x2, x0, 7
    // DIV x3, x1, x2: opcode=0x33, rd=3, funct3=4, rs1=1, rs2=2, funct7=1
    // = (1<<25)|(2<<20)|(1<<15)|(4<<12)|(3<<7)|0x33 = 0x0220C1B3
    writeWord(mem, 8, 0x0220C1B3);  // DIV  x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(6);
  });

  it('DIV: signed — -7 / 2 = -3 (truncate toward zero)', () => {
    writeWord(mem, 0, 0xFF900093);  // ADDI x1, x0, -7
    writeWord(mem, 4, 0x00200113);  // ADDI x2, x0, 2
    writeWord(mem, 8, 0x0220C1B3);  // DIV  x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(-3);
  });

  it('DIV: divide by zero returns -1 (0xFFFFFFFF)', () => {
    writeWord(mem, 0, 0x00500093);  // ADDI x1, x0, 5
    writeWord(mem, 4, 0x00000113);  // ADDI x2, x0, 0
    writeWord(mem, 8, 0x0220C1B3);  // DIV  x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(-1);
  });

  it('REM: 10 % 3 = 1', () => {
    writeWord(mem, 0, 0x00A00093);  // ADDI x1, x0, 10
    writeWord(mem, 4, 0x00300113);  // ADDI x2, x0, 3
    // REM x3, x1, x2: funct3=6 → (1<<25)|(2<<20)|(1<<15)|(6<<12)|(3<<7)|0x33 = 0x0220E1B3
    writeWord(mem, 8, 0x0220E1B3);  // REM  x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(1);
  });

  it('REM: divide by zero returns dividend', () => {
    writeWord(mem, 0, 0x00700093);  // ADDI x1, x0, 7
    writeWord(mem, 4, 0x00000113);  // ADDI x2, x0, 0
    writeWord(mem, 8, 0x0220E1B3);  // REM  x3, x1, x2
    runSteps(core, 3);
    expect(core.regs[3]).toBe(7);
  });
});

// ── Test Group 2: RV32C (compressed instructions) ───────────────────────────

describe('RV32C — 16-bit compressed instruction extension', () => {
  let mem: Uint8Array;
  let core: RiscVCore;

  beforeEach(() => {
    mem  = new Uint8Array(64);
    core = new RiscVCore(mem, 0);
    core.reset(0);
  });

  it('C.LI x1, 5: loads immediate 5 into x1 and advances PC by 2', () => {
    // C.LI: funct3=010, imm[5]=0, rd=x1(00001), imm[4:0]=00101, op=01
    // Encoding: bit15..0 = 0_100_0_00001_00101_01 = 0x4095
    writeHalf(mem, 0, 0x4095);
    core.step();
    expect(core.regs[1]).toBe(5);
    expect(core.pc).toBe(2);
  });

  it('C.LI x2, -1: sign-extends negative immediate', () => {
    // C.LI: funct3=010, imm[5]=1, rd=x2(00010), imm[4:0]=11111, op=01
    // Encoding: bit15..0 = 0_100_1_00010_11111_01 = 0x5105
    // Checking: bit12=1, bits[11:7]=00010=2, bits[6:2]=11111=31, bits[1:0]=01
    // Value: 0_1_0_0 | 1_0_0_0 | 1_0_1_1 | 1_1_1_0_1 ... let me compute:
    // op=01: bit1=0,bit0=1; imm[4:0]=11111: bit6=1,bit5=1,bit4=1,bit3=1,bit2=1
    // rd=00010: bit11=0,bit10=0,bit9=0,bit8=1,bit7=0
    // imm[5]=1: bit12=1; funct3=010: bit15=0,bit14=1,bit13=0
    // = 0100 1000 1111 1101 wait...
    // bit15=0,14=1,13=0,12=1,11=0,10=0,9=0,8=1,7=0,6=1,5=1,4=1,3=1,2=1,1=0,0=1
    // = 0101 0001 0111 1101 = 0x517D? Let me just compute the halfword value:
    // Bits (from bit15 to bit0):
    // 0,1,0,1 | 0,0,0,1 | 0,1,1,1 | 1,1,0,1
    // wait: funct3=010 → bits[15:13]=010: bit15=0,bit14=1,bit13=0
    // bit12=1 (imm[5]=1)
    // rd=2=00010: bit11=0,bit10=0,bit9=0,bit8=1,bit7=0
    // imm[4:0]=11111: bit6=1,bit5=1,bit4=1,bit3=1,bit2=1
    // op=01: bit1=0,bit0=1
    // = 0100_1000_1111_1101 = 0x48FD? Wait let me re-group:
    // bits 15..12: 0,1,0,1 = 0x5... no: bit15=0,bit14=1,bit13=0,bit12=1 → 0101 = 5? That's nibble 0101=5
    // bits 11..8:  0,0,0,1 → 0001 = 1
    // bits 7..4:   0,1,1,1 → 0111 = 7
    // bits 3..0:   1,1,0,1 → 1101 = D
    // So 0x517D. Let's verify the C.LI decode:
    // half=0x517D: op=0x1, funct3=(0x517D>>13)&7=(0x28)&7=... 0x517D=20861, 20861>>13=2, 2&7=2 ✓ (funct3=2=C.LI)
    // bit12=(0x517D>>12)&1=5&1=1 ✓
    // rd=(0x517D>>7)&31=(0xA2)&31... 0x517D>>7=163, 163&31=3? Hmm that gives rd=3 not rd=2...
    // 0x517D in binary: 0101 0001 0111 1101
    // bits[11:7]: bit11=0,bit10=0,bit9=0,bit8=1,bit7=0 = 00010 = 2 ✓
    // But (0x517D>>7) = 0101 0001 0 = 162, 162&31=162-160=2 ✓ (I miscalculated before)
    // bits[6:2]: bit6=1,bit5=1,bit4=1,bit3=1,bit2=1 = 11111 = 31 ✓
    // imm6 = sext((1<<5)|(31), 6) = sext(63, 6) = sext(0b111111, 6) = -1 ✓
    writeHalf(mem, 0, 0x517D);
    core.step();
    expect(core.regs[2]).toBe(-1);
    expect(core.pc).toBe(2);
  });

  it('C.ADDI x1, 3: adds immediate to register', () => {
    // Preset x1=10
    writeWord(mem, 0, 0x00A00093);  // ADDI x1, x0, 10  (32-bit)
    // C.ADDI x1, 3: funct3=000, imm[5]=0, rd=x1, imm[4:0]=00011, op=01
    // bit12=0, bits[11:7]=00001, bits[6:2]=00011, bits[1:0]=01
    // = 0000 0000 1000 1101 = 0x008D
    writeHalf(mem, 4, 0x008D);
    runSteps(core, 2);
    expect(core.regs[1]).toBe(13);
    expect(core.pc).toBe(6);
  });

  it('C.MV x5, x1: copies register (ADD x5, x0, x1)', () => {
    // Preset x1=42
    writeWord(mem, 0, 0x02A00093);  // ADDI x1, x0, 42
    // C.MV x5, x1: funct3=100, bit12=0, rd=x5(00101), rs2=x1(00001), op=10
    // bit15=1,14=0,13=0,12=0,bits[11:7]=00101=5,bits[6:2]=00001=1,bits[1:0]=10
    // = 1000 0010 1000 0110 = 0x8286
    writeHalf(mem, 4, 0x8286);
    runSteps(core, 2);
    expect(core.regs[5]).toBe(42);
  });

  it('C.ADD x1, x2: adds two registers', () => {
    writeWord(mem, 0, 0x00300093);  // ADDI x1, x0, 3
    writeWord(mem, 4, 0x00400113);  // ADDI x2, x0, 4
    // C.ADD x1, x2: funct3=100, bit12=1, rd=x1(00001), rs2=x2(00010), op=10
    // bit15=1,14=0,13=0,12=1,bits[11:7]=00001,bits[6:2]=00010,bits[1:0]=10
    // = 1001 0000 1000 1010 = 0x908A
    writeHalf(mem, 8, 0x908A);
    runSteps(core, 3);
    expect(core.regs[1]).toBe(7);
  });

  it('C.J +4: jumps forward 4 bytes from compressed instruction', () => {
    // C.J with offset=4, starting at PC=0
    // CJ format: funct3=101, imm[3:1]=010 → bits[5:3]=010 → bit4=1, bits[1:0]=01
    // = 1010_0000_0001_0001 = 0xA011
    writeHalf(mem, 0, 0xA011);
    core.step();
    expect(core.pc).toBe(4);  // 0 + 4
  });

  it('C.BEQZ x8, offset: branch taken when register is zero', () => {
    // x8 is 0 (default), so branch should be taken
    // C.BEQZ x8, +4: rs1'=x8(=0 encoded as 0b000), offset=4
    // CB format: funct3=110, imm[8]=0, rs1'=000, imm[7:6]=00, imm[2:1]=10, imm[5]=0, op=01
    // offset=4: imm[2:1]=10 → bits[4:3]=10=2, other imm bits=0
    // bit15=0,14=1,13=1,12=0,bit11=0,bit10=0,bits[9:7]=000,bit6=0,bit5=0,bits[4:3]=10,bit2=0,bit1=0,bit0=1
    // = 0110 0000 0001 0001 ... let me compute more carefully
    // CB: bits[15:13]=110, bit[12]=imm[8]=0, bits[11:10]=imm[4:3]=00, bits[9:7]=rs1'=000
    //     bits[6:5]=imm[7:6]=00, bits[4:3]=imm[2:1]=10, bit[2]=imm[5]=0, bits[1:0]=01
    // For offset=4: imm[2:1]=10 → bits[4:3]=10 → bit4=1, bit3=0
    // = 1100_0000_0001_0001 = 0xC011
    writeHalf(mem, 0, 0xC011);
    core.step();
    expect(core.pc).toBe(4);
  });

  it('C.BEQZ x8, offset: branch NOT taken when register is non-zero', () => {
    core.regs[8] = 5;
    writeHalf(mem, 0, 0xC011);  // C.BEQZ x8, +4
    core.step();
    expect(core.pc).toBe(2);  // falls through
  });

  it('C.SWSP + C.LWSP: stack round-trip', () => {
    // Set sp (x2) to offset 32 within our buffer (so stack writes stay in bounds)
    core.regs[2] = 32;
    core.regs[1] = 0xDEAD;

    // C.SWSP rs2=x1, offset=0:
    // CSS: funct3=110, uimm[5:2]=bits[12:9]=0000, uimm[7:6]=bits[8:7]=00, rs2=bits[6:2]=00001, op=10
    // = 1101 0000 0000 0110 = 0xD006? let me compute:
    // bit15=1,14=1,13=0,12=0,bits[11:10]=uimm[5:4]=00,bits[9:7]=uimm[3:1]=000,bits[6:2]=rs2=00001,bits[1:0]=10
    // Wait the spec says: bits[12:9]=uimm[5:2], bits[8:7]=uimm[7:6]
    // For offset=0: all uimm bits=0 → bits[12:9]=0000, bits[8:7]=00
    // = 1101 0000 0000 0110 = 0xD006?
    // bit15=1,bit14=1,bit13=0,bit12=0 → 1100
    // bits[11:10]=00, bits[9:8]=00 → 0000
    // bits[7]=0 → 0
    // bits[6:2]=00001 → bit6=0,bit5=0,bit4=0,bit3=0,bit2=1
    // bits[1:0]=10
    // = 1100 0000 0000 0110 = 0xC006
    writeHalf(mem, 0, 0xC006);   // C.SWSP x1, 0(sp)

    // C.LWSP rd=x3, offset=0:
    // CI: funct3=010, bit12=uimm[5]=0, rd=x3=00011, bits[6:4]=uimm[4:2]=000, bits[3:2]=uimm[7:6]=00, op=10
    // = 0100 0001 1000 0010 = 0x4182
    writeHalf(mem, 2, 0x4182);   // C.LWSP x3, 0(sp)

    runSteps(core, 2);
    expect(core.regs[3]).toBe(0xDEAD);
  });
});

// ── Test Group 3: Esp32C3Simulator — UART ───────────────────────────────────

describe('Esp32C3Simulator — UART0 serial output', () => {
  let sim: Esp32C3Simulator;

  beforeEach(() => {
    sim = new Esp32C3Simulator(mockPinManager());
  });

  afterEach(() => {
    sim.stop();
  });

  it('writing to UART0 FIFO (0x60000000) triggers onSerialData', () => {
    const received: string[] = [];
    sim.onSerialData = (ch) => received.push(ch);

    const flash = getFlash(sim);
    const core  = getCore(sim);

    // Program at IROM offset 0 (= address 0x42000000):
    // LUI a1, 0x60000   → a1 = 0x60000000  (UART0_BASE)
    // ADDI a0, x0, 72   → a0 = 72 = 'H'
    // SB a0, 0(a1)       → write byte to UART0 FIFO
    writeWord(flash, 0, 0x600005B7);  // LUI a1, 0x60000
    writeWord(flash, 4, 0x04800513);  // ADDI a0, x0, 72
    writeWord(flash, 8, 0x00A58023);  // SB a0, 0(a1)

    core.reset(0x42000000);
    runSteps(core, 3);

    expect(received).toEqual(['H']);
  });

  it('writing multiple bytes emits each character', () => {
    const received: string[] = [];
    sim.onSerialData = (ch) => received.push(ch);

    const flash = getFlash(sim);
    const core  = getCore(sim);

    // LUI a1, 0x60000        → a1 = UART0_BASE
    // ADDI a0, x0, 65 ('A')
    // SB a0, 0(a1)
    // ADDI a0, x0, 66 ('B')
    // SB a0, 0(a1)
    writeWord(flash, 0,  0x600005B7);  // LUI a1, 0x60000
    writeWord(flash, 4,  0x04100513);  // ADDI a0, x0, 65 ('A')
    writeWord(flash, 8,  0x00A58023);  // SB a0, 0(a1)
    writeWord(flash, 12, 0x04200513);  // ADDI a0, x0, 66 ('B')
    writeWord(flash, 16, 0x00A58023);  // SB a0, 0(a1)

    core.reset(0x42000000);
    runSteps(core, 5);

    expect(received).toEqual(['A', 'B']);
  });

  it('serialWrite injects bytes into RX FIFO, firmware can read them', () => {
    const flash = getFlash(sim);
    const core  = getCore(sim);

    sim.serialWrite('X');

    // Program: LB a0, 0(a1) — reads from UART0_FIFO
    // LUI a1, 0x60000        → a1 = 0x60000000
    // LBU a0, 0(a1)          → a0 = UART0_FIFO read
    writeWord(flash, 0, 0x600005B7);  // LUI a1, 0x60000
    writeWord(flash, 4, 0x00058503);  // LBU a0, 0(a1)

    core.reset(0x42000000);
    runSteps(core, 2);

    expect(core.regs[10]).toBe('X'.charCodeAt(0));  // a0 = 88 = 'X'
  });
});

// ── Test Group 4: Esp32C3Simulator — GPIO ────────────────────────────────────

describe('Esp32C3Simulator — GPIO pin toggling', () => {
  let sim: Esp32C3Simulator;

  beforeEach(() => {
    sim = new Esp32C3Simulator(mockPinManager());
  });

  afterEach(() => {
    sim.stop();
  });

  it('SW to GPIO_OUT_W1TS (offset +8) sets GPIO0 high', () => {
    const pinChanges: Array<{ pin: number; state: boolean }> = [];
    sim.onPinChangeWithTime = (pin, state) => pinChanges.push({ pin, state });

    const flash = getFlash(sim);
    const core  = getCore(sim);

    // LUI t1, 0x60004        → t1 = 0x60004000 (GPIO_BASE)
    // ADDI t0, x0, 1         → t0 = 1 (bit 0 = GPIO0)
    // SW t0, 8(t1)            → write to GPIO_OUT_W1TS
    writeWord(flash, 0, 0x60004337);  // LUI t1, 0x60004
    writeWord(flash, 4, 0x00100293);  // ADDI t0, x0, 1
    writeWord(flash, 8, 0x00532423);  // SW t0, 8(t1)   [offset 8 = W1TS]

    core.reset(0x42000000);
    runSteps(core, 3);

    expect(pinChanges).toContainEqual({ pin: 0, state: true });
  });

  it('SW to GPIO_OUT_W1TC (offset +12) clears GPIO0', () => {
    const pinChanges: Array<{ pin: number; state: boolean }> = [];
    sim.onPinChangeWithTime = (pin, state) => pinChanges.push({ pin, state });

    const flash = getFlash(sim);
    const core  = getCore(sim);

    // First set GPIO0 high via W1TS, then clear via W1TC
    writeWord(flash, 0,  0x60004337);  // LUI t1, 0x60004
    writeWord(flash, 4,  0x00100293);  // ADDI t0, x0, 1
    writeWord(flash, 8,  0x00532423);  // SW t0, 8(t1)   — set bit 0 (W1TS)
    writeWord(flash, 12, 0x00532623);  // SW t0, 12(t1)  — clear bit 0 (W1TC)

    core.reset(0x42000000);
    runSteps(core, 4);

    expect(pinChanges).toContainEqual({ pin: 0, state: true  });
    expect(pinChanges).toContainEqual({ pin: 0, state: false });
  });

  it('SW to GPIO_OUT sets multiple pins via direct write', () => {
    const setPins: number[] = [];
    sim.onPinChangeWithTime = (pin, state) => { if (state) setPins.push(pin); };

    const flash = getFlash(sim);
    const core  = getCore(sim);

    // Write 0b101 (bits 0 and 2) to GPIO_OUT (offset +4)
    writeWord(flash, 0, 0x60004337);  // LUI t1, 0x60004
    writeWord(flash, 4, 0x00500293);  // ADDI t0, x0, 5  (0b101)
    writeWord(flash, 8, 0x00532223);  // SW t0, 4(t1)    — GPIO_OUT

    core.reset(0x42000000);
    runSteps(core, 3);

    expect(setPins).toContain(0);
    expect(setPins).toContain(2);
    expect(setPins).not.toContain(1);
  });

  it('pinManager.setPinState is called on GPIO change', () => {
    const pm = mockPinManager();
    const s  = new Esp32C3Simulator(pm);
    const flash = getFlash(s);
    const core  = getCore(s);

    writeWord(flash, 0, 0x60004337);  // LUI t1, 0x60004
    writeWord(flash, 4, 0x00100293);  // ADDI t0, x0, 1
    writeWord(flash, 8, 0x00532423);  // SW t0, 8(t1)

    core.reset(0x42000000);
    runSteps(core, 3);

    expect(pm.setPinState).toHaveBeenCalledWith(0, true);
    s.stop();
  });
});

// ── Test Group 5: Lifecycle ──────────────────────────────────────────────────

describe('Esp32C3Simulator — lifecycle', () => {
  it('starts not running', () => {
    const sim = new Esp32C3Simulator(mockPinManager());
    expect(sim.isRunning()).toBe(false);
    sim.stop();
  });

  it('start() sets running, stop() clears it', () => {
    const sim = new Esp32C3Simulator(mockPinManager());
    sim.start();
    expect(sim.isRunning()).toBe(true);
    sim.stop();
    expect(sim.isRunning()).toBe(false);
  });

  it('reset() stops simulator and clears register state', () => {
    const sim  = new Esp32C3Simulator(mockPinManager());
    const core = getCore(sim);
    core.regs[1] = 999;
    sim.start();
    sim.reset();
    expect(sim.isRunning()).toBe(false);
    expect(core.regs[1]).toBe(0);
    expect(core.pc).toBe(0x42000000);
  });

  it('reset() clears GPIO output state', () => {
    const pinChanges: Array<{ pin: number; state: boolean }> = [];
    const sim = new Esp32C3Simulator(mockPinManager());
    sim.onPinChangeWithTime = (pin, state) => pinChanges.push({ pin, state });

    const flash = getFlash(sim);
    const core  = getCore(sim);
    writeWord(flash, 0, 0x60004337);
    writeWord(flash, 4, 0x00100293);
    writeWord(flash, 8, 0x00532423);
    core.reset(0x42000000);
    runSteps(core, 3);

    sim.reset();

    const gpioOut = (sim as unknown as { gpioOut: number }).gpioOut;
    expect(gpioOut).toBe(0);
  });

  it('double start() is a no-op (does not create duplicate loops)', () => {
    let rafCalls = 0;
    vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
      rafCalls++;
      // Don't recurse
      return rafCalls;
    });

    const sim = new Esp32C3Simulator(mockPinManager());
    sim.start();
    sim.start();  // second call should be ignored
    expect(rafCalls).toBe(1);
    sim.stop();
  });
});
