/**
 * esp32-servo-pot.test.ts
 *
 * Tests for the ESP32 Servo + Potentiometer example, focusing on:
 *   1. Servo subscribes to onPwmChange for ESP32 (not AVR cycle measurement)
 *   2. Servo uses onPinChange for AVR (existing behavior)
 *   3. Servo uses onPinChangeWithTime for RP2040
 *   4. LEDC update routes to correct GPIO pin (not LEDC channel)
 *   5. LEDC duty_pct is normalized to 0.0–1.0
 *   6. LEDC fallback to channel when gpio=-1
 *   7. Servo angle maps correctly from duty cycle (pulse-width based)
 *   8. Potentiometer setAdcVoltage works for ESP32 via bridge shim
 *   9. ESP32 ADC channel mapping (GPIO → ADC1 channel)
 *  10. LEDC polling reads float[] duty (not uint32) from QEMU internals(6)
 */

import { describe, it, expect, beforeEach, vi } from 'vitest';

// ── Mocks ────────────────────────────────────────────────────────────────────

vi.mock('../simulation/AVRSimulator', () => ({
  AVRSimulator: vi.fn(function (this: any) {
    this.onSerialData = null;
    this.onBaudRateChange = null;
    this.onPinChangeWithTime = null;
    this.start = vi.fn();
    this.stop = vi.fn();
    this.reset = vi.fn();
    this.loadHex = vi.fn();
    this.addI2CDevice = vi.fn();
    this.setPinState = vi.fn();
    this.isRunning = vi.fn().mockReturnValue(true);
    this.registerSensor = vi.fn().mockReturnValue(false);
    this.pinManager = {
      onPinChange: vi.fn().mockReturnValue(() => {}),
      onPwmChange: vi.fn().mockReturnValue(() => {}),
      updatePwm: vi.fn(),
    };
    this.getCurrentCycles = vi.fn().mockReturnValue(1000);
    this.getClockHz = vi.fn().mockReturnValue(16_000_000);
    this.cpu = { data: new Uint8Array(512).fill(0), cycles: 1000 };
  }),
}));

vi.mock('../simulation/RP2040Simulator', () => ({
  RP2040Simulator: vi.fn(function (this: any) {
    this.onSerialData = null;
    this.onPinChangeWithTime = null;
    this.start = vi.fn();
    this.stop = vi.fn();
    this.reset = vi.fn();
    this.loadBinary = vi.fn();
    this.addI2CDevice = vi.fn();
    this.isRunning = vi.fn().mockReturnValue(true);
    this.registerSensor = vi.fn().mockReturnValue(false);
    this.pinManager = {
      onPinChange: vi.fn().mockReturnValue(() => {}),
      onPwmChange: vi.fn().mockReturnValue(() => {}),
      updatePwm: vi.fn(),
    };
  }),
}));

vi.mock('../simulation/PinManager', () => ({
  PinManager: vi.fn(function (this: any) {
    this.updatePort = vi.fn();
    this.onPinChange = vi.fn().mockReturnValue(() => {});
    this.onPwmChange = vi.fn().mockReturnValue(() => {});
    this.getListenersCount = vi.fn().mockReturnValue(0);
    this.updatePwm = vi.fn();
    this.triggerPinChange = vi.fn();
  }),
}));

vi.mock('../simulation/I2CBusManager', () => ({
  VirtualDS1307: vi.fn(function (this: any) {}),
  VirtualTempSensor: vi.fn(function (this: any) {}),
  I2CMemoryDevice: vi.fn(function (this: any) {}),
}));

vi.mock('../store/useOscilloscopeStore', () => ({
  useOscilloscopeStore: {
    getState: vi.fn().mockReturnValue({ channels: [], pushSample: vi.fn() }),
  },
}));

vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1);
vi.stubGlobal('cancelAnimationFrame', vi.fn());
vi.stubGlobal('sessionStorage', {
  getItem: vi.fn().mockReturnValue('test-session-id'),
  setItem: vi.fn(),
});

// ── Imports (after mocks) ────────────────────────────────────────────────────

