/**
 * Simulation Parts Tests
 *
 * Tests the component simulation logic registered in PartSimulationRegistry.
 * All DOM elements are mocked as plain objects; no real browser APIs are needed.
 *
 * Covers:
 * - PartSimulationRegistry lookup
 * - LED, Pushbutton, Pushbutton-6mm, Slide-switch, DIP-switch-8
 * - LED-bar-graph, 7segment
 * - RGB LED (digital + PWM)
 * - Potentiometer, Slide-potentiometer, Photoresistor-sensor, Analog-joystick
 * - Servo (OCR register polling via RAF)
 * - Buzzer (Web Audio API mock)
 * - LCD1602 (4-bit mode pin registration)
 */

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

// Side-effect imports — register all parts
import '../simulation/parts/BasicParts';
import '../simulation/parts/ComplexParts';
import '../simulation/parts/ChipParts';
import '../simulation/parts/SensorParts';

// ─── RAF depth-limited mock ───────────────────────────────────────────────────
// Calls the callback once synchronously but prevents re-entrancy so that
// animation loops (servo poll → rAF → poll → rAF → …) don't recurse infinitely.
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());

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

/** Create a mock DOM element with vi.fn() event listeners */
function makeElement(props: Record<string, unknown> = {}): HTMLElement {
  return {
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    ...props,
  } as unknown as HTMLElement;
}

/** Create a mock ADC with 8 channels */
function makeADC() {
  return { channelValues: new Array(8).fill(0) };
}

/** Create a mock simulator whose pinManager is accessible via (sim as any).pinManager */
function makeSimulator(adc?: ReturnType<typeof makeADC> | null) {
  const pinManager = {
    onPinChange:  vi.fn().mockReturnValue(() => {}),
    onPwmChange:  vi.fn().mockReturnValue(() => {}),
    getPwmValue:  vi.fn().mockReturnValue(0),
    updatePwm:    vi.fn(),
    triggerPinChange: vi.fn(),
  };
  return {
    pinManager,
    getADC:      vi.fn().mockReturnValue(adc ?? null),
    setPinState: vi.fn(),
    cpu: { data: new Uint8Array(512).fill(0) },
  };
}

/** Pin helper that always returns the same pin number regardless of pin name */
const pinHelper = (pin: number) => (_name: string): number | null => pin;

/** Pin helper that returns null for every name (no connections) */
const noPins = (_name: string): number | null => null;

/** Multi-pin helper: pass an object mapping pin names to pin numbers */
const pinMap = (map: Record<string, number>) => (name: string): number | null =>
  name in map ? map[name] : null;

// ─── PartSimulationRegistry ───────────────────────────────────────────────────

describe('PartSimulationRegistry — registration', () => {
  const EXPECTED = [
    'pushbutton', 'pushbutton-6mm', 'slide-switch', 'dip-switch-8',
    'led', 'led-bar-graph', '7segment',
    'rgb-led', 'potentiometer', 'slide-potentiometer',
    'photoresistor-sensor', 'analog-joystick',
    'servo', 'buzzer', 'lcd1602', 'lcd2004',
  ];

  it('registers all expected component types', () => {
    for (const id of EXPECTED) {
      expect(PartSimulationRegistry.get(id), `missing: ${id}`).toBeDefined();
    }
  });

  it('returns undefined for an unknown component id', () => {
    expect(PartSimulationRegistry.get('wokwi-banana')).toBeUndefined();
  });

  it('each registered part has onPinStateChange or attachEvents (or both)', () => {
    for (const id of EXPECTED) {
      const logic = PartSimulationRegistry.get(id)!;
      const hasSomething = !!(logic.onPinStateChange || logic.attachEvents);
      expect(hasSomething, `${id} has no simulation logic`).toBe(true);
    }
  });
});

// ─── LED ─────────────────────────────────────────────────────────────────────

