/**
 * protocol-parts.test.ts
 *
 * Tests for the 8 "protocol" components that use I2C, SPI, single-wire, or
 * custom serial communication:
 *
 *   ssd1306       — I2C OLED (VirtualSSD1306)
 *   ds1307        — I2C RTC  (VirtualDS1307)
 *   mpu6050       — I2C IMU  (VirtualMPU6050)
 *   dht22         — single-wire temp/humidity
 *   hx711         — 2-wire load-cell ADC
 *   ir-receiver   — click → NEC pulse on OUT pin
 *   ir-remote     — button-press → ir-signal event
 *   microsd-card  — SPI SD init handshake
 */

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

// ─── Globals ──────────────────────────────────────────────────────────────────

beforeEach(() => {
  vi.useFakeTimers();
  vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => 1);
  vi.stubGlobal('cancelAnimationFrame',  vi.fn());
});

afterEach(() => {
  vi.useRealTimers();
  vi.unstubAllGlobals();
});

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

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

function makeI2CSim() {
  return {
    addI2CDevice:    vi.fn(),
    i2cBus:          { removeDevice: vi.fn() },
    removeI2CDevice: vi.fn(),
    setPinState:     vi.fn(),
    pinManager: {
      onPinChange: vi.fn().mockReturnValue(() => {}),
    },
    spi: null,
    cpu: { data: new Uint8Array(512).fill(0), cycles: 0 },
  };
}

function makePinSim() {
  return {
    pinManager: {
      onPinChange: vi.fn().mockReturnValue(() => {}),
    },
    setPinState: vi.fn(),
    addI2CDevice:    vi.fn(),
    i2cBus:          { removeDevice: vi.fn() },
    spi: null,
    cpu: { data: new Uint8Array(512).fill(0), cycles: 0 },
  };
}

function makeSPISim() {
  const spi = {
    onTransmit:       null as ((b: number) => void) | null,
    completeTransmit: vi.fn(),
  };
  return {
    spi,
    pinManager: { onPinChange: vi.fn().mockReturnValue(() => {}) },
    setPinState: vi.fn(),
    addI2CDevice: vi.fn(),
    i2cBus:       { removeDevice: vi.fn() },
    cpu: { data: new Uint8Array(512).fill(0), cycles: 0 },
  };
}

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

const noPins = (_name: string): number | null => null;

// ─── Registration ─────────────────────────────────────────────────────────────

describe('Protocol parts — registration', () => {
  const IDS = [
    'ssd1306', 'ds1307', 'mpu6050',
    'dht22', 'hx711',
    'ir-receiver', 'ir-remote',
    'microsd-card',
  ];

  it('registers all 8 protocol components', () => {
    for (const id of IDS) {
      expect(PartSimulationRegistry.get(id), `missing: ${id}`).toBeDefined();
    }
  });
});

// ─── ssd1306 ──────────────────────────────────────────────────────────────────

describe('ssd1306 — I2C device', () => {
  it('calls addI2CDevice with address 0x3C', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('ssd1306')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    expect(sim.addI2CDevice).toHaveBeenCalledOnce();
    const device = sim.addI2CDevice.mock.calls[0][0];
    expect(device.address).toBe(0x3C);
  });

  it('cleanup calls removeDevice on i2cBus', () => {
    const sim     = makeI2CSim();
    const logic   = PartSimulationRegistry.get('ssd1306')!;
    const cleanup = logic.attachEvents!(makeElement(), sim as any, noPins);
    cleanup();
    expect(sim.i2cBus.removeDevice).toHaveBeenCalledWith(0x3C);
  });

  it('decodes horizontal addressing: write data bytes into buffer correctly', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('ssd1306')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    const device = sim.addI2CDevice.mock.calls[0][0];
    // Set column address 0–127, page address 0–7 (via commands)
    device.writeByte(0x00);          // control: command stream
    device.writeByte(0x21);          // cmd: set column address
    device.writeByte(0x00);          // col start = 0
    device.writeByte(0x7F);          // col end   = 127
    device.writeByte(0x22);          // cmd: set page address
    device.writeByte(0x00);          // page start = 0
    device.writeByte(0x07);          // page end   = 7
    device.stop(); // flushes command state

    device.writeByte(0x40);          // control: data stream
    device.writeByte(0xAB);          // column 0 of page 0 = 0xAB
    device.stop();

    expect(device.buffer[0]).toBe(0xAB);
  });

  it('readByte returns 0xFF (read not supported)', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('ssd1306')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    const device = sim.addI2CDevice.mock.calls[0][0];
    expect(device.readByte()).toBe(0xFF);
  });

  it('no-op when simulator has no addI2CDevice', () => {
    const sim   = { ...makeI2CSim(), addI2CDevice: undefined };
    const logic = PartSimulationRegistry.get('ssd1306')!;
    expect(() => {
      const c = logic.attachEvents!(makeElement(), sim as any, noPins);
      c();
    }).not.toThrow();
  });
});

