/**
 * ssd1306-render.test.ts
 *
 * Tests the SSD1306 OLED simulation's rendering path:
 *   - GDDRAM is filled correctly via I2C writes
 *   - syncElement() converts 1-bit GDDRAM → RGBA ImageData
 *   - element.imageData is updated and element.redraw() is called
 *
 * This covers the bug fix where syncElement() was calling el.buffer /
 * el.renderFrame() (non-existent) instead of el.imageData / el.redraw().
 */

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

// ─── Polyfill ImageData for Node/Vitest (no browser) ─────────────────────────

beforeAll(() => {
  if (typeof globalThis.ImageData === 'undefined') {
    class ImageDataPolyfill {
      readonly width: number;
      readonly height: number;
      readonly data: Uint8ClampedArray;

      constructor(widthOrData: number | Uint8ClampedArray, height: number) {
        if (typeof widthOrData === 'number') {
          this.width  = widthOrData;
          this.height = height;
          this.data   = new Uint8ClampedArray(widthOrData * height * 4);
        } else {
          this.width  = widthOrData.length / 4 / height;
          this.height = height;
          this.data   = new Uint8ClampedArray(widthOrData);
        }
      }
    }
    (globalThis as any).ImageData = ImageDataPolyfill;
  }
});

// ─── Helpers ──────────────────────────────────────────────────────────────────

/** Build a mock wokwi-ssd1306 element with the real ImageData API. */
function makeOLEDElement() {
  const imageData = new ImageData(128, 64);
  const redraw = vi.fn();
  return {
    imageData,
    redraw,
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
  } as unknown as HTMLElement & { imageData: ImageData; redraw: ReturnType<typeof vi.fn> };
}

/** Build a minimal AVR simulator stub that supports addI2CDevice. */
function makeSim() {
  const devices: any[] = [];
  return {
    addI2CDevice: vi.fn((d: any) => devices.push(d)),
    i2cBus: { removeDevice: vi.fn() },
    _devices: devices,
  };
}

/**
 * Simulate the Adafruit SSD1306 library's I2C init + fill sequence.
 *
 * The library sends:
 *   START → addr 0x3C write → 0x00 (cmd ctrl) → [commands…] → STOP
 *   START → addr 0x3C write → 0x40 (data ctrl) → [data…] → STOP
 *
 * In our model the I2CBusManager calls device.writeByte() for every byte
 * after the address phase, starting with the control byte.
 */
function sendCommandStream(device: any, cmds: number[]) {
  device.writeByte(0x00); // control byte: command stream (Co=0, D/C#=0)
  for (const b of cmds) device.writeByte(b);
  device.stop();
}