describe('LED — onPinStateChange', () => {
  it('sets element.value = true when anode pin goes HIGH', () => {
    const logic = PartSimulationRegistry.get('led')!;
    const el = makeElement({ value: false });
    logic.onPinStateChange!('A', true, el);
    expect((el as any).value).toBe(true);
  });

  it('sets element.value = false when anode pin goes LOW', () => {
    const logic = PartSimulationRegistry.get('led')!;
    const el = makeElement({ value: true });
    logic.onPinStateChange!('A', false, el);
    expect((el as any).value).toBe(false);
  });

  it('ignores non-anode pins (e.g. cathode K)', () => {
    const logic = PartSimulationRegistry.get('led')!;
    const el = makeElement({ value: false });
    logic.onPinStateChange!('K', true, el);
    expect((el as any).value).toBe(false); // unchanged
  });
});

// ─── Pushbutton ──────────────────────────────────────────────────────────────

describe('Pushbutton — attachEvents', () => {
  it('registers button-press and button-release event listeners', () => {
    const logic = PartSimulationRegistry.get('pushbutton')!;
    const el = makeElement({ pressed: false });
    const sim = makeSimulator();
    logic.attachEvents!(el, sim as any, pinHelper(7));

    const events = (el.addEventListener as ReturnType<typeof vi.fn>).mock.calls.map((c: unknown[]) => c[0]);
    expect(events).toContain('button-press');
    expect(events).toContain('button-release');
  });

  it('calls setPinState(pin, false) on button-press (active LOW)', () => {
    const logic = PartSimulationRegistry.get('pushbutton')!;
    const el = makeElement({ pressed: false });
    const sim = makeSimulator();

    let pressHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'button-press') pressHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinHelper(7));
    pressHandler();

    expect(sim.setPinState).toHaveBeenCalledWith(7, false);
    expect((el as any).pressed).toBe(true);
  });

  it('calls setPinState(pin, true) on button-release', () => {
    const logic = PartSimulationRegistry.get('pushbutton')!;
    const el = makeElement({ pressed: true });
    const sim = makeSimulator();

    let releaseHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'button-release') releaseHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinHelper(7));
    releaseHandler();

    expect(sim.setPinState).toHaveBeenCalledWith(7, true);
    expect((el as any).pressed).toBe(false);
  });

  it('cleanup removes both event listeners', () => {
    const logic = PartSimulationRegistry.get('pushbutton')!;
    const el = makeElement();
    const sim = makeSimulator();
    const cleanup = logic.attachEvents!(el, sim as any, pinHelper(2));
    cleanup();
    expect((el.removeEventListener as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThanOrEqual(2);
  });

  it('returns no-op cleanup when no pin is connected', () => {
    const logic = PartSimulationRegistry.get('pushbutton')!;
    const el = makeElement();
    const sim = makeSimulator();
    expect(() => logic.attachEvents!(el, sim as any, noPins)()).not.toThrow();
  });
});

// ─── Pushbutton-6mm ──────────────────────────────────────────────────────────

describe('Pushbutton-6mm — attachEvents', () => {
  it('behaves identically to pushbutton (active LOW on press)', () => {
    const logic = PartSimulationRegistry.get('pushbutton-6mm')!;
    const el = makeElement({ pressed: false });
    const sim = makeSimulator();

    let pressHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'button-press') pressHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinHelper(4));
    pressHandler();
    expect(sim.setPinState).toHaveBeenCalledWith(4, false);
    expect((el as any).pressed).toBe(true);
  });
});

// ─── Slide-switch ─────────────────────────────────────────────────────────────

describe('Slide-switch — attachEvents', () => {
  it('reads initial value=1 and drives pin HIGH immediately', () => {
    const logic = PartSimulationRegistry.get('slide-switch')!;
    const el = makeElement({ value: 1 });
    const sim = makeSimulator();
    logic.attachEvents!(el, sim as any, pinHelper(5));
    expect(sim.setPinState).toHaveBeenCalledWith(5, true);
  });

  it('reads initial value=0 and drives pin LOW immediately', () => {
    const logic = PartSimulationRegistry.get('slide-switch')!;
    const el = makeElement({ value: 0 });
    const sim = makeSimulator();
    logic.attachEvents!(el, sim as any, pinHelper(5));
    expect(sim.setPinState).toHaveBeenCalledWith(5, false);
  });

  it('updates pin state when change event fires', () => {
    const logic = PartSimulationRegistry.get('slide-switch')!;
    const el = makeElement({ value: 0 });
    const sim = makeSimulator();

    let changeHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'change') changeHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinHelper(6));
    (el as any).value = 1;
    changeHandler();

    expect(sim.setPinState).toHaveBeenLastCalledWith(6, true);
  });

  it('returns no-op when no pin is connected', () => {
    const logic = PartSimulationRegistry.get('slide-switch')!;
    const el = makeElement({ value: 0 });
    const sim = makeSimulator();
    expect(() => logic.attachEvents!(el, sim as any, noPins)()).not.toThrow();
  });
});