// ─── ds1307 ───────────────────────────────────────────────────────────────────

describe('ds1307 — I2C RTC', () => {
  it('calls addI2CDevice with address 0x68', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('ds1307')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    const dev   = sim.addI2CDevice.mock.calls[0][0];
    expect(dev.address).toBe(0x68);
  });

  it('readByte returns valid BCD for seconds (register 0)', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('ds1307')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    const dev = sim.addI2CDevice.mock.calls[0][0];
    dev.writeByte(0x00);   // set register pointer to 0 (seconds)
    const seconds = dev.readByte();
    // BCD: upper nibble = tens digit, lower nibble = units digit
    const tens  = (seconds >> 4) & 0xF;
    const units =  seconds       & 0xF;
    expect(tens).toBeLessThanOrEqual(5);
    expect(units).toBeLessThanOrEqual(9);
  });

  it('cleanup removes device from i2cBus', () => {
    const sim     = makeI2CSim();
    const logic   = PartSimulationRegistry.get('ds1307')!;
    const cleanup = logic.attachEvents!(makeElement(), sim as any, noPins);
    cleanup();
    expect(sim.i2cBus.removeDevice).toHaveBeenCalledWith(0x68);
  });
});

// ─── mpu6050 ──────────────────────────────────────────────────────────────────

describe('mpu6050 — I2C IMU', () => {
  it('calls addI2CDevice with address 0x68 (AD0=0)', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('mpu6050')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    const dev = sim.addI2CDevice.mock.calls[0][0];
    expect(dev.address).toBe(0x68);
  });

  it('uses address 0x69 when element.ad0 is true', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('mpu6050')!;
    logic.attachEvents!(makeElement({ ad0: true }), sim as any, noPins);
    const dev = sim.addI2CDevice.mock.calls[0][0];
    expect(dev.address).toBe(0x69);
  });

  it('WHO_AM_I register (0x75) returns 0x68', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('mpu6050')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    const dev = sim.addI2CDevice.mock.calls[0][0];
    dev.writeByte(0x75);            // set register pointer
    expect(dev.readByte()).toBe(0x68);
  });

  it('ACCEL_ZOUT reports +1g (0x40, 0x00)', () => {
    const sim   = makeI2CSim();
    const logic = PartSimulationRegistry.get('mpu6050')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    const dev = sim.addI2CDevice.mock.calls[0][0];
    dev.writeByte(0x3F);            // ACCEL_ZOUT_H
    expect(dev.readByte()).toBe(0x40);
    expect(dev.readByte()).toBe(0x00);
  });

  it('cleanup removes device', () => {
    const sim     = makeI2CSim();
    const logic   = PartSimulationRegistry.get('mpu6050')!;
    const cleanup = logic.attachEvents!(makeElement(), sim as any, noPins);
    cleanup();
    expect(sim.i2cBus.removeDevice).toHaveBeenCalledWith(0x68);
  });
});

// ─── dht22 ───────────────────────────────────────────────────────────────────

describe('dht22 — single-wire sensor', () => {
  it('sets DATA pin HIGH (idle) on attach', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('dht22')!;
    logic.attachEvents!(makeElement(), sim as any, pinMap({ DATA: 7 }));
    expect(sim.setPinState).toHaveBeenCalledWith(7, true);
  });

  it('registers onPinChange for DATA pin', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('dht22')!;
    logic.attachEvents!(makeElement(), sim as any, pinMap({ DATA: 7 }));
    expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(7, expect.any(Function));
  });

  it('drives DATA in response after LOW → HIGH start sequence', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('dht22')!;
    logic.attachEvents!(makeElement({ temperature: 25.0, humidity: 50.0 }), sim as any, pinMap({ DATA: 7 }));

    const cb = sim.pinManager.onPinChange.mock.calls[0][1] as (pin: number, state: boolean) => void;
    sim.setPinState.mockClear();

    cb(7, false); // MCU pulls DATA LOW  (start signal)
    cb(7, true);  // MCU releases DATA HIGH → DHT22 should begin response

    // Response should include at least one LOW pulse
    const calls = (sim.setPinState as ReturnType<typeof vi.fn>).mock.calls;
    expect(calls.some(([, s]) => s === false)).toBe(true);
  });

  it('no-op if DATA pin not found', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('dht22')!;
    expect(() => {
      const c = logic.attachEvents!(makeElement(), sim as any, noPins);
      c();
    }).not.toThrow();
    expect(sim.pinManager.onPinChange).not.toHaveBeenCalled();
  });

  it('cleanup drives DATA HIGH', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('dht22')!;
    const cleanup = logic.attachEvents!(makeElement(), sim as any, pinMap({ DATA: 7 }));
    sim.setPinState.mockClear();
    cleanup();
    expect(sim.setPinState).toHaveBeenCalledWith(7, true);
  });
});

