/**
 * virtual-i2c-devices.test.ts
 *
 * Unit tests for the virtual I2C sensor library:
 *   VirtualBMP280   — barometric pressure / temperature sensor (0x76 / 0x77)
 *   VirtualDS3231   — real-time clock with on-chip temperature sensor (0x68)
 *   VirtualPCF8574  — 8-bit I/O expander (0x20–0x27 / 0x38–0x3F)
 *
 * Also covers the I2CBusManager routing (connectToSlave / writeByte / readByte)
 * and the pre-existing VirtualDS1307, VirtualTempSensor, I2CMemoryDevice helpers.
 *
 * NOTE: these tests import directly from I2CBusManager.ts which only uses
 * `import type` from avr8js — so they run in the plain Node / Vitest environment
 * without needing the wokwi-libs to be built.
 */

import { describe, it, expect } from 'vitest';
import {
  I2CBusManager,
  I2CMemoryDevice,
  VirtualDS1307,
  VirtualTempSensor,
  VirtualBMP280,
  VirtualDS3231,
  VirtualPCF8574,
} from '../simulation/I2CBusManager';

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

/** Minimal mock of AVRTWI used by I2CBusManager */
function makeTWI() {
  const calls: string[] = [];
  let   readResult = 0xFF;
  let   writeAck   = true;
  let   connectAck = true;

  return {
    calls,
    _setReadResult : (v: number)  => { readResult  = v; },
    _setWriteAck   : (v: boolean) => { writeAck    = v; },
    _setConnectAck : (v: boolean) => { connectAck  = v; },

    // --- AVRTWI API ---
    set eventHandler(_: any) { /* set by I2CBusManager constructor */ },
    completeStart   ()                   { calls.push('start'); },
    completeStop    ()                   { calls.push('stop'); },
    completeConnect (ack: boolean)       { calls.push(`connect:${ack}`); },
    completeWrite   (ack: boolean)       { calls.push(`write:${ack}`); },
    completeRead    (value: number)      { calls.push(`read:${value}`); },
  };
}

// ─── I2CBusManager routing ────────────────────────────────────────────────────

describe('I2CBusManager — routing', () => {
  it('completes start unconditionally', () => {
    const twi = makeTWI();
    const bus = new I2CBusManager(twi as any);
    bus.start(false);
    expect(twi.calls).toContain('start');
  });

  it('NACKs an address with no registered device', () => {
    const twi = makeTWI();
    const bus = new I2CBusManager(twi as any);
    bus.connectToSlave(0x42, true);
    expect(twi.calls).toContain('connect:false');
  });

  it('ACKs when a device is registered at the address', () => {
    const twi = makeTWI();
    const bus = new I2CBusManager(twi as any);
    bus.addDevice(new I2CMemoryDevice(0x42));
    bus.connectToSlave(0x42, true);
    expect(twi.calls).toContain('connect:true');
  });

  it('routes writeByte to active device and returns ACK', () => {
    const twi    = makeTWI();
    const bus    = new I2CBusManager(twi as any);
    const device = new I2CMemoryDevice(0x50);
    bus.addDevice(device);
    bus.connectToSlave(0x50, true);
    bus.writeByte(0x10);   // set register pointer
    expect(twi.calls).toContain('write:true');
  });

  it('routes readByte to active device', () => {
    const twi    = makeTWI();
    const bus    = new I2CBusManager(twi as any);
    const device = new I2CMemoryDevice(0x50);
    device.registers[0x00] = 0xAB;
    bus.addDevice(device);
    bus.connectToSlave(0x50, true);
    bus.writeByte(0x00);   // set register pointer to 0
    bus.connectToSlave(0x50, false); // repeated start, read mode
    bus.readByte(true);
    const readCall = twi.calls.find(c => c.startsWith('read:'));
    expect(readCall).toBe('read:171'); // 0xAB = 171
  });

  it('returns 0xFF on read when no device at address', () => {
    const twi = makeTWI();
    const bus = new I2CBusManager(twi as any);
    bus.readByte(true);
    expect(twi.calls).toContain('read:255');
  });

  it('NACKs write when no active device', () => {
    const twi = makeTWI();
    const bus = new I2CBusManager(twi as any);
    bus.writeByte(0x00);
    expect(twi.calls).toContain('write:false');
  });

  it('removeDevice stops routing to that address', () => {
    const twi = makeTWI();
    const bus = new I2CBusManager(twi as any);
    bus.addDevice(new I2CMemoryDevice(0x42));
    bus.removeDevice(0x42);
    bus.connectToSlave(0x42, true);
    expect(twi.calls).toContain('connect:false');
  });

  it('calls device.stop() on stop condition', () => {
    const twi    = makeTWI();
    const bus    = new I2CBusManager(twi as any);
    let   stopped = false;
    const device: any = {
      address   : 0x42,
      writeByte : () => true,
      readByte  : () => 0,
      stop      : () => { stopped = true; },
    };
    bus.addDevice(device);
    bus.connectToSlave(0x42, true);
    bus.stop();
    expect(stopped).toBe(true);
    expect(twi.calls).toContain('stop');
  });
});

