/**
 * ESP32-C3 blink integration test.
 *
 * Compiles a bare-metal C program with riscv32-esp-elf-gcc (the toolchain
 * bundled with arduino-cli's ESP32 package), loads the raw binary into
 * Esp32C3Simulator, and verifies that GPIO 8 toggles as expected.
 *
 * Why bare-metal and not a full Arduino sketch?
 *   The full ESP-IDF/Arduino framework requires dozens of peripherals
 *   (cache controller, interrupt matrix, RTC, FreeRTOS scheduler, etc.) that
 *   the browser-side emulator does not implement. A bare-metal program that
 *   writes directly to GPIO MMIO registers tests exactly what the emulator
 *   is built for: pure RV32IMC instruction execution with GPIO/UART MMIO.
 *
 * Source: frontend/src/__tests__/fixtures/esp32c3-blink/blink.c
 */

import { describe, it, expect, vi, beforeAll } from 'vitest';
import { execSync } from 'child_process';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator';
import type { PinManager } from '../simulation/PinManager';

// ── Mocks (Node has no requestAnimationFrame) ─────────────────────────────────
vi.stubGlobal('requestAnimationFrame', (cb: () => void) => { cb(); return 0; });
vi.stubGlobal('cancelAnimationFrame', () => {});

// ── Paths ─────────────────────────────────────────────────────────────────────
const FIXTURE_DIR = join(__dirname, 'fixtures/esp32c3-blink');
const BIN_PATH    = join(FIXTURE_DIR, 'blink.bin');
const DIS_PATH    = join(FIXTURE_DIR, 'blink.dis');
const BUILD_SH    = join(FIXTURE_DIR, 'build.sh');

// ── Helpers ───────────────────────────────────────────────────────────────────
function mockPinManager(): PinManager {
  return {
    setPinState: vi.fn(),
    getPinState: vi.fn(() => false),
  } as unknown as PinManager;
}

/** Run N CPU steps on the simulator's internal core */
function runSteps(sim: Esp32C3Simulator, n: number): void {
  const core = (sim as unknown as { core: { step(): void } }).core;
  for (let i = 0; i < n; i++) core.step();
}

// ── Build the binary once before all tests ────────────────────────────────────
let buildError: string | null = null;
let binData: Uint8Array;

