/**
 * Integration Tests
 *
 * End-to-end tests that combine real simulators with PinManager and
 * component-style callbacks to verify the full simulation pipeline.
 *
 * Covers:
 * - AVRSimulator + PinManager: HEX execution drives pins, ADC, PWM
 * - RP2040Simulator + PinManager: GPIO updates, setPinState, reset
 * - Board switching: both simulators can share the same PinManager
 * - PWM pipeline: OCR register → PinManager.updatePwm → component callback
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { AVRSimulator } from '../simulation/AVRSimulator';
import { RP2040Simulator } from '../simulation/RP2040Simulator';
import { PinManager } from '../simulation/PinManager';

// ─── RAF depth-limited mock ───────────────────────────────────────────────────
// Calls execute() once synchronously per start(); prevents infinite recursion.
beforeEach(() => {
  let counter = 0;
  let depth = 0;
  vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
    if (depth === 0) {
      depth++;
      cb(0);
      depth--;
    }
    return ++counter;
  });
  vi.stubGlobal('cancelAnimationFrame', vi.fn());
});
afterEach(() => vi.unstubAllGlobals());

// ─── HEX / binary fixtures ────────────────────────────────────────────────────

/**
 * Minimal Intel HEX that sets pin 13 HIGH and loops:
 *   LDI r16, 0xFF  → OUT DDRB → LDI r16, 0x20 → OUT PORTB → RJMP .-2
 */
const BLINK_HEX =
  ':0A0000000FEF04B900E205B9FFCFCD\n' +
  ':00000001FF\n';

/** Empty program (EOF record only) */
const EMPTY_HEX = ':00000001FF\n';

/** Create a base64-encoded all-zero binary of the given size in KB */
function zeroBinary(sizeKb = 1): string {
  const bytes = new Uint8Array(sizeKb * 1024);
  let s = '';
  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
  return btoa(s);
}

// ─── AVRSimulator + PinManager ────────────────────────────────────────────────

describe('Integration — AVRSimulator + PinManager', () => {
  let pm: PinManager;
  let sim: AVRSimulator;

  beforeEach(() => {
    pm = new PinManager();
    sim = new AVRSimulator(pm);
  });
  afterEach(() => sim.stop());

  // ── step()-based tests (no RAF needed) ──────────────────────────────────────

  it('executes BLINK_HEX and drives pin 13 HIGH after 4 instructions', () => {
    sim.loadHex(BLINK_HEX);

    const changes: boolean[] = [];
    pm.onPinChange(13, (_pin, state) => changes.push(state));

    sim.step(); // LDI r16, 0xFF
    sim.step(); // OUT DDRB, r16
    sim.step(); // LDI r16, 0x20
    sim.step(); // OUT PORTB, r16  → pin 13 HIGH

    expect(pm.getPinState(13)).toBe(true);
    expect(changes).toContain(true);
  });

  it('pin 13 is LOW before any instructions execute', () => {
    sim.loadHex(BLINK_HEX);
    expect(pm.getPinState(13)).toBe(false);
  });

  it('LED component callback fires when pin 13 goes HIGH', () => {
    sim.loadHex(BLINK_HEX);

    const ledState = { on: false };
    pm.onPinChange(13, (_pin, state) => { ledState.on = state; });

    sim.step(); sim.step(); sim.step(); sim.step();

    expect(ledState.on).toBe(true);
  });

  it('pin 13 stays HIGH through the RJMP loop', () => {
    sim.loadHex(BLINK_HEX);
    sim.step(); sim.step(); sim.step(); sim.step(); // set pin HIGH

    for (let i = 0; i < 10; i++) sim.step(); // execute 10 more (RJMP loops)

    expect(pm.getPinState(13)).toBe(true);
  });

  it('ADC channel is accessible and writeable after loadHex()', () => {
    sim.loadHex(EMPTY_HEX);

    const adc = sim.getADC();
    expect(adc).not.toBeNull();

    adc!.channelValues[0] = 3.3; // Inject 3.3V on A0
    expect(adc!.channelValues[0]).toBe(3.3);

    adc!.channelValues[3] = 1.8; // A3
    expect(adc!.channelValues[3]).toBe(1.8);
  });

  it('ADC resets to default on reset()', () => {
    sim.loadHex(EMPTY_HEX);
    sim.getADC()!.channelValues[0] = 4.5;
    sim.reset();
    // Fresh AVRADC: channelValues[0] is unset (undefined) or 0 — either means no voltage
    expect(sim.getADC()!.channelValues[0] ?? 0).toBe(0);
  });

  // ── start()/stop() — one frame via depth-limited RAF ───────────────────────

  it('PWM OCR register write propagates to PinManager via one frame', () => {
    sim.loadHex(EMPTY_HEX);

    const pwmChanges: Array<{ pin: number; dc: number }> = [];
    pm.onPwmChange(9, (pin, dc) => pwmChanges.push({ pin, dc }));

    // Directly set OCR1AL = 128 (pin 9, ~50% duty cycle)
    (sim as any).cpu.data[0x88] = 128;

    sim.start(); // one frame executes, polls PWM registers
    sim.stop();

    expect(pwmChanges.length).toBeGreaterThan(0);
    expect(pwmChanges[0].pin).toBe(9);
    expect(pwmChanges[0].dc).toBeCloseTo(128 / 255, 2);
  });

  it('all six PWM pins propagate their OCR values in one frame', () => {
    sim.loadHex(EMPTY_HEX);

    const PWM_MAP = [
      { addr: 0x47, pin: 6,  value: 50  },
      { addr: 0x48, pin: 5,  value: 100 },
      { addr: 0x88, pin: 9,  value: 150 },
      { addr: 0x8A, pin: 10, value: 200 },
      { addr: 0xB3, pin: 11, value: 25  },
      { addr: 0xB4, pin: 3,  value: 75  },
    ];

    const received: Record<number, number> = {};
    PWM_MAP.forEach(({ pin }) => {
      pm.onPwmChange(pin, (_p, dc) => { received[pin] = dc; });
    });

    const cpu = (sim as any).cpu;
    PWM_MAP.forEach(({ addr, value }) => { cpu.data[addr] = value; });

    sim.start();
    sim.stop();

    PWM_MAP.forEach(({ pin, value }) => {
      expect(received[pin]).toBeCloseTo(value / 255, 2);
    });
  });

  it('setPinState drives external input pins without throwing', () => {
    sim.loadHex(EMPTY_HEX);
    // setPinState drives external INPUT (button/switch); PinManager tracks CPU PORT OUTPUT.
    // These should not throw regardless of pin range.
    expect(() => sim.setPinState(4, true)).not.toThrow();   // PORTD
    expect(() => sim.setPinState(13, true)).not.toThrow();  // PORTB
    expect(() => sim.setPinState(14, true)).not.toThrow();  // PORTC/A0
  });

  it('multiple onPinChange subscribers all fire for the same pin', () => {
    sim.loadHex(BLINK_HEX);

    const cb1 = vi.fn();
    const cb2 = vi.fn();
    pm.onPinChange(13, cb1);
    pm.onPinChange(13, cb2);

    sim.step(); sim.step(); sim.step(); sim.step();

    expect(cb1).toHaveBeenCalledWith(13, true);
    expect(cb2).toHaveBeenCalledWith(13, true);
  });
});