import { PartSimulationRegistry } from '../simulation/parts/PartSimulationRegistry';
import '../simulation/parts/ComplexParts';
import { PinManager } from '../simulation/PinManager';
import { RP2040Simulator } from '../simulation/RP2040Simulator';
import { setAdcVoltage } from '../simulation/parts/partUtils';

// ─── Mock factories ──────────────────────────────────────────────────────────

function makeElement(props: Record<string, unknown> = {}): HTMLElement {
  return {
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
    angle: 0,
    ...props,
  } as unknown as HTMLElement;
}

/** Simulator mock that mimics Esp32BridgeShim (no valid CPU cycles) */
function makeEsp32Shim() {
  let pwmCallback: ((pin: number, duty: number) => void) | null = null;
  const unsubPwm = vi.fn();
  const adcCalls: { channel: number; millivolts: number }[] = [];

  return {
    pinManager: {
      onPinChange: vi.fn().mockReturnValue(() => {}),
      onPwmChange: vi.fn().mockImplementation((_pin: number, cb: (pin: number, duty: number) => void) => {
        pwmCallback = cb;
        return unsubPwm;
      }),
      updatePwm: vi.fn(),
      triggerPinChange: vi.fn(),
    },
    setPinState: vi.fn(),
    isRunning: vi.fn().mockReturnValue(true),
    getCurrentCycles: vi.fn().mockReturnValue(-1), // ESP32: no valid cycles
    getClockHz: vi.fn().mockReturnValue(240_000_000),
    registerSensor: vi.fn().mockReturnValue(true),
    updateSensor: vi.fn(),
    unregisterSensor: vi.fn(),
    // Esp32BridgeShim.setAdcVoltage — mirrors the real implementation
    setAdcVoltage: vi.fn().mockImplementation((pin: number, voltage: number) => {
      let channel = -1;
      if (pin >= 36 && pin <= 39) channel = pin - 36;
      else if (pin >= 32 && pin <= 35) channel = pin - 28;
      if (channel < 0) return false;
      adcCalls.push({ channel, millivolts: Math.round(voltage * 1000) });
      return true;
    }),
    // Test helpers
    _getPwmCallback: () => pwmCallback,
    _unsubPwm: unsubPwm,
    _getAdcCalls: () => adcCalls,
  };
}

/** Simulator mock that mimics AVR (has valid CPU cycles) */
function makeAVRSim() {
  let pinCallback: ((pin: number, state: boolean) => void) | null = null;
  const unsubPin = vi.fn();

  return {
    pinManager: {
      onPinChange: vi.fn().mockImplementation((_pin: number, cb: (pin: number, state: boolean) => void) => {
        pinCallback = cb;
        return unsubPin;
      }),
      onPwmChange: vi.fn().mockReturnValue(() => {}),
      updatePwm: vi.fn(),
    },
    setPinState: vi.fn(),
    isRunning: vi.fn().mockReturnValue(true),
    getCurrentCycles: vi.fn().mockReturnValue(1000),
    getClockHz: vi.fn().mockReturnValue(16_000_000),
    cpu: { data: new Uint8Array(512).fill(0), cycles: 1000 },
    registerSensor: vi.fn().mockReturnValue(false),
    // Test helpers
    _getPinCallback: () => pinCallback,
    _unsubPin: unsubPin,
  };
}

const pinMap =
  (map: Record<string, number>) =>
  (name: string): number | null =>
    name in map ? map[name] : null;

// ─────────────────────────────────────────────────────────────────────────────
// 1. Servo — ESP32 path: subscribes to onPwmChange
// ─────────────────────────────────────────────────────────────────────────────

