/**
 * sensor-parts.test.ts
 *
 * Tests simulation logic for sensor and stepper-motor components registered
 * in SensorParts.ts (and stepper-motor in particular).
 */

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

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

// ─── RAF mock (no-op to prevent infinite loops) ───────────────────────────────
beforeEach(() => {
  let counter = 0;
  vi.stubGlobal('requestAnimationFrame', (_cb: FrameRequestCallback) => ++counter);
  vi.stubGlobal('cancelAnimationFrame', vi.fn());
  vi.stubGlobal('setInterval', vi.fn().mockReturnValue(42));
  vi.stubGlobal('clearInterval', vi.fn());
  vi.stubGlobal('setTimeout', vi.fn().mockReturnValue(1));
  vi.stubGlobal('clearTimeout', vi.fn());
});
afterEach(() => vi.unstubAllGlobals());

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

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

function makeADC() {
  return { channelValues: new Array(8).fill(0) };
}

function makeSimulator(adc?: ReturnType<typeof makeADC> | null) {
  const pinManager = {
    onPinChange:  vi.fn().mockReturnValue(() => {}),
    onPwmChange:  vi.fn().mockReturnValue(() => {}),
    triggerPinChange: vi.fn(),
  };
  return {
    pinManager,
    getADC:      vi.fn().mockReturnValue(adc ?? null),
    setPinState: 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;

// ─── SensorParts registration check ──────────────────────────────────────────

describe('SensorParts — registration', () => {
  const SENSOR_IDS = [
    'tilt-switch',
    'ntc-temperature-sensor',
    'gas-sensor',
    'flame-sensor',
    'heart-beat-sensor',
    'big-sound-sensor',
    'small-sound-sensor',
    'stepper-motor',
    'led-ring',
    'neopixel-matrix',
  ];

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

// ─── Tilt Switch ─────────────────────────────────────────────────────────────

describe('tilt-switch — attachEvents', () => {
  it('sets OUT pin LOW on attach (upright), then HIGH after click, then LOW again', () => {
    const logic = PartSimulationRegistry.get('tilt-switch')!;
    const sim = makeSimulator();
    const element = makeElement();

    // Capture addEventListener calls
    const listeners: Record<string, (...args: any[]) => void> = {};
    (element.addEventListener as ReturnType<typeof vi.fn>).mockImplementation(
      (event: string, handler: (...args: any[]) => void) => {
        listeners[event] = handler;
      }
    );

    logic.attachEvents!(element, sim as any, pinMap({ OUT: 14 }));

    // Should have started LOW (upright)
    expect(sim.setPinState).toHaveBeenCalledWith(14, false);

    // First click — tilts HIGH
    sim.setPinState.mockClear();
    listeners['click']?.();
    expect(sim.setPinState).toHaveBeenCalledWith(14, true);

    // Second click — returns LOW
    sim.setPinState.mockClear();
    listeners['click']?.();
    expect(sim.setPinState).toHaveBeenCalledWith(14, false);
  });

  it('does nothing when OUT pin is not connected', () => {
    const logic = PartSimulationRegistry.get('tilt-switch')!;
    const sim = makeSimulator();
    const element = makeElement();

    const cleanup = logic.attachEvents!(element, sim as any, noPins);
    expect(cleanup).toBeDefined();
    expect(sim.setPinState).not.toHaveBeenCalled();
  });
});

// ─── NTC Temperature Sensor ──────────────────────────────────────────────────

describe('ntc-temperature-sensor — attachEvents', () => {
  it('injects 2.5V (mid-range) on OUT pin at room temperature', () => {
    const logic = PartSimulationRegistry.get('ntc-temperature-sensor')!;
    const adc = makeADC();
    const sim = makeSimulator(adc);
    const element = makeElement();

    logic.attachEvents!(element, sim as any, pinMap({ OUT: 14 }));

    // Pin 14 = ADC channel 0.  2.5V should be stored in channelValues[0]
    expect(adc.channelValues[0]).toBeCloseTo(2.5, 2);
  });

  it('does nothing when OUT pin is not connected', () => {
    const logic = PartSimulationRegistry.get('ntc-temperature-sensor')!;
    const adc = makeADC();
    const sim = makeSimulator(adc);
    const element = makeElement();

    logic.attachEvents!(element, sim as any, noPins);
    // ADC should remain zeroed
    expect(adc.channelValues[0]).toBe(0);
  });
});

// ─── Gas Sensor ──────────────────────────────────────────────────────────────

describe('gas-sensor — attachEvents', () => {
  it('injects baseline analog voltage on AOUT and sets ledPower=true', () => {
    const logic = PartSimulationRegistry.get('gas-sensor')!;
    const adc = makeADC();
    const sim = makeSimulator(adc);
    const el = makeElement() as any;

    logic.attachEvents!(el, sim as any, pinMap({ AOUT: 14, DOUT: 7 }));

    // AOUT → ADC channel 0, baseline 1.5V
    expect(adc.channelValues[0]).toBeCloseTo(1.5, 2);
    expect(el.ledPower).toBe(true);
  });

  it('registers pin-change listener for DOUT to update ledD0', () => {
    const logic = PartSimulationRegistry.get('gas-sensor')!;
    const sim = makeSimulator();
    const el = makeElement() as any;

    logic.attachEvents!(el, sim as any, pinMap({ DOUT: 7 }));

    // Should have registered a onPinChange listener for DOUT (pin 7)
    expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(7, expect.any(Function));

    // Simulate DOUT going HIGH → ledD0 should update
    const handler = sim.pinManager.onPinChange.mock.calls[0][1];
    handler(7, true);
    expect(el.ledD0).toBe(true);

    handler(7, false);
    expect(el.ledD0).toBe(false);
  });
});

// ─── Flame Sensor ────────────────────────────────────────────────────────────

describe('flame-sensor — attachEvents', () => {
  it('injects baseline analog voltage on AOUT and sets ledPower=true', () => {
    const logic = PartSimulationRegistry.get('flame-sensor')!;
    const adc = makeADC();
    const sim = makeSimulator(adc);
    const el = makeElement() as any;

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

    expect(adc.channelValues[0]).toBeCloseTo(1.5, 2);
    expect(el.ledPower).toBe(true);
  });

  it('updates ledSignal when DOUT pin state changes', () => {
    const logic = PartSimulationRegistry.get('flame-sensor')!;
    const sim = makeSimulator();
    const el = makeElement() as any;

    logic.attachEvents!(el, sim as any, pinMap({ DOUT: 8 }));

    expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(8, expect.any(Function));

    const handler = sim.pinManager.onPinChange.mock.calls[0][1];
    handler(8, true);
    expect(el.ledSignal).toBe(true);
    handler(8, false);
    expect(el.ledSignal).toBe(false);
  });
});

// ─── Heart Beat Sensor ───────────────────────────────────────────────────────

describe('heart-beat-sensor — attachEvents', () => {
  it('starts OUT pin LOW and sets up an interval for pulse generation', () => {
    const logic = PartSimulationRegistry.get('heart-beat-sensor')!;
    const sim = makeSimulator();
    const element = makeElement();

    logic.attachEvents!(element, sim as any, pinMap({ OUT: 14 }));

    // Should start LOW
    expect(sim.setPinState).toHaveBeenCalledWith(14, false);
    // Should have called setInterval
    expect(setInterval).toHaveBeenCalled();
  });

  it('clears the interval on cleanup', () => {
    const logic = PartSimulationRegistry.get('heart-beat-sensor')!;
    const sim = makeSimulator();
    const element = makeElement();

    const cleanup = logic.attachEvents!(element, sim as any, pinMap({ OUT: 14 }));
    cleanup();

    expect(clearInterval).toHaveBeenCalledWith(42); // 42 is the mock return from setInterval
  });

  it('does nothing when OUT pin is not connected', () => {
    const logic = PartSimulationRegistry.get('heart-beat-sensor')!;
    const sim = makeSimulator();
    const element = makeElement();

    logic.attachEvents!(element, sim as any, noPins);
    expect(setInterval).not.toHaveBeenCalled();
  });
});

// ─── Big Sound Sensor ────────────────────────────────────────────────────────

describe('big-sound-sensor — attachEvents', () => {
  it('injects 2.5V on AOUT and sets led2=true (power LED)', () => {
    const logic = PartSimulationRegistry.get('big-sound-sensor')!;
    const adc = makeADC();
    const sim = makeSimulator(adc);
    const el = makeElement() as any;

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

    expect(adc.channelValues[0]).toBeCloseTo(2.5, 2);
    expect(el.led2).toBe(true);
  });

  it('updates led1 when DOUT pin changes', () => {
    const logic = PartSimulationRegistry.get('big-sound-sensor')!;
    const sim = makeSimulator();
    const el = makeElement() as any;

    logic.attachEvents!(el, sim as any, pinMap({ DOUT: 9 }));

    expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(9, expect.any(Function));
    const handler = sim.pinManager.onPinChange.mock.calls[0][1];
    handler(9, true);  expect(el.led1).toBe(true);
    handler(9, false); expect(el.led1).toBe(false);
  });
});

// ─── Small Sound Sensor ──────────────────────────────────────────────────────

describe('small-sound-sensor — attachEvents', () => {
  it('injects 2.5V on AOUT and sets ledPower=true', () => {
    const logic = PartSimulationRegistry.get('small-sound-sensor')!;
    const adc = makeADC();
    const sim = makeSimulator(adc);
    const el = makeElement() as any;

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

    expect(adc.channelValues[0]).toBeCloseTo(2.5, 2);
    expect(el.ledPower).toBe(true);
  });

  it('updates ledSignal when DOUT pin changes', () => {
    const logic = PartSimulationRegistry.get('small-sound-sensor')!;
    const sim = makeSimulator();
    const el = makeElement() as any;

    logic.attachEvents!(el, sim as any, pinMap({ DOUT: 10 }));

    const handler = sim.pinManager.onPinChange.mock.calls[0][1];
    handler(10, true);  expect(el.ledSignal).toBe(true);
    handler(10, false); expect(el.ledSignal).toBe(false);
  });
});

// ─── Stepper Motor ───────────────────────────────────────────────────────────

describe('stepper-motor — attachEvents', () => {
  it('registers pin-change listeners for all 4 coil pins', () => {
    const logic = PartSimulationRegistry.get('stepper-motor')!;
    const sim = makeSimulator();
    const el = makeElement() as any;
    el.angle = 0;

    const pins = { 'A-': 4, 'A+': 5, 'B+': 6, 'B-': 7 };
    logic.attachEvents!(el, sim as any, pinMap(pins));

    expect(sim.pinManager.onPinChange).toHaveBeenCalledTimes(4);
    const registeredPins = sim.pinManager.onPinChange.mock.calls.map(([p]: [number]) => p);
    expect(registeredPins).toEqual(expect.arrayContaining([4, 5, 6, 7]));
  });

  it('advances angle by 1.8° per forward step (full-step sequence)', () => {
    const logic = PartSimulationRegistry.get('stepper-motor')!;
    const sim = makeSimulator();
    const el = makeElement() as any;
    el.angle = 0;

    const pins = { 'A-': 4, 'A+': 5, 'B+': 6, 'B-': 7 };
    logic.attachEvents!(el, sim as any, pinMap(pins));

    // Collect handlers indexed by pin number
    const handlers: Record<number, (pin: number, s: boolean) => void> = {};
    for (const [pin, handler] of sim.pinManager.onPinChange.mock.calls) {
      handlers[pin as number] = handler;
    }

    // Step 0: A+ = HIGH (others LOW)
    handlers[5]?.(5, true);  // A+

    // Step 1: B+ = HIGH, A+ = LOW → should advance angle
    handlers[5]?.(5, false); // A+ LOW
    handlers[6]?.(6, true);  // B+ HIGH

    expect(el.angle).toBeCloseTo(1.8, 1);
  });

  it('does nothing with zero coil pins connected', () => {
    const logic = PartSimulationRegistry.get('stepper-motor')!;
    const sim = makeSimulator();
    const el = makeElement() as any;
    el.angle = 0;

    logic.attachEvents!(el, sim as any, noPins);

    expect(sim.pinManager.onPinChange).not.toHaveBeenCalled();
    expect(el.angle).toBe(0);
  });
});

// ─── LED Ring (NeoPixel) ─────────────────────────────────────────────────────

describe('led-ring — attachEvents', () => {
  it('registers a pin-change listener on the DIN pin', () => {
    const logic = PartSimulationRegistry.get('led-ring')!;
    const sim = makeSimulator();
    sim.cpu = { data: new Uint8Array(512), cycles: 0 } as any;
    const el = makeElement() as any;
    el.setPixel = vi.fn();

    logic.attachEvents!(el, sim as any, pinMap({ DIN: 6 }));

    expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(6, expect.any(Function));
  });

  it('does nothing when DIN pin is not connected', () => {
    const logic = PartSimulationRegistry.get('led-ring')!;
    const sim = makeSimulator();
    const el = makeElement() as any;

    const cleanup = logic.attachEvents!(el, sim as any, noPins);
    expect(cleanup).toBeDefined();
    expect(sim.pinManager.onPinChange).not.toHaveBeenCalled();
  });
});

// ─── NeoPixel Matrix ─────────────────────────────────────────────────────────

describe('neopixel-matrix — attachEvents', () => {
  it('registers a pin-change listener on the DIN pin', () => {
    const logic = PartSimulationRegistry.get('neopixel-matrix')!;
    const sim = makeSimulator();
    sim.cpu = { data: new Uint8Array(512), cycles: 0 } as any;
    const el = makeElement() as any;
    el.setPixel = vi.fn();
    el.cols = 8;

    logic.attachEvents!(el, sim as any, pinMap({ DIN: 6 }));

    expect(sim.pinManager.onPinChange).toHaveBeenCalledWith(6, expect.any(Function));
  });
});