// ─── I2CMemoryDevice ─────────────────────────────────────────────────────────

describe('I2CMemoryDevice', () => {
  it('first byte sets register pointer, subsequent bytes write data', () => {
    const dev = new I2CMemoryDevice(0x50);
    dev.writeByte(0x05);  // register pointer
    dev.writeByte(0xAB);  // write data to register 5
    expect(dev.registers[0x05]).toBe(0xAB);
  });

  it('reads back written data starting at register pointer', () => {
    const dev = new I2CMemoryDevice(0x50);
    dev.registers[0x02] = 0xCC;
    dev.writeByte(0x02);           // set pointer
    expect(dev.readByte()).toBe(0xCC);
  });

  it('auto-increments register pointer on read', () => {
    const dev = new I2CMemoryDevice(0x50);
    dev.registers[0x00] = 0x11;
    dev.registers[0x01] = 0x22;
    dev.writeByte(0x00);
    expect(dev.readByte()).toBe(0x11);
    expect(dev.readByte()).toBe(0x22);
  });

  it('fires onRegisterWrite callback', () => {
    const dev = new I2CMemoryDevice(0x50);
    const log: [number, number][] = [];
    dev.onRegisterWrite = (r, v) => log.push([r, v]);
    dev.writeByte(0x0A);  // pointer
    dev.writeByte(0xFF);  // data → register 0x0A
    expect(log).toEqual([[0x0A, 0xFF]]);
  });

  it('resets firstByte on stop()', () => {
    const dev = new I2CMemoryDevice(0x50);
    dev.writeByte(0x03);  // pointer set, firstByte = false
    dev.stop();           // reset
    dev.writeByte(0x07);  // new pointer
    dev.writeByte(0x55);  // write to register 7
    expect(dev.registers[0x07]).toBe(0x55);
  });
});

// ─── VirtualDS1307 ────────────────────────────────────────────────────────────

describe('VirtualDS1307', () => {
  it('address is 0x68', () => {
    expect(new VirtualDS1307().address).toBe(0x68);
  });

  it('readByte for reg 0 returns valid BCD seconds (0–59)', () => {
    const dev = new VirtualDS1307();
    dev.writeByte(0x00);  // set pointer to seconds
    const raw   = dev.readByte();
    const tens  = (raw >> 4) & 0xF;
    const units =  raw       & 0xF;
    expect(tens).toBeLessThanOrEqual(5);
    expect(units).toBeLessThanOrEqual(9);
  });

  it('reads 7 consecutive BCD time registers', () => {
    const dev = new VirtualDS1307();
    dev.writeByte(0x00);  // start at seconds
    for (let i = 0; i < 7; i++) {
      const byte = dev.readByte();
      expect(byte).toBeGreaterThanOrEqual(0x00);
      expect(byte).toBeLessThanOrEqual(0x99);
    }
  });

  it('stop() resets firstByte so next write is a new pointer', () => {
    const dev = new VirtualDS1307();
    dev.writeByte(0x02);  // set pointer to hours
    dev.stop();
    dev.writeByte(0x00);  // new pointer (seconds)
    const raw = dev.readByte();
    // raw should be seconds BCD (tens ≤ 5)
    expect((raw >> 4) & 0xF).toBeLessThanOrEqual(5);
  });
});

// ─── VirtualTempSensor ────────────────────────────────────────────────────────

