/**
 * AVRSimulator Tests
 *
 * Tests the ATmega328p emulator (Arduino Uno backend) including:
 * - Lifecycle: create, loadHex, start, stop, reset
 * - ADC initialization (AVRADC)
 * - Timer1 / Timer2 instantiation
 * - PWM OCR register monitoring
 * - Pin state changes via step() execution
 * - Integration: real HEX that toggles pin 13
 */

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

// ─── Mock requestAnimationFrame (not available in node) ──────────────────────
// Depth-limited: calls cb once synchronously but prevents re-entrancy so that
// animation loops (start() → execute → requestAnimationFrame(execute) → …)
// do not cause infinite recursion / stack overflow in tests.
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());

// ─── Minimal Intel HEX payloads ───────────────────────────────────────────────
//
// BLINK_HEX: 5 AVR instructions that set pin 13 HIGH then loop:
//   LDI r16, 0xFF       ; 0F EF  — load 0xFF
//   OUT DDRB, r16       ; 04 B9  — set PORTB as output  (I/O 0x04 = DDRB)
//   LDI r16, 0x20       ; 00 E2  — load 0x20 (bit 5 = pin 13)
//   OUT PORTB, r16      ; 05 B9  — set pin 13 HIGH      (I/O 0x05 = PORTB)
//   RJMP .-2            ; FF CF  — infinite loop (jump to self)
//
// Checksum verified manually.
const BLINK_HEX =
  ':0A0000000FEF04B900E205B9FFCFCD\n' +
  ':00000001FF\n';

// EOF-only HEX — minimal valid file, empty program
const EMPTY_HEX = ':00000001FF\n';

// ─── Basic lifecycle ──────────────────────────────────────────────────────────

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

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

  it('starts in idle state', () => {
    expect(sim).toBeDefined();
    expect(sim.isRunning()).toBe(false);
    expect(sim.getSpeed()).toBe(1.0);
  });

  it('loads a valid HEX without throwing', () => {
    expect(() => sim.loadHex(EMPTY_HEX)).not.toThrow();
    expect(() => sim.loadHex(BLINK_HEX)).not.toThrow();
  });

  it('start() transitions to running state', () => {
    sim.loadHex(EMPTY_HEX);
    sim.start();
    expect(sim.isRunning()).toBe(true);
    sim.stop();
    expect(sim.isRunning()).toBe(false);
  });

  it('stop() is idempotent (safe to call when not running)', () => {
    expect(() => sim.stop()).not.toThrow();
    expect(sim.isRunning()).toBe(false);
  });

  it('reset() stops the simulation and re-initializes', () => {
    sim.loadHex(BLINK_HEX);
    sim.start();
    expect(sim.isRunning()).toBe(true);
    sim.reset();
    expect(sim.isRunning()).toBe(false);
  });

  it('clamps speed within [0.1, 10.0]', () => {
    sim.setSpeed(0.001);
    expect(sim.getSpeed()).toBe(0.1);

    sim.setSpeed(999);
    expect(sim.getSpeed()).toBe(10.0);

    sim.setSpeed(2.5);
    expect(sim.getSpeed()).toBe(2.5);
  });
});

// ─── ADC ─────────────────────────────────────────────────────────────────────

describe('AVRSimulator — ADC', () => {
  it('getADC() returns null before loadHex()', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    expect(sim.getADC()).toBeNull();
  });

  it('getADC() returns an AVRADC instance after loadHex()', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);
    const adc = sim.getADC();
    expect(adc).not.toBeNull();
    expect(adc).toBeDefined();
  });

  it('can inject voltage on ADC channels', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);
    const adc = sim.getADC();
    expect(adc).not.toBeNull();

    // channelValues is an array of floats (0..5V)
    adc!.channelValues[0] = 2.5;  // A0
    expect(adc!.channelValues[0]).toBe(2.5);

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

  it('ADC state is preserved after reset()', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);
    sim.getADC()!.channelValues[0] = 3.3;

    sim.reset();
    // After reset a fresh AVRADC is created — channel[0] is unset (undefined) or 0
    expect(sim.getADC()!.channelValues[0] ?? 0).toBe(0);
  });
});