// ─── hx711 ───────────────────────────────────────────────────────────────────

describe('hx711 — load cell amplifier', () => {
  it('drives DOUT LOW (ready) on attach', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('hx711')!;
    logic.attachEvents!(makeElement(), sim as any, pinMap({ SCK: 2, DOUT: 3 }));
    expect(sim.setPinState).toHaveBeenCalledWith(3, false);
  });

  it('registers onPinChange for SCK pin', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('hx711')!;
    logic.attachEvents!(makeElement(), sim as any, pinMap({ SCK: 2, DOUT: 3 }));
    expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(2, expect.any(Function));
  });

  it('outputs 24 bits on 24 rising SCK edges (MSB first)', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('hx711')!;
    // weight=100 → raw = 100000 = 0x0186A0
    logic.attachEvents!(makeElement({ weight: 100 }), sim as any, pinMap({ SCK: 2, DOUT: 3 }));
    const cb = sim.pinManager.onPinChange.mock.calls[0][1] as (p: number, s: boolean) => void;
    sim.setPinState.mockClear();

    // 24 rising edges
    const doutValues: boolean[] = [];
    for (let i = 0; i < 24; i++) {
      cb(2, true);  // rising
      const last = (sim.setPinState as ReturnType<typeof vi.fn>).mock.lastCall;
      if (last && last[0] === 3) doutValues.push(last[1]);
      cb(2, false); // falling
    }
    expect(doutValues).toHaveLength(24);

    // Reconstruct 24-bit value
    const reconstructed = doutValues.reduce((acc, bit, i) => acc | ((bit ? 1 : 0) << (23 - i)), 0);
    const expected       = (100 * 1000) & 0xFF_FFFF; // = 100000 = 0x0186A0
    expect(reconstructed).toBe(expected);
  });

  it('drives DOUT HIGH after 25 rising edges (gain select done)', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('hx711')!;
    logic.attachEvents!(makeElement(), sim as any, pinMap({ SCK: 2, DOUT: 3 }));
    const cb = sim.pinManager.onPinChange.mock.calls[0][1] as (p: number, s: boolean) => void;
    // 24 data bits + 1 gain pulse
    for (let i = 0; i < 25; i++) { cb(2, true); cb(2, false); }
    // After 25th rising, DOUT goes HIGH
    const highCalls = (sim.setPinState as ReturnType<typeof vi.fn>).mock.calls
      .filter(([pin, state]) => pin === 3 && state === true);
    expect(highCalls.length).toBeGreaterThanOrEqual(1);
  });

  it('no-op if SCK or DOUT not connected', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('hx711')!;
    expect(() => {
      const c = logic.attachEvents!(makeElement(), sim as any, noPins);
      c();
    }).not.toThrow();
    expect(sim.pinManager.onPinChange).not.toHaveBeenCalled();
  });

  it('cleanup drives DOUT HIGH', () => {
    const sim     = makePinSim();
    const logic   = PartSimulationRegistry.get('hx711')!;
    const cleanup = logic.attachEvents!(makeElement(), sim as any, pinMap({ SCK: 2, DOUT: 3 }));
    sim.setPinState.mockClear();
    cleanup();
    expect(sim.setPinState).toHaveBeenCalledWith(3, true);
  });
});

// ─── ir-receiver ─────────────────────────────────────────────────────────────