describe('VirtualTempSensor', () => {
  it('address is 0x48', () => {
    expect(new VirtualTempSensor().address).toBe(0x48);
  });

  it('reads temperature high byte (register 0) correctly', () => {
    const dev = new VirtualTempSensor();
    dev.temperature = 2350;  // 23.50°C
    dev.writeByte(0x00);
    expect(dev.readByte()).toBe((2350 >> 8) & 0xFF);
  });

  it('reads humidity bytes from registers 2–3', () => {
    const dev = new VirtualTempSensor();
    dev.humidity = 5500;  // 55.00%
    dev.writeByte(0x02);
    expect(dev.readByte()).toBe((5500 >> 8) & 0xFF);
    expect(dev.readByte()).toBe(5500 & 0xFF);
  });
});

// ─── VirtualBMP280 ────────────────────────────────────────────────────────────

describe('VirtualBMP280 — construction & addresses', () => {
  it('default address is 0x76', () => {
    expect(new VirtualBMP280().address).toBe(0x76);
  });

  it('accepts 0x77 as alternate address', () => {
    expect(new VirtualBMP280(0x77).address).toBe(0x77);
  });

  it('chip_id register (0xD0) reads 0x60', () => {
    const dev = new VirtualBMP280();
    dev.writeByte(0xD0);
    expect(dev.readByte()).toBe(0x60);
  });

  it('stop() resets firstByte so register pointer can be re-set', () => {
    const dev = new VirtualBMP280();
    dev.writeByte(0xD0);
    dev.stop();
    dev.writeByte(0xF3);  // new pointer → status register
    expect(dev.readByte()).toBe(0x00);  // status = 0 (ready)
  });
});

describe('VirtualBMP280 — calibration registers', () => {
  /**
   * Reads an unsigned 16-bit little-endian value from two consecutive register
   * bytes starting at `regAddr`.
   */
  function readU16LE(dev: VirtualBMP280, regAddr: number): number {
    dev.writeByte(regAddr);
    const lo = dev.readByte();
    const hi = dev.readByte();
    dev.stop();
    return lo | (hi << 8);
  }

  function readS16LE(dev: VirtualBMP280, regAddr: number): number {
    const u = readU16LE(dev, regAddr);
    return u > 0x7FFF ? u - 0x10000 : u;
  }

  it('dig_T1 (0x88) equals 27504', () => {
    const dev = new VirtualBMP280();
    expect(readU16LE(dev, 0x88)).toBe(27504);
  });

  it('dig_T2 (0x8A) equals 26435', () => {
    expect(readS16LE(new VirtualBMP280(), 0x8A)).toBe(26435);
  });

  it('dig_T3 (0x8C) equals -1000', () => {
    expect(readS16LE(new VirtualBMP280(), 0x8C)).toBe(-1000);
  });

  it('dig_P1 (0x8E) equals 36477', () => {
    expect(readU16LE(new VirtualBMP280(), 0x8E)).toBe(36477);
  });

  it('dig_P2 (0x90) equals -10685', () => {
    expect(readS16LE(new VirtualBMP280(), 0x90)).toBe(-10685);
  });
});