// ─── DIP-switch-8 ────────────────────────────────────────────────────────────

describe('DIP-switch-8 — attachEvents', () => {
  it('syncs all 8 initial switch states on attach', () => {
    const logic = PartSimulationRegistry.get('dip-switch-8')!;
    const values = [1, 0, 1, 0, 1, 0, 1, 0];
    const el = makeElement({ values });
    const sim = makeSimulator();

    const helper = pinMap({ '1A': 2, '2A': 3, '3A': 4, '4A': 5, '5A': 6, '6A': 7, '7A': 8, '8A': 9 });
    logic.attachEvents!(el, sim as any, helper);

    expect(sim.setPinState.mock.calls.length).toBe(8);
    expect(sim.setPinState).toHaveBeenCalledWith(2, true);  // switch 1 ON
    expect(sim.setPinState).toHaveBeenCalledWith(3, false); // switch 2 OFF
    expect(sim.setPinState).toHaveBeenCalledWith(9, false); // switch 8 OFF
  });

  it('updates pins when change event fires', () => {
    const logic = PartSimulationRegistry.get('dip-switch-8')!;
    const el = makeElement({ values: new Array(8).fill(0) });
    const sim = makeSimulator();

    let changeHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'change') changeHandler = handler;
    });

    const helper = pinMap({ '1A': 2 });
    logic.attachEvents!(el, sim as any, helper);

    (el as any).values = [1, 0, 0, 0, 0, 0, 0, 0];
    changeHandler();
    expect(sim.setPinState).toHaveBeenLastCalledWith(2, true);
  });
});

// ─── LED Bar Graph ────────────────────────────────────────────────────────────

describe('LED-bar-graph — attachEvents', () => {
  it('registers onPinChange for all 10 LED pins (A1-A10)', () => {
    const logic = PartSimulationRegistry.get('led-bar-graph')!;
    const el = makeElement({ values: new Array(10).fill(0) });
    const sim = makeSimulator();

    const helper = (name: string): number | null => {
      const idx = parseInt(name.replace('A', ''));
      return isNaN(idx) || idx < 1 || idx > 10 ? null : idx + 1;
    };

    logic.attachEvents!(el, sim as any, helper);
    expect(sim.pinManager.onPinChange.mock.calls.length).toBe(10);
  });

  it('updates element.values[0] when LED A1 pin goes HIGH', () => {
    const logic = PartSimulationRegistry.get('led-bar-graph')!;
    const el = makeElement({ values: new Array(10).fill(0) });
    const sim = makeSimulator();

    const callbacks: Array<(pin: number, state: boolean) => void> = [];
    sim.pinManager.onPinChange.mockImplementation((_pin: number, cb: (pin: number, state: boolean) => void) => {
      callbacks.push(cb);
      return () => {};
    });

    logic.attachEvents!(el, sim as any, pinMap({ A1: 2 }));
    callbacks[0](2, true);
    expect((el as any).values[0]).toBe(1);
  });

  it('cleanup unsubscribes all 10 listeners', () => {
    const unsubMock = vi.fn();
    const logic = PartSimulationRegistry.get('led-bar-graph')!;
    const el = makeElement({ values: new Array(10).fill(0) });
    const sim = makeSimulator();
    sim.pinManager.onPinChange.mockReturnValue(unsubMock);

    const helper = (name: string): number | null => {
      const idx = parseInt(name.replace('A', ''));
      return isNaN(idx) ? null : idx;
    };

    const cleanup = logic.attachEvents!(el, sim as any, helper);
    cleanup();
    expect(unsubMock).toHaveBeenCalledTimes(10);
  });
});

// ─── 7-Segment Display ───────────────────────────────────────────────────────