describe('ir-receiver — NEC click simulation', () => {
  it('sets OUT pin HIGH (idle) on attach', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('ir-receiver')!;
    logic.attachEvents!(makeElement(), sim as any, pinMap({ OUT: 5 }));
    expect(sim.setPinState).toHaveBeenCalledWith(5, true);
  });

  it('registers a click listener on the element', () => {
    const sim   = makePinSim();
    const el    = makeElement();
    const logic = PartSimulationRegistry.get('ir-receiver')!;
    logic.attachEvents!(el, sim as any, pinMap({ OUT: 5 }));
    expect(el.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));
  });

  it('click drives OUT LOW (IR burst start) and later HIGH via setTimeout', () => {
    const sim   = makePinSim();
    const el    = makeElement();
    const logic = PartSimulationRegistry.get('ir-receiver')!;
    logic.attachEvents!(el, sim as any, pinMap({ OUT: 5 }));

    const clickCb = (el.addEventListener as ReturnType<typeof vi.fn>).mock.calls.find(
      ([event]) => event === 'click',
    )?.[1] as (() => void) | undefined;
    expect(clickCb).toBeDefined();

    sim.setPinState.mockClear();
    clickCb!();

    // First call should drive pin LOW (beginning of 9 ms preamble burst)
    const firstLow = (sim.setPinState as ReturnType<typeof vi.fn>).mock.calls[0];
    expect(firstLow).toEqual([5, false]);
  });

  it('NEC sequence produces more than 60 setPinState calls (35+ transitions)', () => {
    const sim   = makePinSim();
    const el    = makeElement();
    const logic = PartSimulationRegistry.get('ir-receiver')!;
    logic.attachEvents!(el, sim as any, pinMap({ OUT: 5 }));

    const clickCb = (el.addEventListener as ReturnType<typeof vi.fn>).mock.calls.find(
      ([ev]) => ev === 'click',
    )?.[1] as () => void;

    sim.setPinState.mockClear();
    clickCb();
    // Run all queued timeouts to exhaust the chain
    vi.runAllTimers();

    expect((sim.setPinState as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThan(60);
  });

  it('cleanup removes click listener and sets pin HIGH', () => {
    const sim     = makePinSim();
    const el      = makeElement();
    const logic   = PartSimulationRegistry.get('ir-receiver')!;
    const cleanup = logic.attachEvents!(el, sim as any, pinMap({ OUT: 5 }));
    sim.setPinState.mockClear();
    cleanup();
    expect(el.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function));
    expect(sim.setPinState).toHaveBeenCalledWith(5, true);
  });

  it('no-op when no pin connected (no throw)', () => {
    const sim   = makePinSim();
    const logic = PartSimulationRegistry.get('ir-receiver')!;
    expect(() => {
      const c = logic.attachEvents!(makeElement(), sim as any, noPins);
      c();
    }).not.toThrow();
  });
});

// ─── ir-remote ───────────────────────────────────────────────────────────────

describe('ir-remote — button dispatch', () => {
  it('registers button-press listener on element', () => {
    const sim   = makePinSim();
    const el    = makeElement();
    const logic = PartSimulationRegistry.get('ir-remote')!;
    logic.attachEvents!(el, sim as any, noPins);
    const events = (el.addEventListener as ReturnType<typeof vi.fn>).mock.calls.map(([e]) => e);
    expect(events).toContain('button-press');
  });

  it('button-press fires ir-signal CustomEvent with address and command', () => {
    const sim   = makePinSim();
    const el    = makeElement();
    const logic = PartSimulationRegistry.get('ir-remote')!;
    logic.attachEvents!(el, sim as any, noPins);

    const onButtonPress = (el.addEventListener as ReturnType<typeof vi.fn>).mock.calls.find(
      ([ev]) => ev === 'button-press',
    )?.[1] as ((e: Event) => void) | undefined;
    expect(onButtonPress).toBeDefined();

    const fakeEvent = new CustomEvent('button-press', { detail: { key: 'power' } });
    onButtonPress!(fakeEvent);

    expect(el.dispatchEvent).toHaveBeenCalledWith(
      expect.objectContaining({ type: 'ir-signal' }),
    );
    const dispatched = (el.dispatchEvent as ReturnType<typeof vi.fn>).mock.calls[0][0] as CustomEvent;
    expect(dispatched.detail.command).toBe(0x45); // POWER key
  });

  it('drives IR pin if connected', () => {
    const sim   = makePinSim();
    const el    = makeElement();
    const logic = PartSimulationRegistry.get('ir-remote')!;
    logic.attachEvents!(el, sim as any, pinMap({ IR: 4 }));

    const onButtonPress = (el.addEventListener as ReturnType<typeof vi.fn>).mock.calls.find(
      ([ev]) => ev === 'button-press',
    )?.[1] as (e: Event) => void;

    sim.setPinState.mockClear();
    onButtonPress(new CustomEvent('button-press', { detail: { key: 'power' } }));

    // Should start the NEC pulse sequence (first edge LOW)
    expect(sim.setPinState).toHaveBeenCalledWith(4, false);
  });

  it('cleanup removes all listeners', () => {
    const sim     = makePinSim();
    const el      = makeElement();
    const logic   = PartSimulationRegistry.get('ir-remote')!;
    const cleanup = logic.attachEvents!(el, sim as any, noPins);
    cleanup();
    expect(el.removeEventListener).toHaveBeenCalledTimes(2);
  });
});