describe('VirtualBMP280 — temperature compensation', () => {
  /**
   * Reads the 6-byte pressure+temperature burst (0xF7–0xFC) and reconstructs
   * the two 20-bit raw ADC values.  Returns { adcP, adcT }.
   */
  function readRawAdc(dev: VirtualBMP280): { adcP: number; adcT: number } {
    dev.writeByte(0xF7);
    const pMsb  = dev.readByte();
    const pLsb  = dev.readByte();
    const pXlsb = dev.readByte();
    const tMsb  = dev.readByte();
    const tLsb  = dev.readByte();
    const tXlsb = dev.readByte();
    dev.stop();
    const adcP = (pMsb << 12) | (pLsb << 4) | (pXlsb >> 4);
    const adcT = (tMsb << 12) | (tLsb << 4) | (tXlsb >> 4);
    return { adcP, adcT };
  }

  /**
   * BMP280 Bosch 32-bit integer temperature compensation formula.
   * Returns temperature in 0.01°C.
   */
  function compensateT(adcT: number, digT1 = 27504, digT2 = 26435, digT3 = -1000): number {
    const var1 = (((adcT >> 3) - (digT1 << 1)) * digT2) >> 11;
    const sub  = (adcT >> 4) - digT1;
    const var2 = ((sub * sub >> 12) * digT3) >> 14;
    const tFine = var1 + var2;
    return (tFine * 5 + 128) >> 8;
  }

  /**
   * BMP280 floating-point pressure compensation formula.
   * Returns pressure in Pa.
   */
  function compensateP(
    adcP: number, adcT: number,
    digT1 = 27504, digT2 = 26435, digT3 = -1000,
    digP1 = 36477, digP2 = -10685, digP3 = 3024,
    digP4 = 2855,  digP5 = 140,    digP6 = -7,
    digP7 = 15500, digP8 = -14600, digP9 = 6000,
  ): number {
    const var1 = (((adcT >> 3) - (digT1 << 1)) * digT2) >> 11;
    const sub  = (adcT >> 4) - digT1;
    const var2 = ((sub * sub >> 12) * digT3) >> 14;
    const tf   = var1 + var2;

    let v1 = tf / 2.0 - 64000.0;
    let v2 = v1 * v1 * digP6 / 32768.0;
    v2 = v2 + v1 * digP5 * 2.0;
    v2 = v2 / 4.0 + digP4 * 65536.0;
    v1 = (digP3 * v1 * v1 / 524288.0 + digP2 * v1) / 524288.0;
    v1 = (1.0 + v1 / 32768.0) * digP1;
    if (v1 === 0) return 0;
    let p = 1048576.0 - adcP;
    p = (p - v2 / 4096.0) * 6250.0 / v1;
    return p + (digP9 * p * p / 2147483648.0 + p * digP8 / 32768.0 + digP7) / 16.0;
  }

  it('default 25°C produces compensated temperature within ±0.5°C', () => {
    const dev = new VirtualBMP280();
    const { adcT } = readRawAdc(dev);
    const centideg = compensateT(adcT);
    expect(centideg / 100).toBeCloseTo(25, 0);
  });

  it('setting temperatureC = 20 produces ~20°C compensated output', () => {
    const dev = new VirtualBMP280();
    dev.temperatureC = 20;
    const { adcT } = readRawAdc(dev);
    expect(compensateT(adcT) / 100).toBeCloseTo(20, 0);
  });

  it('setting temperatureC = 0 produces ~0°C compensated output', () => {
    const dev = new VirtualBMP280();
    dev.temperatureC = 0;
    const { adcT } = readRawAdc(dev);
    expect(compensateT(adcT) / 100).toBeCloseTo(0, 0);
  });

  it('setting temperatureC = 85 produces ~85°C compensated output', () => {
    const dev = new VirtualBMP280();
    dev.temperatureC = 85;
    const { adcT } = readRawAdc(dev);
    expect(compensateT(adcT) / 100).toBeCloseTo(85, 0);
  });
});

describe('VirtualBMP280 — pressure compensation', () => {
  function readRawAdc(dev: VirtualBMP280): { adcP: number; adcT: number } {
    dev.writeByte(0xF7);
    const pMsb = dev.readByte(), pLsb = dev.readByte(), pXlsb = dev.readByte();
    const tMsb = dev.readByte(), tLsb = dev.readByte(), tXlsb = dev.readByte();
    dev.stop();
    return {
      adcP: (pMsb << 12) | (pLsb << 4) | (pXlsb >> 4),
      adcT: (tMsb << 12) | (tLsb << 4) | (tXlsb >> 4),
    };
  }

  function compensateP(adcP: number, adcT: number): number {
    const digT1 = 27504, digT2 = 26435, digT3 = -1000;
    const digP1 = 36477, digP2 = -10685, digP3 = 3024;
    const digP4 = 2855,  digP5 = 140,    digP6 = -7;
    const digP7 = 15500, digP8 = -14600, digP9 = 6000;
    const var1 = (((adcT >> 3) - (digT1 << 1)) * digT2) >> 11;
    const sub  = (adcT >> 4) - digT1;
    const var2 = ((sub * sub >> 12) * digT3) >> 14;
    const tf   = var1 + var2;
    let v1 = tf / 2.0 - 64000.0;
    let v2 = v1 * v1 * digP6 / 32768.0;
    v2 = v2 + v1 * digP5 * 2.0;
    v2 = v2 / 4.0 + digP4 * 65536.0;
    v1 = (digP3 * v1 * v1 / 524288.0 + digP2 * v1) / 524288.0;
    v1 = (1.0 + v1 / 32768.0) * digP1;
    if (v1 === 0) return 0;
    let p = 1048576.0 - adcP;
    p = (p - v2 / 4096.0) * 6250.0 / v1;
    return p + (digP9 * p * p / 2147483648.0 + p * digP8 / 32768.0 + digP7) / 16.0;
  }

  it('default 1013.25 hPa produces compensated pressure within ±5 hPa', () => {
    const dev = new VirtualBMP280();
    const { adcP, adcT } = readRawAdc(dev);
    const pHPa = compensateP(adcP, adcT) / 100;
    expect(Math.abs(pHPa - 1013.25)).toBeLessThan(5);
  });

  it('setting pressureHPa = 900 produces ~900 hPa compensated output (±5)', () => {
    const dev = new VirtualBMP280();
    dev.pressureHPa = 900;
    const { adcP, adcT } = readRawAdc(dev);
    expect(Math.abs(compensateP(adcP, adcT) / 100 - 900)).toBeLessThan(5);
  });

  it('setting pressureHPa = 1100 produces ~1100 hPa compensated output (±5)', () => {
    const dev = new VirtualBMP280();
    dev.pressureHPa = 1100;
    const { adcP, adcT } = readRawAdc(dev);
    expect(Math.abs(compensateP(adcP, adcT) / 100 - 1100)).toBeLessThan(5);
  });
});