describe('7segment — attachEvents', () => {
  it('registers onPinChange for all 8 segments (A B C D E F G DP)', () => {
    const logic = PartSimulationRegistry.get('7segment')!;
    const el = makeElement({ values: new Array(8).fill(0) });
    const sim = makeSimulator();

    const helper = pinMap({ A: 2, B: 3, C: 4, D: 5, E: 6, F: 7, G: 8, DP: 9 });
    logic.attachEvents!(el, sim as any, helper);

    expect(sim.pinManager.onPinChange.mock.calls.length).toBe(8);
  });

  it('updates element.values[0] when segment A pin goes HIGH', () => {
    const logic = PartSimulationRegistry.get('7segment')!;
    const el = makeElement({ values: new Array(8).fill(0) });
    const sim = makeSimulator();

    let segACallback!: (pin: number, state: boolean) => void;
    sim.pinManager.onPinChange.mockImplementation((_pin: number, cb: (pin: number, state: boolean) => void) => {
      if (!segACallback) segACallback = cb;
      return () => {};
    });

    logic.attachEvents!(el, sim as any, pinMap({ A: 2 }));
    segACallback(2, true);
    expect((el as any).values[0]).toBe(1); // 'A' is index 0
  });

  it('cleanup unsubscribes all 8 segment listeners', () => {
    const unsubMock = vi.fn();
    const logic = PartSimulationRegistry.get('7segment')!;
    const el = makeElement({ values: new Array(8).fill(0) });
    const sim = makeSimulator();
    sim.pinManager.onPinChange.mockReturnValue(unsubMock);

    const helper = pinMap({ A: 2, B: 3, C: 4, D: 5, E: 6, F: 7, G: 8, DP: 9 });
    const cleanup = logic.attachEvents!(el, sim as any, helper);
    cleanup();
    expect(unsubMock).toHaveBeenCalledTimes(8);
  });
});

// ─── RGB LED ──────────────────────────────────────────────────────────────────

describe('RGB LED — attachEvents', () => {
  it('subscribes to 3 digital (onPinChange) and 3 PWM (onPwmChange) listeners', () => {
    const logic = PartSimulationRegistry.get('rgb-led')!;
    const el = makeElement({ ledRed: 0, ledGreen: 0, ledBlue: 0 });
    const sim = makeSimulator();

    logic.attachEvents!(el, sim as any, pinMap({ R: 9, G: 10, B: 11 }));

    expect(sim.pinManager.onPinChange.mock.calls.length).toBe(3);
    expect(sim.pinManager.onPwmChange.mock.calls.length).toBe(3);
  });

  it('sets ledRed to 255 on digital HIGH, 0 on LOW', () => {
    const logic = PartSimulationRegistry.get('rgb-led')!;
    const el = makeElement({ ledRed: 0, ledGreen: 0, ledBlue: 0 });
    const sim = makeSimulator();

    const digitalCbs: Record<number, (pin: number, state: boolean) => void> = {};
    sim.pinManager.onPinChange.mockImplementation((pin: number, cb: (pin: number, state: boolean) => void) => {
      digitalCbs[pin] = cb;
      return () => {};
    });
    sim.pinManager.onPwmChange.mockReturnValue(() => {});

    logic.attachEvents!(el, sim as any, pinMap({ R: 9 }));
    digitalCbs[9](9, true);
    expect((el as any).ledRed).toBe(255);
    digitalCbs[9](9, false);
    expect((el as any).ledRed).toBe(0);
  });

  it('sets ledGreen via PWM duty cycle (0.5 → 128)', () => {
    const logic = PartSimulationRegistry.get('rgb-led')!;
    const el = makeElement({ ledRed: 0, ledGreen: 0, ledBlue: 0 });
    const sim = makeSimulator();

    const pwmCbs: Record<number, (pin: number, dc: number) => void> = {};
    sim.pinManager.onPinChange.mockReturnValue(() => {});
    sim.pinManager.onPwmChange.mockImplementation((pin: number, cb: (pin: number, dc: number) => void) => {
      pwmCbs[pin] = cb;
      return () => {};
    });

    logic.attachEvents!(el, sim as any, pinMap({ G: 10 }));
    pwmCbs[10](10, 0.5);
    expect((el as any).ledGreen).toBe(128); // Math.round(0.5 * 255)
  });

  it('sets ledBlue via PWM full brightness (1.0 → 255)', () => {
    const logic = PartSimulationRegistry.get('rgb-led')!;
    const el = makeElement({ ledBlue: 0 });
    const sim = makeSimulator();

    const pwmCbs: Record<number, (pin: number, dc: number) => void> = {};
    sim.pinManager.onPinChange.mockReturnValue(() => {});
    sim.pinManager.onPwmChange.mockImplementation((pin: number, cb: (pin: number, dc: number) => void) => {
      pwmCbs[pin] = cb;
      return () => {};
    });

    logic.attachEvents!(el, sim as any, pinMap({ B: 11 }));
    pwmCbs[11](11, 1.0);
    expect((el as any).ledBlue).toBe(255);
  });

  it('cleanup unsubscribes all 6 listeners', () => {
    const unsubMock = vi.fn();
    const logic = PartSimulationRegistry.get('rgb-led')!;
    const el = makeElement({ ledRed: 0, ledGreen: 0, ledBlue: 0 });
    const sim = makeSimulator();
    sim.pinManager.onPinChange.mockReturnValue(unsubMock);
    sim.pinManager.onPwmChange.mockReturnValue(unsubMock);

    const cleanup = logic.attachEvents!(el, sim as any, pinMap({ R: 9, G: 10, B: 11 }));
    cleanup();
    expect(unsubMock).toHaveBeenCalledTimes(6);
  });
});