// ─── microsd-card ─────────────────────────────────────────────────────────────

describe('microsd-card — SPI init handshake', () => {
  it('hooks into simulator.spi.onTransmit', () => {
    const sim   = makeSPISim();
    const logic = PartSimulationRegistry.get('microsd-card')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);
    expect(sim.spi.onTransmit).toBeTypeOf('function');
  });

  it('CMD0 (0x40 + 4 zeroes + CRC) returns R1=0x01 (idle)', () => {
    const sim   = makeSPISim();
    const logic = PartSimulationRegistry.get('microsd-card')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);

    const tx = sim.spi.onTransmit as (b: number) => void;
    // Send CMD0: [0x40, 0x00, 0x00, 0x00, 0x00, 0x95]
    [0x40, 0x00, 0x00, 0x00, 0x00, 0x95].forEach(b => tx(b));
    // Poll with 0xFF to receive response
    tx(0xFF);
    const replies = (sim.spi.completeTransmit as ReturnType<typeof vi.fn>).mock.calls.map(([v]) => v);
    expect(replies).toContain(0x01);
  });

  it('CMD8 returns R7 with echo-back 0x1AA', () => {
    const sim   = makeSPISim();
    const logic = PartSimulationRegistry.get('microsd-card')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);

    const tx = sim.spi.onTransmit as (b: number) => void;
    // CMD8: [0x48, 0x00, 0x00, 0x01, 0xAA, 0x87]
    [0x48, 0x00, 0x00, 0x01, 0xAA, 0x87].forEach(b => tx(b));
    // Read 5 bytes of R7 response
    for (let i = 0; i < 5; i++) tx(0xFF);
    const replies = (sim.spi.completeTransmit as ReturnType<typeof vi.fn>).mock.calls.map(([v]) => v);
    // R7 = 0x01, 0x00, 0x00, 0x01, 0xAA
    expect(replies).toContain(0x01);
    expect(replies).toContain(0xAA);
  });

  it('ACMD41 (CMD55 + CMD41) returns R1=0x00 (ready)', () => {
    const sim   = makeSPISim();
    const logic = PartSimulationRegistry.get('microsd-card')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);

    const tx = sim.spi.onTransmit as (b: number) => void;
    [0x77, 0x00, 0x00, 0x00, 0x00, 0x65].forEach(b => tx(b)); // CMD55
    tx(0xFF); // poll
    sim.spi.completeTransmit.mockClear();
    [0x69, 0x40, 0x00, 0x00, 0x00, 0x77].forEach(b => tx(b)); // ACMD41
    tx(0xFF);
    const replies = (sim.spi.completeTransmit as ReturnType<typeof vi.fn>).mock.calls.map(([v]) => v);
    expect(replies).toContain(0x00);
  });

  it('0xFF clock bytes return 0xFF (idle) when no pending response', () => {
    const sim   = makeSPISim();
    const logic = PartSimulationRegistry.get('microsd-card')!;
    logic.attachEvents!(makeElement(), sim as any, noPins);

    const tx = sim.spi.onTransmit as (b: number) => void;
    tx(0xFF);
    expect(sim.spi.completeTransmit).toHaveBeenLastCalledWith(0xFF);
  });

  it('cleanup restores previous onTransmit and is callable without SPI', () => {
    const sim   = makeSPISim();
    const logic = PartSimulationRegistry.get('microsd-card')!;
    const cleanup = logic.attachEvents!(makeElement(), sim as any, noPins);
    expect(() => cleanup()).not.toThrow();
    expect(sim.spi.onTransmit).toBeNull();
  });

  it('no-op when simulator has no spi', () => {
    const sim   = { ...makeSPISim(), spi: null };
    const logic = PartSimulationRegistry.get('microsd-card')!;
    expect(() => {
      const c = logic.attachEvents!(makeElement(), sim as any, noPins);
      c();
    }).not.toThrow();
  });
});