describe('Servo — ESP32 PWM subscription', () => {
  const logic = () => PartSimulationRegistry.get('servo')!;

  it('subscribes to onPwmChange when simulator has no valid CPU cycles (ESP32 shim)', () => {
    const shim = makeEsp32Shim();
    const el = makeElement();
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-esp32');

    expect(shim.pinManager.onPwmChange).toHaveBeenCalledWith(13, expect.any(Function));
  });

  it('updates angle when PWM duty cycle changes (pulse-width mapping)', () => {
    const shim = makeEsp32Shim();
    const el = makeElement() as any;
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-esp32-angle');

    const cb = shim._getPwmCallback();
    expect(cb).not.toBeNull();

    // ESP32 servo pulse-width mapping:
    // MIN_DC = 544/20000 = 0.0272 → 0°
    // MAX_DC = 2400/20000 = 0.12 → 180°
    const MIN_DC = 544 / 20000;
    const MAX_DC = 2400 / 20000;

    // At min duty → 0°
    cb!(13, MIN_DC);
    expect(el.angle).toBe(0);

    // At max duty → 180°
    cb!(13, MAX_DC);
    expect(el.angle).toBe(180);

    // At mid duty → ~90°
    const midDC = (MIN_DC + MAX_DC) / 2;
    cb!(13, midDC);
    expect(el.angle).toBe(90);
  });

  it('ignores out-of-range duty cycles (noise filtering)', () => {
    const shim = makeEsp32Shim();
    const el = makeElement() as any;
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-noise');

    const cb = shim._getPwmCallback();

    // Set to a known angle first
    cb!(13, 0.075); // mid-range
    const knownAngle = el.angle;

    // Very low duty (< 1%) is ignored
    cb!(13, 0.005);
    expect(el.angle).toBe(knownAngle); // unchanged

    // Very high duty (> 20%) is ignored
    cb!(13, 0.5);
    expect(el.angle).toBe(knownAngle); // unchanged
  });

  it('clamps angle to 0-180 range', () => {
    const shim = makeEsp32Shim();
    const el = makeElement() as any;
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-clamp');

    const cb = shim._getPwmCallback();

    // Slightly below MIN_DC (but above 1% filter) → clamps to 0°
    cb!(13, 0.015);
    expect(el.angle).toBe(0);

    // Slightly above MAX_DC (but below 20% filter) → clamps to 180°
    cb!(13, 0.15);
    expect(el.angle).toBe(180);
  });

  it('cleanup unsubscribes from onPwmChange', () => {
    const shim = makeEsp32Shim();
    const el = makeElement();
    const cleanup = logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-cleanup');

    cleanup();
    expect(shim._unsubPwm).toHaveBeenCalled();
  });

  it('does NOT subscribe to onPinChange (AVR cycle measurement)', () => {
    const shim = makeEsp32Shim();
    const el = makeElement();
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-no-pin');

    expect(shim.pinManager.onPinChange).not.toHaveBeenCalled();
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 2. Servo — AVR path: uses onPinChange + cycle measurement
// ─────────────────────────────────────────────────────────────────────────────

describe('Servo — AVR cycle-based measurement', () => {
  const logic = () => PartSimulationRegistry.get('servo')!;

  it('subscribes to onPinChange (not onPwmChange) when simulator has valid CPU cycles', () => {
    const avr = makeAVRSim();
    const el = makeElement();
    logic().attachEvents!(el, avr as any, pinMap({ PWM: 9 }), 'servo-avr');

    expect(avr.pinManager.onPinChange).toHaveBeenCalledWith(9, expect.any(Function));
    // Should NOT use onPwmChange for AVR
    expect(avr.pinManager.onPwmChange).not.toHaveBeenCalled();
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 3. Servo — RP2040 path: uses onPinChangeWithTime (instanceof check)
// ─────────────────────────────────────────────────────────────────────────────

describe('Servo — RP2040 timing-based measurement', () => {
  const logic = () => PartSimulationRegistry.get('servo')!;

  it('uses onPinChangeWithTime when simulator is RP2040Simulator instance', () => {
    const rp = new RP2040Simulator() as any;
    const el = makeElement();
    logic().attachEvents!(el, rp as any, pinMap({ PWM: 15 }), 'servo-rp2040');

    // RP2040 path sets onPinChangeWithTime
    expect(rp.onPinChangeWithTime).toBeTypeOf('function');
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 4-6. LEDC update routing — PinManager.updatePwm
// ─────────────────────────────────────────────────────────────────────────────

describe('LEDC update routing', () => {
  let pm: any;

  beforeEach(() => {
    pm = new PinManager();
  });

  it('routes to GPIO pin when update.gpio >= 0', () => {
    const update = { channel: 0, duty: 7.5, duty_pct: 7.5, gpio: 13 };
    const targetPin = (update.gpio !== undefined && update.gpio >= 0)
      ? update.gpio
      : update.channel;
    pm.updatePwm(targetPin, update.duty_pct / 100);

    expect(pm.updatePwm).toHaveBeenCalledWith(13, 0.075);
  });

  it('falls back to channel when gpio is -1', () => {
    const update = { channel: 2, duty: 50, duty_pct: 50, gpio: -1 };
    const targetPin = (update.gpio !== undefined && update.gpio >= 0)
      ? update.gpio
      : update.channel;
    pm.updatePwm(targetPin, update.duty_pct / 100);

    expect(pm.updatePwm).toHaveBeenCalledWith(2, 0.5);
  });

  it('falls back to channel when gpio is undefined', () => {
    const update = { channel: 3, duty: 100, duty_pct: 100 } as any;
    const targetPin = (update.gpio !== undefined && update.gpio >= 0)
      ? update.gpio
      : update.channel;
    pm.updatePwm(targetPin, update.duty_pct / 100);

    expect(pm.updatePwm).toHaveBeenCalledWith(3, 1.0);
  });

  it('normalizes duty_pct to 0.0–1.0 (divides by 100)', () => {
    const update = { channel: 0, duty: 25, duty_pct: 25, gpio: 5 };
    const targetPin = (update.gpio !== undefined && update.gpio >= 0)
      ? update.gpio
      : update.channel;
    pm.updatePwm(targetPin, update.duty_pct / 100);

    expect(pm.updatePwm).toHaveBeenCalledWith(5, 0.25);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 7. Servo angle mapping — pulse-width based for ESP32
// ─────────────────────────────────────────────────────────────────────────────

describe('Servo angle mapping (pulse-width)', () => {
  const logic = () => PartSimulationRegistry.get('servo')!;

  it('maps real servo duty cycles to correct angles', () => {
    const shim = makeEsp32Shim();
    const el = makeElement() as any;
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-map');

    const cb = shim._getPwmCallback();

    // Servo pulse widths at 50Hz (20ms period):
    // 544µs = 2.72% duty → 0°
    // 1472µs = 7.36% duty → 90°
    // 2400µs = 12.00% duty → 180°
    const testCases = [
      { pulseUs: 544, expectedAngle: 0 },
      { pulseUs: 1008, expectedAngle: 45 },
      { pulseUs: 1472, expectedAngle: 90 },
      { pulseUs: 1936, expectedAngle: 135 },
      { pulseUs: 2400, expectedAngle: 180 },
    ];

    for (const { pulseUs, expectedAngle } of testCases) {
      const dutyCycle = pulseUs / 20000; // fraction of 20ms period
      cb!(13, dutyCycle);
      expect(el.angle).toBe(expectedAngle);
    }
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 8. Potentiometer — setAdcVoltage on ESP32
// ─────────────────────────────────────────────────────────────────────────────

describe('Potentiometer — ESP32 ADC path', () => {
  it('setAdcVoltage delegates to Esp32BridgeShim.setAdcVoltage', () => {
    const shim = makeEsp32Shim();
    const result = setAdcVoltage(shim as any, 34, 1.65);
    expect(result).toBe(true);
    expect(shim.setAdcVoltage).toHaveBeenCalledWith(34, 1.65);
  });

  it('setAdcVoltage returns false for non-ADC ESP32 pins', () => {
    const shim = makeEsp32Shim();
    // GPIO 13 is not an ADC pin on ESP32
    const result = setAdcVoltage(shim as any, 13, 1.65);
    expect(result).toBe(false);
  });

  it('setAdcVoltage works for AVR (pin 14-19)', () => {
    const avrSim = makeAVRSim() as any;
    avrSim.getADC = () => ({ channelValues: new Array(6).fill(0) });
    const result = setAdcVoltage(avrSim as any, 14, 2.5);
    expect(result).toBe(true);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 9. ESP32 ADC channel mapping (GPIO → ADC1 channel)
// ─────────────────────────────────────────────────────────────────────────────

describe('ESP32 ADC channel mapping', () => {
  it('maps GPIO 36-39 → ADC1 CH0-3', () => {
    const shim = makeEsp32Shim();
    setAdcVoltage(shim as any, 36, 1.0);
    setAdcVoltage(shim as any, 37, 1.0);
    setAdcVoltage(shim as any, 38, 1.0);
    setAdcVoltage(shim as any, 39, 1.0);

    const calls = shim._getAdcCalls();
    expect(calls.map(c => c.channel)).toEqual([0, 1, 2, 3]);
  });

  it('maps GPIO 32-35 → ADC1 CH4-7', () => {
    const shim = makeEsp32Shim();
    setAdcVoltage(shim as any, 32, 1.0);
    setAdcVoltage(shim as any, 33, 1.0);
    setAdcVoltage(shim as any, 34, 1.0);
    setAdcVoltage(shim as any, 35, 1.0);

    const calls = shim._getAdcCalls();
    expect(calls.map(c => c.channel)).toEqual([4, 5, 6, 7]);
  });

  it('converts voltage to millivolts correctly', () => {
    const shim = makeEsp32Shim();
    setAdcVoltage(shim as any, 34, 1.65);

    const calls = shim._getAdcCalls();
    expect(calls[0].millivolts).toBe(1650);
  });

  it('rejects non-ADC GPIOs (0-31)', () => {
    const shim = makeEsp32Shim();
    const result = setAdcVoltage(shim as any, 13, 1.0);
    expect(result).toBe(false);
    expect(shim._getAdcCalls()).toHaveLength(0);
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 10. LEDC 0x5000 marker decoding — channel extraction fix
// ─────────────────────────────────────────────────────────────────────────────

describe('LEDC 0x5000 marker decoding', () => {
  // QEMU fires: qemu_set_irq(ledc_sync, 0x5000 | (ledn << 8) | intensity)
  // Worker must extract: ledc_ch = (direction >> 8) & 0x0F (NOT & 0xFF)

  function decodeLedc(direction: number) {
    const marker = direction & 0xF000;
    if (marker !== 0x5000) return null;
    const ledc_ch = (direction >> 8) & 0x0F;  // correct: strips marker bits
    const intensity = direction & 0xFF;
    return { ledc_ch, intensity };
  }

  function decodeLedcBroken(direction: number) {
    const marker = direction & 0xF000;
    if (marker !== 0x5000) return null;
    const ledc_ch = (direction >> 8) & 0xFF;  // BUG: includes marker bits
    const intensity = direction & 0xFF;
    return { ledc_ch, intensity };
  }

  it('HS channel 0 (ledn=0): direction=0x500B → ch=0, not ch=80', () => {
    const direction = 0x5000 | (0 << 8) | 11; // 0x500B
    const correct = decodeLedc(direction)!;
    const broken = decodeLedcBroken(direction)!;

    expect(correct.ledc_ch).toBe(0);   // correct
    expect(broken.ledc_ch).toBe(80);   // BUG: 0x50 = 80
    expect(correct.intensity).toBe(11);
  });

  it('LS channel 0 (ledn=8): direction=0x5811 → ch=8, not ch=88', () => {
    const direction = 0x5000 | (8 << 8) | 17; // 0x5811
    const correct = decodeLedc(direction)!;
    const broken = decodeLedcBroken(direction)!;

    expect(correct.ledc_ch).toBe(8);   // correct
    expect(broken.ledc_ch).toBe(88);   // BUG: 0x58 = 88
    expect(correct.intensity).toBe(17);
  });

  it('HS channel 7 (ledn=7): direction=0x5732 → ch=7', () => {
    const direction = 0x5000 | (7 << 8) | 50; // 0x5732
    expect(decodeLedc(direction)!.ledc_ch).toBe(7);
    expect(decodeLedc(direction)!.intensity).toBe(50);
  });

  it('LS channel 7 (ledn=15): direction=0x5F64 → ch=15', () => {
    const direction = 0x5000 | (15 << 8) | 100; // 0x5F64
    expect(decodeLedc(direction)!.ledc_ch).toBe(15);
    expect(decodeLedc(direction)!.intensity).toBe(100);
  });

  it('all 16 channels decode correctly', () => {
    for (let ledn = 0; ledn < 16; ledn++) {
      const direction = 0x5000 | (ledn << 8) | 42;
      const result = decodeLedc(direction)!;
      expect(result.ledc_ch).toBe(ledn);
      expect(result.intensity).toBe(42);
    }
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 11. GPIO out_sel scanning — LEDC→GPIO mapping
// ─────────────────────────────────────────────────────────────────────────────

describe('GPIO out_sel scanning for LEDC mapping', () => {
  // Simulates what the LEDC poll thread does: read gpio_out_sel[40] and
  // scan for LEDC signal values (72-87) to build _ledc_gpio_map

  function scanOutSel(outSel: number[]): Map<number, number> {
    const ledcGpioMap = new Map<number, number>();
    for (let gpioPin = 0; gpioPin < outSel.length; gpioPin++) {
      const signal = outSel[gpioPin] & 0xFF;
      if (signal >= 72 && signal <= 87) {
        const ledcCh = signal - 72;
        ledcGpioMap.set(ledcCh, gpioPin);
      }
    }
    return ledcGpioMap;
  }

  it('detects LEDC HS ch0 (signal=72) on GPIO 13', () => {
    const outSel = new Array(40).fill(256); // 256 = no function
    outSel[13] = 72; // LEDC HS ch0 → GPIO 13
    const map = scanOutSel(outSel);

    expect(map.get(0)).toBe(13);
    expect(map.size).toBe(1);
  });

  it('detects LEDC LS ch0 (signal=80) on GPIO 2', () => {
    const outSel = new Array(40).fill(256);
    outSel[2] = 80; // LEDC LS ch0 → GPIO 2
    const map = scanOutSel(outSel);

    expect(map.get(8)).toBe(2); // ch8 = LS ch0
  });

  it('detects multiple LEDC channels', () => {
    const outSel = new Array(40).fill(256);
    outSel[13] = 72;  // HS ch0 → GPIO 13
    outSel[12] = 73;  // HS ch1 → GPIO 12
    outSel[14] = 80;  // LS ch0 → GPIO 14
    const map = scanOutSel(outSel);

    expect(map.get(0)).toBe(13);
    expect(map.get(1)).toBe(12);
    expect(map.get(8)).toBe(14);
    expect(map.size).toBe(3);
  });

  it('ignores non-LEDC signals (< 72 or > 87)', () => {
    const outSel = new Array(40).fill(256);
    outSel[5] = 71;  // signal 71 = not LEDC
    outSel[6] = 88;  // signal 88 = not LEDC
    outSel[7] = 0;   // signal 0 = GPIO matrix simple
    const map = scanOutSel(outSel);

    expect(map.size).toBe(0);
  });

  it('explains why 0x2000 marker was broken for LEDC signals', () => {
    // QEMU fires: 0x2000 | ((signal & 0xFF) << 8) | (gpio & 0xFF)
    // For signal=72 (0x48), gpio=13: direction = 0x2000 | 0x4800 | 0x0D = 0x680D
    // marker = direction & 0xF000 = 0x6000 ≠ 0x2000 → NEVER MATCHED!
    const signal = 72;
    const gpio = 13;
    const direction = 0x2000 | ((signal & 0xFF) << 8) | (gpio & 0xFF);

    expect(direction).toBe(0x680D);
    expect(direction & 0xF000).toBe(0x6000); // NOT 0x2000!
    expect(direction & 0xF000).not.toBe(0x2000); // confirms the bug
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 12. End-to-end: LEDC update with correct GPIO routes to servo
// ─────────────────────────────────────────────────────────────────────────────

describe('End-to-end: LEDC → servo angle', () => {
  const logic = () => PartSimulationRegistry.get('servo')!;

  it('ledc_update with gpio=13 → updatePwm(13, duty) → servo moves', () => {
    const shim = makeEsp32Shim();
    const el = makeElement() as any;
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-e2e');

    // Simulate what useSimulatorStore.onLedcUpdate does:
    const update = { channel: 0, duty: 7.36, duty_pct: 7.36, gpio: 13 };
    const targetPin = (update.gpio >= 0) ? update.gpio : update.channel;
    const dutyCycleFraction = update.duty_pct / 100;

    // This is what the store calls:
    shim.pinManager.updatePwm(targetPin, dutyCycleFraction);

    // The servo's onPwmChange callback should have been triggered
    const cb = shim._getPwmCallback();
    expect(cb).not.toBeNull();

    // Manually invoke the callback (simulating PinManager dispatching)
    cb!(13, dutyCycleFraction);

    // 7.36% duty = 1472µs pulse → ~90°
    expect(el.angle).toBeGreaterThanOrEqual(88);
    expect(el.angle).toBeLessThanOrEqual(92);
  });

  it('ledc_update with WRONG ch=80 and gpio=-1 would NOT reach servo on pin 13', () => {
    // This demonstrates the bug that was fixed:
    // ch=80 (from broken & 0xFF) with gpio=-1 → updatePwm(80, duty)
    // But servo listens on pin 13 → callback never fires
    const shim = makeEsp32Shim();
    const el = makeElement() as any;
    logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-bug-demo');

    const cb = shim._getPwmCallback();

    // With the bug: updatePwm would be called with pin=80 (wrong)
    // The servo registered on pin 13, so this would NOT trigger it
    // (PinManager only dispatches to callbacks registered for that pin)
    expect(cb).not.toBeNull();

    // Calling with wrong pin does nothing (servo registered on 13, not 80)
    cb!(80, 0.075); // wrong pin
    // angle still 0 since the real PinManager wouldn't route pin 80 to pin 13's callback
    // (In our mock, the callback is directly invoked, but in production it wouldn't fire)
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// 13. LEDC polling — data type and internal config
// ─────────────────────────────────────────────────────────────────────────────

describe('LEDC polling — data format', () => {
  it('duty values from QEMU are floats representing percentages (0-100)', () => {
    // Simulates what the LEDC polling thread reads from QEMU
    // QEMU stores: duty[ch] = 100.0 * raw_duty / (16 * (2^duty_res - 1))
    // For a servo at 50Hz, 13-bit resolution, 1500µs pulse:
    //   raw_duty = 1500/20000 * 8192 = 614.4
    //   duty_pct = 100 * 614.4 / (16 * 8191) ≈ 0.469... but QEMU formula differs

    // What matters: duty is a float percentage
    const dutyPct = 7.5; // 7.5% = 1500µs at 50Hz = ~90°

    // Frontend receives duty_pct, divides by 100
    const dutyCycleFraction = dutyPct / 100; // 0.075

    // Servo maps pulse width:
    const MIN_DC = 544 / 20000;   // 0.0272
    const MAX_DC = 2400 / 20000;  // 0.12
    const angle = Math.round(
      ((dutyCycleFraction - MIN_DC) / (MAX_DC - MIN_DC)) * 180
    );

    // 7.5% duty ≈ 93° (close to 90°)
    expect(angle).toBeGreaterThanOrEqual(88);
    expect(angle).toBeLessThanOrEqual(95);
  });

  it('LEDC internal config ID is 6 (QEMU_INTERNAL_LEDC_CHANNEL_DUTY)', () => {
    // Verifies the constant matches QEMU's definition
    // #define QEMU_INTERNAL_LEDC_CHANNEL_DUTY 6
    const QEMU_INTERNAL_LEDC_CHANNEL_DUTY = 6;
    expect(QEMU_INTERNAL_LEDC_CHANNEL_DUTY).toBe(6);
  });

  it('deduplication: identical duty values are not re-emitted', () => {
    // Simulates the _last_duty tracking in _ledc_poll_thread
    const lastDuty = [0.0, 0.0, 0.0];
    const emitted: { ch: number; duty: number }[] = [];

    function pollOnce(duties: number[]) {
      for (let ch = 0; ch < duties.length; ch++) {
        const duty = duties[ch];
        if (Math.abs(duty - lastDuty[ch]) < 0.01) continue;
        lastDuty[ch] = duty;
        if (duty > 0) emitted.push({ ch, duty });
      }
    }

    // First poll: duty = 7.5 → emitted
    pollOnce([7.5, 0, 0]);
    expect(emitted).toHaveLength(1);
    expect(emitted[0]).toEqual({ ch: 0, duty: 7.5 });

    // Second poll: same duty → NOT emitted (deduplication)
    pollOnce([7.5, 0, 0]);
    expect(emitted).toHaveLength(1); // still 1

    // Third poll: duty changed → emitted
    pollOnce([12.0, 0, 0]);
    expect(emitted).toHaveLength(2);
    expect(emitted[1]).toEqual({ ch: 0, duty: 12.0 });
  });
});