// ─── Potentiometer ───────────────────────────────────────────────────────────

describe('Potentiometer — attachEvents', () => {
  it('injects ADC voltage on input event (512/1023 * 5V ≈ 2.5V)', () => {
    const logic = PartSimulationRegistry.get('potentiometer')!;
    const adc = makeADC();
    const el = makeElement({ value: '512' });
    const sim = makeSimulator(adc);

    let inputHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'input') inputHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ SIG: 14 })); // A0 = pin 14
    inputHandler();

    expect(adc.channelValues[0]).toBeCloseTo(2.5, 1); // channel 0 = A0
  });

  it('injects 0V when value is 0', () => {
    const logic = PartSimulationRegistry.get('potentiometer')!;
    const adc = makeADC();
    const el = makeElement({ value: '0' });
    const sim = makeSimulator(adc);

    let inputHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'input') inputHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ SIG: 14 }));
    inputHandler();

    expect(adc.channelValues[0]).toBeCloseTo(0, 5);
  });

  it('injects 5V when value is 1023', () => {
    const logic = PartSimulationRegistry.get('potentiometer')!;
    const adc = makeADC();
    const el = makeElement({ value: '1023' });
    const sim = makeSimulator(adc);

    let inputHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'input') inputHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ SIG: 14 }));
    inputHandler();

    expect(adc.channelValues[0]).toBeCloseTo(5.0, 2);
  });

  it('returns no-op when SIG pin is not connected', () => {
    const logic = PartSimulationRegistry.get('potentiometer')!;
    const el = makeElement({ value: '100' });
    const sim = makeSimulator(makeADC());
    expect(() => logic.attachEvents!(el, sim as any, noPins)()).not.toThrow();
  });
});

// ─── Slide Potentiometer ─────────────────────────────────────────────────────

describe('Slide-potentiometer — attachEvents', () => {
  it('injects mid-range voltage on input event', () => {
    const logic = PartSimulationRegistry.get('slide-potentiometer')!;
    const adc = makeADC();
    const el = makeElement({ value: 512, min: 0, max: 1023 });
    const sim = makeSimulator(adc);

    let inputHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'input') inputHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ SIG: 14 }));
    inputHandler();

    expect(adc.channelValues[0]).toBeCloseTo(2.5, 1);
  });
});

// ─── Photoresistor Sensor ────────────────────────────────────────────────────