// ─── PWM monitoring ───────────────────────────────────────────────────────────

describe('AVRSimulator — PWM OCR monitoring', () => {
  it('PinManager receives PWM update when OCR register changes', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);

    const pwmCb = vi.fn();
    pm.onPwmChange(9, pwmCb);  // D9 = OCR1AL (0x88)

    // Directly write a value to the OCR1AL register in CPU data memory
    const cpu = (sim as any).cpu;
    cpu.data[0x88] = 128;  // 50% duty cycle

    // Simulate a frame tick to trigger OCR polling
    sim.start();
    sim.stop();

    expect(pwmCb).toHaveBeenCalledWith(9, 128 / 255);
  });

  it('PWM covers all six Arduino PWM pins', () => {
    // OCR addr → Arduino pin mapping
    const PWM_MAP = [
      { addr: 0x47, pin: 6  },  // OCR0A
      { addr: 0x48, pin: 5  },  // OCR0B
      { addr: 0x88, pin: 9  },  // OCR1A
      { addr: 0x8A, pin: 10 },  // OCR1B
      { addr: 0xB3, pin: 11 },  // OCR2A
      { addr: 0xB4, pin: 3  },  // OCR2B
    ];

    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);

    const cbs: Record<number, ReturnType<typeof vi.fn>> = {};
    PWM_MAP.forEach(({ pin }) => {
      cbs[pin] = vi.fn();
      pm.onPwmChange(pin, cbs[pin]);
    });

    const cpu = (sim as any).cpu;
    PWM_MAP.forEach(({ addr }, i) => {
      cpu.data[addr] = (i + 1) * 25; // 25, 50, 75, 100, 125, 150
    });

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

    PWM_MAP.forEach(({ pin }, i) => {
      const expected = ((i + 1) * 25) / 255;
      expect(cbs[pin]).toHaveBeenCalledWith(pin, expected);
    });
  });
});

// ─── Pin output via setPinState ───────────────────────────────────────────────

describe('AVRSimulator — external pin driving', () => {
  it('setPinState drives PORTD pins 0-7 without throwing', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);

    // setPinState drives external INPUT on the pin (like a button press).
    // It does NOT directly update PinManager — PinManager reflects CPU PORT OUTPUT.
    expect(() => sim.setPinState(0, true)).not.toThrow();
    expect(() => sim.setPinState(4, true)).not.toThrow();
    expect(() => sim.setPinState(7, false)).not.toThrow();
  });

  it('setPinState drives PORTB pins 8-13', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);

    const cb = vi.fn();
    pm.onPinChange(13, cb);  // D13 = PORTB bit 5

    sim.setPinState(13, true);
    // Note: setPinState calls portB.setPin which may not immediately update the
    // PORTB listener in test context, but state is reflected internally
    expect(() => sim.setPinState(13, false)).not.toThrow();
  });

  it('setPinState drives PORTC analog pins 14-19', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(EMPTY_HEX);
    expect(() => sim.setPinState(14, true)).not.toThrow();  // A0
    expect(() => sim.setPinState(19, false)).not.toThrow(); // A5
  });
});

// ─── Integration: real HEX execution ─────────────────────────────────────────

describe('AVRSimulator — integration with real HEX', () => {
  it('executes BLINK_HEX and sets pin 13 HIGH after 4 steps', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(BLINK_HEX);

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

    // Step through: LDI → OUT DDRB → LDI → OUT PORTB
    sim.step(); // LDI r16, 0xFF
    sim.step(); // OUT DDRB, r16
    sim.step(); // LDI r16, 0x20
    sim.step(); // OUT PORTB, r16  → triggers PORTB listener

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

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

  it('pin 13 stays HIGH after RJMP loop', () => {
    const pm = new PinManager();
    const sim = new AVRSimulator(pm);
    sim.loadHex(BLINK_HEX);

    sim.step(); // LDI
    sim.step(); // OUT DDRB
    sim.step(); // LDI
    sim.step(); // OUT PORTB (pin 13 → HIGH)

    // Execute 10 more steps (RJMP loop, OUT PORTB again, etc.)
    for (let i = 0; i < 10; i++) sim.step();

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