beforeAll(() => {
  // Rebuild whenever the source changes (or if binary is missing)
  try {
    execSync(`bash "${BUILD_SH}"`, { stdio: 'pipe' });
  } catch (err: unknown) {
    buildError = String((err as NodeJS.ErrnoException).stderr ?? err);
    if (!existsSync(BIN_PATH)) return;  // binary missing AND build failed
    // Binary exists from a previous build — use it but warn
    console.warn('[esp32c3-blink] Build script failed; using existing binary.\n' + buildError);
    buildError = null;
  }
  binData = new Uint8Array(readFileSync(BIN_PATH));
});

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('ESP32-C3 bare-metal blink (compiled with riscv32-esp-elf-gcc)', () => {

  it('build.sh produces a non-empty blink.bin', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    expect(binData.byteLength).toBeGreaterThan(0);
    expect(binData.byteLength).toBeLessThan(4096); // sanity: tiny bare-metal binary
    console.log(`Binary size: ${binData.byteLength} bytes`);
  });

  it('binary starts with a valid RV32 instruction at 0x42000000', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    // First 4 bytes must decode as a valid 32-bit or 16-bit RISC-V instruction
    const b0 = binData[0], b1 = binData[1];
    const half = b0 | (b1 << 8);
    const is32bit = (half & 0x3) === 0x3;
    const is16bit = (half & 0x3) !== 0x3;
    expect(is32bit || is16bit).toBe(true);

    // First instruction in the disassembly should be _start at 0x42000000
    if (existsSync(DIS_PATH)) {
      const dis = readFileSync(DIS_PATH, 'utf8');
      expect(dis).toContain('42000000 <_start>');
      console.log('\nDisassembly (first 20 lines):\n' + dis.split('\n').slice(0, 20).join('\n'));
    }
  });

  it('loadBin() loads the binary and resets PC to 0x42000000', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    const sim = new Esp32C3Simulator(mockPinManager());
    sim.loadBin(binData);
    const core = (sim as unknown as { core: { pc: number } }).core;
    expect(core.pc).toBe(0x42000000);
  });

  it('GPIO 8 goes HIGH after the first SW to GPIO_W1TS', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    const pm = mockPinManager();
    const sim = new Esp32C3Simulator(pm);
    const pinEvents: Array<{ pin: number; state: boolean; timeMs: number }> = [];
    sim.onPinChangeWithTime = (pin, state, timeMs) => pinEvents.push({ pin, state, timeMs });

    sim.loadBin(binData);
    // The blink program writes GPIO_W1TS within the first ~10 instructions
    runSteps(sim, 20);

    const gpio8Events = pinEvents.filter(e => e.pin === 8);
    expect(gpio8Events.length).toBeGreaterThan(0);
    expect(gpio8Events[0].state).toBe(true);        // first event is LED ON
    expect(gpio8Events[0].timeMs).toBeGreaterThan(0); // has a meaningful timestamp

    console.log(`GPIO 8 went HIGH at ${gpio8Events[0].timeMs.toFixed(4)} ms (simulated)`);
  });

  it('GPIO 8 toggles ON and OFF within a full blink cycle', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    const pm = mockPinManager();
    const sim = new Esp32C3Simulator(pm);
    const pinEvents: Array<{ pin: number; state: boolean }> = [];
    sim.onPinChangeWithTime = (pin, state) => pinEvents.push({ pin, state });

    sim.loadBin(binData);
    // delay(50) = ~200 instructions; two delays + GPIO writes ≈ 410 instructions/cycle
    // Run 2000 steps → ~4 complete blink cycles
    runSteps(sim, 2000);

    const gpio8 = pinEvents.filter(e => e.pin === 8);
    const highEvents = gpio8.filter(e => e.state === true);
    const lowEvents  = gpio8.filter(e => e.state === false);

    expect(highEvents.length).toBeGreaterThanOrEqual(2); // at least 2 ON events
    expect(lowEvents.length).toBeGreaterThanOrEqual(2);  // at least 2 OFF events

    // Events should strictly alternate: HIGH, LOW, HIGH, LOW ...
    for (let i = 0; i < gpio8.length - 1; i++) {
      expect(gpio8[i].state).not.toBe(gpio8[i + 1].state);
    }

    console.log(`GPIO 8 toggled ${gpio8.length} times in 2000 steps (${highEvents.length} ON, ${lowEvents.length} OFF)`);
  });

  it('PinManager.setPinState is called with correct pin and state', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    const pm = mockPinManager();
    const sim = new Esp32C3Simulator(pm);
    sim.loadBin(binData);
    runSteps(sim, 2000);

    // PinManager must have received GPIO 8 state changes
    expect(pm.setPinState).toHaveBeenCalledWith(8, true);
    expect(pm.setPinState).toHaveBeenCalledWith(8, false);
  });

  it('timestamps increase monotonically across blink events', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    const sim = new Esp32C3Simulator(mockPinManager());
    const times: number[] = [];
    sim.onPinChangeWithTime = (pin, _state, timeMs) => {
      if (pin === 8) times.push(timeMs);
    };

    sim.loadBin(binData);
    runSteps(sim, 2000);

    expect(times.length).toBeGreaterThan(2);
    for (let i = 1; i < times.length; i++) {
      expect(times[i]).toBeGreaterThan(times[i - 1]);
    }
    const totalSimMs = times[times.length - 1];
    console.log(`Simulated ${totalSimMs.toFixed(4)} ms over 2000 steps`);
  });

  it('reset() after run clears GPIO state', () => {
    if (buildError) throw new Error('Build failed:\n' + buildError);
    const sim = new Esp32C3Simulator(mockPinManager());
    sim.loadBin(binData);
    runSteps(sim, 500);

    sim.reset();
    expect(sim.isRunning()).toBe(false);

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