function sendDataStream(device: any, data: number[]) {
  device.writeByte(0x40); // control byte: GDDRAM data
  for (const b of data) device.writeByte(b);
  device.stop();
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe('SSD1306 — ImageData rendering (syncElement fix)', () => {
  it('registers ssd1306 in PartSimulationRegistry', () => {
    expect(PartSimulationRegistry.get('ssd1306')).toBeDefined();
  });

  it('creates a VirtualSSD1306 device at address 0x3C', () => {
    const el  = makeOLEDElement();
    const sim = makeSim();
    PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
    expect(sim.addI2CDevice).toHaveBeenCalledOnce();
    expect(sim._devices[0].address).toBe(0x3C);
  });

  it('calls element.redraw() after a STOP', () => {
    const el  = makeOLEDElement();
    const sim = makeSim();
    PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
    const device = sim._devices[0];

    // Simple data write — fill first byte
    sendDataStream(device, [0xFF]);

    expect(el.redraw).toHaveBeenCalled();
  });

  it('renders a fully-lit column 0 of page 0 (0xFF → top 8 pixels lit)', () => {
    const el  = makeOLEDElement();
    const sim = makeSim();
    PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
    const device = sim._devices[0];

    // Set horizontal addressing, col 0–127, page 0–7
    sendCommandStream(device, [
      0x20, 0x00,        // horizontal addressing mode
      0x21, 0x00, 0x7F,  // col 0–127
      0x22, 0x00, 0x07,  // page 0–7
    ]);

    // Write 0xFF to column 0 of page 0 → all 8 bits set → rows 0–7, col 0 lit
    sendDataStream(device, [0xFF]);

    const px = el.imageData.data; // RGBA

    // Row 0, col 0 → pixel index 0
    const idx = (0 * 128 + 0) * 4;
    expect(px[idx + 3]).toBe(255);             // alpha = 255 (opaque)
    expect(px[idx] + px[idx + 1] + px[idx + 2]).toBeGreaterThan(0); // not black

    // Row 7, col 0 → pixel index (7 * 128 + 0) * 4
    const idx7 = (7 * 128 + 0) * 4;
    expect(px[idx7 + 3]).toBe(255);
    expect(px[idx7] + px[idx7 + 1] + px[idx7 + 2]).toBeGreaterThan(0);
  });

  it('renders an unlit pixel as black (RGB = 0)', () => {
    const el  = makeOLEDElement();
    const sim = makeSim();
    PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
    const device = sim._devices[0];

    sendCommandStream(device, [0x20, 0x00, 0x21, 0x00, 0x7F, 0x22, 0x00, 0x07]);
    // 0x01 → only bit 0 set → only row 0 of page 0 is lit; row 1 is off
    sendDataStream(device, [0x01]);

    const px = el.imageData.data;

    // Row 0 col 0 → lit
    const idxLit = (0 * 128 + 0) * 4;
    expect(px[idxLit] + px[idxLit + 1] + px[idxLit + 2]).toBeGreaterThan(0);

    // Row 1 col 0 → unlit (bit 1 of 0x01 = 0)
    const idxOff = (1 * 128 + 0) * 4;
    expect(px[idxOff]).toBe(0);
    expect(px[idxOff + 1]).toBe(0);
    expect(px[idxOff + 2]).toBe(0);
  });

  it('fills all 1024 GDDRAM bytes via horizontal addressing', () => {
    const el  = makeOLEDElement();
    const sim = makeSim();
    PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
    const device = sim._devices[0];

    sendCommandStream(device, [0x20, 0x00, 0x21, 0x00, 0x7F, 0x22, 0x00, 0x07]);

    // Fill all 1024 GDDRAM bytes with a checkerboard pattern (0xAA / 0x55)
    const data: number[] = [];
    for (let i = 0; i < 1024; i++) data.push(i % 2 === 0 ? 0xAA : 0x55);
    sendDataStream(device, data);

    // Spot-check: page 7, col 127 = index 7*128+127 = 1023
    expect(device.buffer[1023]).toBe(0x55);

    // All 128*64 pixels must have alpha=255
    const px = el.imageData.data;
    let allOpaque = true;
    for (let i = 3; i < px.length; i += 4) {
      if (px[i] !== 255) { allOpaque = false; break; }
    }
    expect(allOpaque).toBe(true);
  });

  it('does not throw when element has no imageData yet (null/undefined)', () => {
    const el = {
      imageData: undefined,
      redraw: vi.fn(),
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
    } as unknown as HTMLElement;

    const sim = makeSim();
    PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
    const device = sim._devices[0];

    expect(() => sendDataStream(device, [0xFF])).not.toThrow();
  });

  it('Adafruit SSD1306 init sequence: processes multi-byte commands without crashing', () => {
    const el  = makeOLEDElement();
    const sim = makeSim();
    PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
    const device = sim._devices[0];

    // Minimal Adafruit init (from Adafruit_SSD1306.cpp begin())
    const initCmds = [
      0xAE,        // Display OFF
      0xD5, 0x80,  // Set display clock divide
      0xA8, 0x3F,  // Set multiplex ratio (64-1)
      0xD3, 0x00,  // Set display offset
      0x40,        // Set start line
      0x8D, 0x14,  // Charge pump ON
      0x20, 0x00,  // Horizontal addressing
      0xA1,        // Segment remap
      0xC8,        // COM output scan direction
      0xDA, 0x12,  // COM pins hardware config
      0x81, 0xCF,  // Contrast
      0xD9, 0xF1,  // Pre-charge period
      0xDB, 0x40,  // VCOMH deselect level
      0xA4,        // Display from RAM
      0xA6,        // Normal display
      0x2E,        // Deactivate scroll
      0xAF,        // Display ON
    ];

    expect(() => {
      sendCommandStream(device, initCmds);
      // After init, write one page of data
      sendCommandStream(device, [0x21, 0x00, 0x7F, 0x22, 0x00, 0x07]);
      sendDataStream(device, new Array(1024).fill(0x00));
    }).not.toThrow();

    expect(el.redraw).toHaveBeenCalled();
  });
});