describe('VirtualBMP280 — ctrl_meas register is writable', () => {
  it('can write and read back ctrl_meas (0xF4)', () => {
    const dev = new VirtualBMP280();
    // Set reg pointer via normal write
    dev.writeByte(0xF4);   // pointer → 0xF4
    dev.writeByte(0x57);   // write 0x57 (forced mode + oversampling)
    dev.stop();
    dev.writeByte(0xF4);   // read back
    expect(dev.readByte()).toBe(0x57);
  });
});

// ─── VirtualDS3231 ────────────────────────────────────────────────────────────

describe('VirtualDS3231 — time registers', () => {
  it('address is 0x68', () => {
    expect(new VirtualDS3231().address).toBe(0x68);
  });

  it('register 0x00 returns valid BCD seconds (0–59)', () => {
    const dev = new VirtualDS3231();
    dev.writeByte(0x00);
    const raw   = dev.readByte();
    const tens  = (raw >> 4) & 0xF;
    const units =  raw       & 0xF;
    expect(tens).toBeLessThanOrEqual(5);
    expect(units).toBeLessThanOrEqual(9);
  });

  it('register 0x02 (hours) returns valid BCD 0–23', () => {
    const dev = new VirtualDS3231();
    dev.writeByte(0x02);
    const raw   = dev.readByte();
    const tens  = (raw >> 4) & 0xF;
    const units =  raw       & 0xF;
    const hours = tens * 10 + units;
    expect(hours).toBeGreaterThanOrEqual(0);
    expect(hours).toBeLessThanOrEqual(23);
  });

  it('reads 7 consecutive BCD time registers without error', () => {
    const dev = new VirtualDS3231();
    dev.writeByte(0x00);
    for (let i = 0; i < 7; i++) {
      expect(() => dev.readByte()).not.toThrow();
    }
  });

  it('register 0x0E (control) reads 0x00', () => {
    const dev = new VirtualDS3231();
    dev.writeByte(0x0E);
    expect(dev.readByte()).toBe(0x00);
  });

  it('register 0x0F (status) reads 0x00 (OSF cleared)', () => {
    const dev = new VirtualDS3231();
    dev.writeByte(0x0F);
    expect(dev.readByte()).toBe(0x00);
  });
});