describe('Photoresistor-sensor — attachEvents', () => {
  it('injects initial 2.5V (moderate light) on the AO pin immediately', () => {
    const logic = PartSimulationRegistry.get('photoresistor-sensor')!;
    const adc = makeADC();
    const el = makeElement();
    const sim = makeSimulator(adc);

    logic.attachEvents!(el, sim as any, pinMap({ AO: 14 }));
    expect(adc.channelValues[0]).toBeCloseTo(2.5, 1);
  });

  it('updates ADC voltage to 5V on input event (full light)', () => {
    const logic = PartSimulationRegistry.get('photoresistor-sensor')!;
    const adc = makeADC();
    const el = makeElement({ value: 1023 });
    const sim = makeSimulator(adc);

    let inputHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'input') inputHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ AO: 14 }));
    inputHandler();
    expect(adc.channelValues[0]).toBeCloseTo(5.0, 1);
  });

  it('registers DO indicator listener when digital output pin is connected', () => {
    const logic = PartSimulationRegistry.get('photoresistor-sensor')!;
    const el = makeElement({ ledDO: false });
    const sim = makeSimulator(makeADC());

    logic.attachEvents!(el, sim as any, pinMap({ AO: 14, DO: 4 }));
    expect(sim.pinManager.onPinChange.mock.calls.length).toBeGreaterThanOrEqual(1);
  });
});

// ─── Analog Joystick ─────────────────────────────────────────────────────────

describe('Analog-joystick — attachEvents', () => {
  it('injects 2.5V on both X and Y axes initially (center)', () => {
    const logic = PartSimulationRegistry.get('analog-joystick')!;
    const adc = makeADC();
    const el = makeElement();
    const sim = makeSimulator(adc);

    logic.attachEvents!(el, sim as any, pinMap({ VRX: 14, VRY: 15 }));

    expect(adc.channelValues[0]).toBeCloseTo(2.5, 1); // A0 = X
    expect(adc.channelValues[1]).toBeCloseTo(2.5, 1); // A1 = Y
  });

  it('calls setPinState(pin, false) on button-press (active LOW)', () => {
    const logic = PartSimulationRegistry.get('analog-joystick')!;
    const el = makeElement({ pressed: false });
    const sim = makeSimulator(makeADC());

    let pressHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'button-press') pressHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ SW: 2 }));
    pressHandler();
    expect(sim.setPinState).toHaveBeenCalledWith(2, false);
    expect((el as any).pressed).toBe(true);
  });

  it('calls setPinState(pin, true) on button-release', () => {
    const logic = PartSimulationRegistry.get('analog-joystick')!;
    const el = makeElement({ pressed: true });
    const sim = makeSimulator(makeADC());

    let releaseHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'button-release') releaseHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ SW: 2 }));
    releaseHandler();
    expect(sim.setPinState).toHaveBeenCalledWith(2, true);
    expect((el as any).pressed).toBe(false);
  });

  it('updates ADC voltages on joystick-move event', () => {
    const logic = PartSimulationRegistry.get('analog-joystick')!;
    const adc = makeADC();
    const el = makeElement({ xValue: 0, yValue: 1023 });
    const sim = makeSimulator(adc);

    let moveHandler!: () => void;
    (el.addEventListener as ReturnType<typeof vi.fn>).mockImplementation((event: string, handler: () => void) => {
      if (event === 'joystick-move') moveHandler = handler;
    });

    logic.attachEvents!(el, sim as any, pinMap({ VRX: 14, VRY: 15 }));
    moveHandler();

    expect(adc.channelValues[0]).toBeCloseTo(0.0, 1); // X at min = 0V
    expect(adc.channelValues[1]).toBeCloseTo(5.0, 1); // Y at max = 5V
  });
});

// ─── Servo ───────────────────────────────────────────────────────────────────