// ─── RP2040Simulator + PinManager ────────────────────────────────────────────

describe('Integration — RP2040Simulator + PinManager', () => {
  let pm: PinManager;
  let sim: RP2040Simulator;

  beforeEach(() => {
    pm = new PinManager();
    sim = new RP2040Simulator(pm);
  });
  afterEach(() => sim.stop());

  it('triggerPinChange on GPIO25 fires LED_BUILTIN listener', () => {
    sim.loadBinary(zeroBinary());

    const ledState = { on: false };
    pm.onPinChange(25, (_pin, state) => { ledState.on = state; });

    pm.triggerPinChange(25, true);
    expect(ledState.on).toBe(true);
  });

  it('triggerPinChange HIGH then LOW fires listener twice', () => {
    sim.loadBinary(zeroBinary());

    const cb = vi.fn();
    pm.onPinChange(25, cb);

    pm.triggerPinChange(25, true);
    pm.triggerPinChange(25, false);

    expect(cb).toHaveBeenCalledTimes(2);
    expect(cb).toHaveBeenNthCalledWith(1, 25, true);
    expect(cb).toHaveBeenNthCalledWith(2, 25, false);
  });

  it('setPinState does not throw for all 30 GPIO pins', () => {
    sim.loadBinary(zeroBinary());
    for (let i = 0; i < 30; i++) {
      expect(() => sim.setPinState(i, true)).not.toThrow();
      expect(() => sim.setPinState(i, false)).not.toThrow();
    }
  });

  it('multiple independent GPIO listeners fire only their own pin', () => {
    sim.loadBinary(zeroBinary());

    const cb7  = vi.fn();
    const cb25 = vi.fn();
    const cb0  = vi.fn();

    pm.onPinChange(7,  cb7);
    pm.onPinChange(25, cb25);
    pm.onPinChange(0,  cb0);

    pm.triggerPinChange(25, true);

    expect(cb25).toHaveBeenCalledWith(25, true);
    expect(cb7).not.toHaveBeenCalled();
    expect(cb0).not.toHaveBeenCalled();
  });

  it('GPIO listeners are still active after reset()', () => {
    sim.loadBinary(zeroBinary());
    sim.reset();

    const cb = vi.fn();
    pm.onPinChange(25, cb);
    pm.triggerPinChange(25, true);

    expect(cb).toHaveBeenCalledWith(25, true);
  });

  it('getADC() returns null before loadBinary()', () => {
    expect(sim.getADC()).toBeNull();
  });

  it('getADC() returns an object after loadBinary()', () => {
    sim.loadBinary(zeroBinary());
    expect(sim.getADC()).not.toBeNull();
  });
});