describe('VirtualDS3231 — temperature registers', () => {
  it('register 0x11 returns integer part of temperature (25°C = 0x19)', () => {
    const dev = new VirtualDS3231();
    dev.temperatureC = 25.0;
    dev.writeByte(0x11);
    expect(dev.readByte()).toBe(25);  // 25 = 0x19
  });

  it('register 0x12 returns 0x00 for integer temperature (no fractional)', () => {
    const dev = new VirtualDS3231();
    dev.temperatureC = 25.0;
    dev.writeByte(0x12);
    expect(dev.readByte()).toBe(0x00);
  });

  it('register 0x12 returns 0x80 for 0.5°C fractional (bits 7:6 = 0b10)', () => {
    const dev = new VirtualDS3231();
    dev.temperatureC = 25.5;
    dev.writeByte(0x12);
    // 0.5°C / 0.25°C = 2 = 0b10 → stored in bits 7:6 = 0x80
    expect(dev.readByte()).toBe(0x80);
  });

  it('handles negative temperature: -5°C MSB = 0xFB (251 as unsigned)', () => {
    const dev = new VirtualDS3231();
    dev.temperatureC = -5.0;
    dev.writeByte(0x11);
    // trunc(-5) & 0xFF = 251
    expect(dev.readByte()).toBe((-5) & 0xFF);
  });
});

describe('VirtualDS3231 — stop/firstByte reset', () => {
  it('stop() resets pointer acquisition', () => {
    const dev = new VirtualDS3231();
    dev.writeByte(0x11);   // set pointer
    dev.stop();
    dev.writeByte(0x00);   // new pointer → seconds
    const raw = dev.readByte();
    expect((raw >> 4) & 0xF).toBeLessThanOrEqual(5); // valid BCD tens digit
  });
});

// ─── VirtualPCF8574 ────────────────────────────────────────────────────────────

describe('VirtualPCF8574 — construction', () => {
  it('default address is 0x27', () => {
    expect(new VirtualPCF8574().address).toBe(0x27);
  });

  it('accepts custom address', () => {
    expect(new VirtualPCF8574(0x20).address).toBe(0x20);
    expect(new VirtualPCF8574(0x3F).address).toBe(0x3F);
  });

  it('default portState and outputLatch are both 0xFF', () => {
    const dev = new VirtualPCF8574();
    expect(dev.portState).toBe(0xFF);
    expect(dev.outputLatch).toBe(0xFF);
  });
});

describe('VirtualPCF8574 — write', () => {
  it('writeByte updates outputLatch', () => {
    const dev = new VirtualPCF8574();
    dev.writeByte(0b10101010);
    expect(dev.outputLatch).toBe(0b10101010);
  });

  it('writeByte fires onWrite callback with the written value', () => {
    const dev = new VirtualPCF8574();
    const log: number[] = [];
    dev.onWrite = v => log.push(v);
    dev.writeByte(0x42);
    expect(log).toEqual([0x42]);
  });

  it('multiple writes update outputLatch each time', () => {
    const dev = new VirtualPCF8574();
    dev.writeByte(0xAA);
    dev.writeByte(0x55);
    expect(dev.outputLatch).toBe(0x55);
  });
});

describe('VirtualPCF8574 — read (open-drain model)', () => {
  it('readByte returns portState & outputLatch (both 0xFF → 0xFF)', () => {
    const dev = new VirtualPCF8574();
    expect(dev.readByte()).toBe(0xFF);
  });

  it('readByte: pin driven LOW by Arduino (outputLatch=0) → reads 0 regardless of portState', () => {
    const dev = new VirtualPCF8574();
    dev.portState    = 0xFF;
    dev.writeByte(0x00);   // drive all LOW
    expect(dev.readByte()).toBe(0x00);
  });

  it('readByte: external input LOW overrides Hi-Z output (open-drain)', () => {
    const dev = new VirtualPCF8574();
    dev.portState    = 0b00001111;  // lower 4 pins pulled low by external device
    dev.outputLatch  = 0xFF;        // Arduino released all pins
    expect(dev.readByte()).toBe(0b00001111);
  });

  it('readByte reflects mix of output-low and external-low', () => {
    const dev = new VirtualPCF8574();
    dev.outputLatch = 0b11110000;  // Arduino drives lower 4 LOW, upper 4 Hi-Z
    dev.portState   = 0b10101010;  // external: alternate HIGH/LOW
    // result: upper 4 from portState masked, lower 4 forced LOW by outputLatch
    expect(dev.readByte()).toBe(0b10100000);
  });
});

describe('VirtualPCF8574 — writeByte returns ACK', () => {
  it('always returns true (ACK)', () => {
    const dev = new VirtualPCF8574();
    expect(dev.writeByte(0x00)).toBe(true);
    expect(dev.writeByte(0xFF)).toBe(true);
  });
});