describe('Servo — attachEvents', () => {
  it('starts polling via requestAnimationFrame immediately', () => {
    const logic = PartSimulationRegistry.get('servo')!;
    const el = makeElement({ angle: 0 });
    const sim = makeSimulator();
    const rafSpy = vi.spyOn(globalThis, 'requestAnimationFrame');

    logic.attachEvents!(el, sim as any, noPins);
    expect(rafSpy).toHaveBeenCalled();
  });

  it('calculates 90° when OCR1A = ICR1/2 (servo midpoint)', () => {
    // ICR1 = 20000 (50 Hz at prescaler 8, 16 MHz)
    // OCR1A = 10000 → pulseUs = 1000 + (10000/20000)*1000 = 1500 µs → 90°
    const logic = PartSimulationRegistry.get('servo')!;
    const el = makeElement({ angle: -1 });
    const sim = makeSimulator();

    // ICR1L=0x86, ICR1H=0x87; OCR1AL=0x88, OCR1AH=0x89
    sim.cpu.data[0x88] = 10000 & 0xFF;        // OCR1AL
    sim.cpu.data[0x89] = (10000 >> 8) & 0xFF; // OCR1AH
    sim.cpu.data[0x86] = 20000 & 0xFF;        // ICR1L
    sim.cpu.data[0x87] = (20000 >> 8) & 0xFF; // ICR1H

    logic.attachEvents!(el, sim as any, noPins);
    expect((el as any).angle).toBe(90);
  });

  it('calculates 0° when OCR1A = 0 (minimum pulse)', () => {
    // OCR1A=0, ICR1=20000 → pulseUs = 1000 + 0 = 1000 µs → 0°
    const logic = PartSimulationRegistry.get('servo')!;
    const el = makeElement({ angle: -1 });
    const sim = makeSimulator();

    sim.cpu.data[0x86] = 20000 & 0xFF;
    sim.cpu.data[0x87] = (20000 >> 8) & 0xFF;
    // OCR1A = 0 (default)

    logic.attachEvents!(el, sim as any, noPins);
    expect((el as any).angle).toBe(0);
  });

  it('calculates 180° when OCR1A = ICR1 (maximum pulse)', () => {
    // OCR1A=20000, ICR1=20000 → pulseUs = 1000 + 1000 = 2000 µs → 180°
    const logic = PartSimulationRegistry.get('servo')!;
    const el = makeElement({ angle: -1 });
    const sim = makeSimulator();

    sim.cpu.data[0x88] = 20000 & 0xFF;
    sim.cpu.data[0x89] = (20000 >> 8) & 0xFF;
    sim.cpu.data[0x86] = 20000 & 0xFF;
    sim.cpu.data[0x87] = (20000 >> 8) & 0xFF;

    logic.attachEvents!(el, sim as any, noPins);
    expect((el as any).angle).toBe(180);
  });

  it('cleanup cancels the RAF loop', () => {
    const logic = PartSimulationRegistry.get('servo')!;
    const el = makeElement({ angle: 0 });
    const sim = makeSimulator();
    const cancelSpy = vi.spyOn(globalThis, 'cancelAnimationFrame');

    const cleanup = logic.attachEvents!(el, sim as any, noPins);
    cleanup();
    expect(cancelSpy).toHaveBeenCalled();
  });
});

// ─── Buzzer ──────────────────────────────────────────────────────────────────