// ─── Board switching ──────────────────────────────────────────────────────────

describe('Integration — board switching (AVR ↔ RP2040)', () => {
  it('both simulators share the same PinManager instance', () => {
    const pm = new PinManager();
    const avrSim = new AVRSimulator(pm);
    const rpSim  = new RP2040Simulator(pm);

    expect((avrSim as any).pinManager).toBe(pm);
    expect((rpSim  as any).pinManager).toBe(pm);
  });

  it('stopping AVR and starting RP2040 does not corrupt PinManager', () => {
    const pm = new PinManager();

    const avrSim = new AVRSimulator(pm);
    avrSim.loadHex(EMPTY_HEX);
    avrSim.start();
    avrSim.stop();

    // Load RP2040 binary (no start() to avoid running 2M ARM cycles in tests)
    const rpSim = new RP2040Simulator(pm);
    rpSim.loadBinary(zeroBinary());

    // PinManager must still dispatch callbacks correctly after board switch
    const cb = vi.fn();
    pm.onPinChange(13, cb);
    pm.triggerPinChange(13, true);
    expect(cb).toHaveBeenCalledWith(13, true);
  });

  it('clearAllListeners resets PinManager between board switches', () => {
    const pm = new PinManager();

    const cb = vi.fn();
    pm.onPinChange(5, cb);
    expect(pm.getListenersCount()).toBe(1);

    pm.clearAllListeners();
    pm.triggerPinChange(5, true);

    expect(cb).not.toHaveBeenCalled();
    expect(pm.getListenersCount()).toBe(0);
  });

  it('AVR pin 13 HIGH does not affect RP2040 GPIO13 listener (no cross-talk)', () => {
    const pm = new PinManager();

    // Step 1: AVR drives pin 13 HIGH via CPU execution
    const avrSim = new AVRSimulator(pm);
    avrSim.loadHex(BLINK_HEX);

    const avrCb = vi.fn();
    pm.onPinChange(13, avrCb);
    avrSim.step(); avrSim.step(); avrSim.step(); avrSim.step();
    expect(pm.getPinState(13)).toBe(true);

    // Step 2: Switch to RP2040 — clear AVR listeners, load RP2040 binary
    pm.clearAllListeners();
    const rpCb = vi.fn();
    pm.onPinChange(13, rpCb);

    const rpSim = new RP2040Simulator(pm);
    rpSim.loadBinary(zeroBinary());

    // Simulate RP2040 driving GPIO13 LOW via triggerPinChange
    pm.triggerPinChange(13, false);
    expect(rpCb).toHaveBeenCalledWith(13, false);
  });
});

// ─── PWM pipeline: OCR → PinManager → component callback ─────────────────────

describe('Integration — PWM pipeline (AVR → PinManager → component)', () => {
  it('OCR1AL=128 → onPwmChange(9) fires with duty 128/255', () => {
    const pm  = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);

    const pwmValues: number[] = [];
    pm.onPwmChange(9, (_pin, dc) => pwmValues.push(dc));

    (sim as any).cpu.data[0x88] = 128;
    sim.start();
    sim.stop();

    expect(pwmValues.length).toBeGreaterThan(0);
    expect(pwmValues[0]).toBeCloseTo(128 / 255, 3);
  });

  it('RGB LED component receives correct brightness via PWM', () => {
    const pm  = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);

    // Simulate RGB LED component listening to PWM on pins 9, 10, 11
    const rgb = { red: 0, green: 0, blue: 0 };
    pm.onPwmChange(9,  (_p, dc) => { rgb.red   = Math.round(dc * 255); });
    pm.onPwmChange(10, (_p, dc) => { rgb.green = Math.round(dc * 255); });
    pm.onPwmChange(11, (_p, dc) => { rgb.blue  = Math.round(dc * 255); });

    (sim as any).cpu.data[0x88] = 255; // OCR1A → D9  = 100%
    (sim as any).cpu.data[0x8A] = 128; // OCR1B → D10 = ~50%
    (sim as any).cpu.data[0xB3] = 0;   // OCR2A → D11 = 0%

    sim.start();
    sim.stop();

    expect(rgb.red).toBe(255);
    expect(rgb.green).toBe(128);
    // OCR2A=0 → updatePwm fires only if it differs from -1 (initial lastOcrValue)
    // With lastOcrValue initialized to -1 and data[0xB3]=0, 0 !== -1 → fires
    expect(rgb.blue).toBe(0);
  });

  it('PWM value does not re-fire if OCR register stays the same', () => {
    const pm  = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);

    const pwmCb = vi.fn();
    pm.onPwmChange(9, pwmCb);

    (sim as any).cpu.data[0x88] = 200;
    sim.start(); // frame 1: fires updatePwm(9, 200/255)
    sim.stop();

    const callsAfterFrame1 = pwmCb.mock.calls.length;

    sim.start(); // frame 2: OCR value unchanged → should NOT fire again
    sim.stop();

    expect(pwmCb.mock.calls.length).toBe(callsAfterFrame1); // no additional calls
  });
});