describe('Buzzer — attachEvents', () => {
  // Mock Web Audio API before each test in this suite.
  // Must use a real function/class (not arrow) so `new AudioContext()` works.
  beforeEach(() => {
    const mockOscillator = {
      type: 'square',
      frequency: { value: 440, setTargetAtTime: vi.fn() },
      connect: vi.fn(),
      start: vi.fn(),
      stop: vi.fn(),
      disconnect: vi.fn(),
    };
    const mockGain = {
      gain: { value: 0.1 },
      connect: vi.fn(),
    };
    function MockAudioContext(this: any) {
      this.createOscillator = vi.fn().mockReturnValue(mockOscillator);
      this.createGain       = vi.fn().mockReturnValue(mockGain);
      this.destination      = {};
      this.currentTime      = 0;
      this.close            = vi.fn().mockResolvedValue(undefined);
    }
    vi.stubGlobal('AudioContext', MockAudioContext);
  });

  it('subscribes to onPwmChange and onPinChange for the buzzer pin', () => {
    const logic = PartSimulationRegistry.get('buzzer')!;
    const el = makeElement({ playing: false });
    const sim = makeSimulator();

    logic.attachEvents!(el, sim as any, pinMap({ '1': 3 }));

    expect(sim.pinManager.onPwmChange.mock.calls.length).toBeGreaterThanOrEqual(1);
    expect(sim.pinManager.onPinChange.mock.calls.length).toBeGreaterThanOrEqual(1);
  });

  it('creates AudioContext when PWM duty cycle goes above 0', () => {
    const logic = PartSimulationRegistry.get('buzzer')!;
    const el = makeElement({ playing: false });
    const sim = makeSimulator();

    let pwmCallback!: (pin: number, dc: number) => void;
    sim.pinManager.onPwmChange.mockImplementation((_pin: number, cb: (pin: number, dc: number) => void) => {
      pwmCallback = cb;
      return () => {};
    });
    sim.pinManager.onPinChange.mockReturnValue(() => {});

    // Track instantiation via a flag (AudioContext is a regular function, not a spy)
    let audioCtxCreated = false;
    const PrevMock = (globalThis as any).AudioContext;
    function TrackingCtx(this: any) { audioCtxCreated = true; PrevMock.call(this); }
    TrackingCtx.prototype = Object.create(PrevMock.prototype);
    vi.stubGlobal('AudioContext', TrackingCtx);

    logic.attachEvents!(el, sim as any, pinMap({ '1': 3 }));
    pwmCallback(3, 0.5); // 50% duty cycle → should start tone

    expect(audioCtxCreated).toBe(true);
  });

  it('cleanup calls unsubscribers for both PWM and digital listeners', () => {
    const unsubMock = vi.fn();
    const logic = PartSimulationRegistry.get('buzzer')!;
    const el = makeElement({ playing: false });
    const sim = makeSimulator();
    sim.pinManager.onPwmChange.mockReturnValue(unsubMock);
    sim.pinManager.onPinChange.mockReturnValue(unsubMock);

    const cleanup = logic.attachEvents!(el, sim as any, pinMap({ '1': 3 }));
    cleanup();
    expect(unsubMock).toHaveBeenCalledTimes(2);
  });

  it('returns no-op cleanup when no pin is connected', () => {
    const logic = PartSimulationRegistry.get('buzzer')!;
    const el = makeElement({ playing: false });
    const sim = makeSimulator();
    expect(() => logic.attachEvents!(el, sim as any, noPins)()).not.toThrow();
  });
});

// ─── LCD1602 ─────────────────────────────────────────────────────────────────

describe('LCD1602 — attachEvents', () => {
  const LCD_PINS = pinMap({ RS: 2, E: 3, D4: 4, D5: 5, D6: 6, D7: 7 });

  it('registers onPinChange for RS, E, D4-D7 (6 pins total)', () => {
    const logic = PartSimulationRegistry.get('lcd1602')!;
    const el = makeElement({ characters: null, cursor: false, blink: false });
    const sim = makeSimulator();

    logic.attachEvents!(el, sim as any, LCD_PINS);
    expect(sim.pinManager.onPinChange.mock.calls.length).toBe(6);
  });

  it('cleanup unsubscribes all 6 pin listeners', () => {
    const unsubMock = vi.fn();
    const logic = PartSimulationRegistry.get('lcd1602')!;
    const el = makeElement({ characters: null });
    const sim = makeSimulator();
    sim.pinManager.onPinChange.mockReturnValue(unsubMock);

    const cleanup = logic.attachEvents!(el, sim as any, LCD_PINS);
    cleanup();
    expect(unsubMock).toHaveBeenCalledTimes(6);
  });

  it('returns no-op cleanup when pinManager is unavailable', () => {
    const logic = PartSimulationRegistry.get('lcd1602')!;
    const el = makeElement({ characters: null });
    // Simulator without pinManager
    const sim = { getADC: vi.fn(), setPinState: vi.fn(), cpu: { data: new Uint8Array(512) } };
    expect(() => logic.attachEvents!(el, sim as any, LCD_PINS)()).not.toThrow();
  });
});

// ─── LCD2004 ─────────────────────────────────────────────────────────────────

describe('LCD2004 — attachEvents', () => {
  it('registers 6 pin listeners (same pinout as LCD1602)', () => {
    const logic = PartSimulationRegistry.get('lcd2004')!;
    const el = makeElement({ characters: null });
    const sim = makeSimulator();

    const pins = pinMap({ RS: 2, E: 3, D4: 4, D5: 5, D6: 6, D7: 7 });
    logic.attachEvents!(el, sim as any, pins);
    expect(sim.pinManager.onPinChange.mock.calls.length).toBe(6);
  });
});
