/**
 * Behavioral Fidelity Tests — Studio 5000 compliance
 * Covers: OSR, OSF, MOV, SCALE, COMPARE, LATCH/UNLATCH, RTO persistence,
 *         OV/UND counter bits, integer math mode, TOF off-delay
 */
import { describe, it, expect, beforeEach } from 'vitest';
import { LadderEngine } from './engine';
import type { Program, Rung } from './types';

describe('Studio 5000 Behavioral Fidelity', () => {
  let engine: LadderEngine;

  beforeEach(() => {
    engine = new LadderEngine();
  });

  // ─── One-Shot Rising (OSR) ────────────────────────────────────────

  describe('OSR (One-Shot Rising)', () => {
    it('fires true for exactly one scan on rising edge', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'oneshot', address: 'I:0.0', edgeType: 'RISING' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Scan 1: I:0.0 transitions from false → true — fires
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Scan 2: I:0.0 still true — does NOT fire again
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Scan 3: I:0.0 goes false
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Scan 4: I:0.0 transitions false → true again — fires again
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });

    it('does not fire when input was already true at start', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'oneshot', address: 'I:0.0', edgeType: 'RISING' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Input already set before first scan — no previous value to compare against
      // First scan from cold start: previousInputs is empty, so prevBool=false, fires once
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true); // fires on first scan (false→true edge)

      // Second scan: still true, no edge — does not fire
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });

  // ─── One-Shot Falling (OSF) ────────────────────────────────────────

  describe('OSF (One-Shot Falling)', () => {
    it('fires true for exactly one scan on falling edge', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'oneshot', address: 'I:0.0', edgeType: 'FALLING' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Scan 1: I:0.0 = true, no falling edge
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Scan 2: I:0.0 transitions true → false — fires
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Scan 3: I:0.0 still false — does NOT fire again
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });

  // ─── OSR/OSF on memory addresses ─────────────────────────────────

  describe('OSR on memory address (previousInputs fix)', () => {
    it('detects rising edge on a memory address', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'oneshot', address: 'M:0.0', edgeType: 'RISING' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Scan 1: M:0.0 = false (initial)
      engine.setMemory('M:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Scan 2: M:0.0 transitions false → true — fires
      engine.setMemory('M:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Scan 3: M:0.0 still true — does not fire
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });

  // ─── MOV ─────────────────────────────────────────────────────────

  describe('MOV (Move)', () => {
    it('copies value from source to destination when rung is true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'move', source: 'I:val', destination: 'M:result' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:0.0', true);
      engine.setInput('I:val', 42);

      engine.scan();
      expect(engine.getMemory('M:result')).toBe(42);
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });

    it('does not copy when rung is false', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'move', source: 'I:val', destination: 'M:result' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:0.0', false);  // rung is false
      engine.setInput('I:val', 99);
      engine.setMemory('M:result', 0);

      engine.scan();
      expect(engine.getMemory('M:result')).toBe(0); // unchanged
    });
  });

  // ─── SCALE (SCL) ─────────────────────────────────────────────────

  describe('SCALE (SCL)', () => {
    it('scales input linearly to output range', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          {
            type: 'scale',
            inputAddress: 'I:raw',
            destination: 'M:scaled',
            inMin: 0, inMax: 100,
            outMin: 0, outMax: 1000,
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:0.0', true);
      engine.setInput('I:raw', 50);

      engine.scan();
      expect(engine.getMemory('M:scaled')).toBe(500); // 50% of 0-1000
    });

    it('clamps output to outMin when input is below inMin', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'scale',
            inputAddress: 'I:raw',
            destination: 'M:scaled',
            inMin: 0, inMax: 100,
            outMin: 10, outMax: 200,
          },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:raw', -50); // below inMin

      engine.scan();
      expect(engine.getMemory('M:scaled')).toBe(10); // clamped to outMin
    });

    it('clamps output to outMax when input is above inMax', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'scale',
            inputAddress: 'I:raw',
            destination: 'M:scaled',
            inMin: 0, inMax: 100,
            outMin: 0, outMax: 255,
          },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:raw', 200); // above inMax

      engine.scan();
      expect(engine.getMemory('M:scaled')).toBe(255); // clamped to outMax
    });
  });

  // ─── COMPARE (EQU, NEQ, GRT, LES, GEQ, LEQ) ─────────────────────

  describe('COMPARE', () => {
    it('EQU (==) returns true when values are equal', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'compare',
            op: '==',
            inputA: 'I:a',
            inputB: 'I:b',
            outputAddress: 'M:result',
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:a', 5);
      engine.setInput('I:b', 5);
      engine.scan();
      expect(engine.getMemory('M:result')).toBe(true);
      expect(engine.getOutput('Q:0.0')).toBe(true); // compare passes power when true
    });

    it('EQU (==) blocks power when values are NOT equal', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'compare',
            op: '==',
            inputA: 'I:a',
            inputB: 'I:b',
            outputAddress: 'M:result',
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:a', 5);
      engine.setInput('I:b', 10);
      engine.scan();
      expect(engine.getMemory('M:result')).toBe(false);
      expect(engine.getOutput('Q:0.0')).toBe(false); // FIX 1: compare blocks power when false
    });

    it('GRT (>) passes when A > B', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'compare', op: '>', inputA: 'I:a', inputB: 'I:b', outputAddress: 'M:res' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:a', 10);
      engine.setInput('I:b', 5);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });

    it('LES (<) blocks when A >= B', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'compare', op: '<', inputA: 'I:a', inputB: 'I:b', outputAddress: 'M:res' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:a', 10);
      engine.setInput('I:b', 5);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });

  // ─── LATCH / UNLATCH ─────────────────────────────────────────────

  describe('LATCH / UNLATCH (OTL / OTU)', () => {
    it('LATCH coil stays ON after rung goes false', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'LATCH', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Energize latch
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Input goes away — latch stays ON
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });

    it('UNLATCH clears a latched output', () => {
      const latchRung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'LATCH', address: 'Q:0.0' },
        ],
      };
      const unlatchRung: Rung = {
        id: 'r2',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.1' },
          { type: 'coil', coilType: 'UNLATCH', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [latchRung, unlatchRung], cycleTime: 100 });

      // Latch
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true); // still latched

      // Unlatch
      engine.setInput('I:0.1', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });

  // ─── RTO (Retain On-Delay) persistence ───────────────────────────

  describe('RTO (Retain On-Delay)', () => {
    it('accumulates time even when input goes false (retentive)', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'timer', timerType: 'RTO', instanceId: 'T1', preset: 300, accumulated: 0 },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Scan 1: Input true, accumulate 100ms
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(100);

      // Scan 2: Input false — RTO retains accumulated time (unlike TON which resets to 0)
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(100); // retained!

      // Scan 3: Input true again, accumulates further
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(200);

      // Scan 4: One more scan to reach 300 = preset
      engine.scan();
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(300);
      expect(engine.getOutput('Q:0.0')).toBe(true); // DN bit set
    });

    it('resets accumulated time when resetAddress is true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          {
            type: 'timer',
            timerType: 'RTO',
            instanceId: 'T1',
            preset: 300,
            accumulated: 0,
            resetAddress: 'I:reset',
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Accumulate some time
      engine.setInput('I:0.0', true);
      engine.scan();
      engine.scan();
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(200);

      // Assert reset
      engine.setInput('I:reset', true);
      engine.scan();
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(0);
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });

  // ─── Counter OV/UND bits ─────────────────────────────────────────

  describe('Counter OV/UND bits', () => {
    it('sets OV bit and clamps to 9999 when CTU overflows', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'counter', counterType: 'CTU', instanceId: 'C1', preset: 9999, current: 0 },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Set counter to 9999 already
      engine.state.counters.set('C1', {
        type: 'counter', counterType: 'CTU', instanceId: 'C1',
        preset: 9999, current: 9999,
      });

      // One more rising edge should try to increment to 10000
      engine.setInput('I:0.0', true);
      engine.scan();

      const counter = engine.getState().counters.get('C1');
      expect(counter?.current).toBe(9999); // clamped
      expect(counter?.ov).toBe(true);      // overflow flag set
    });

    it('sets UND bit and clamps to 0 when CTD underflows', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'counter', counterType: 'CTD', instanceId: 'C1', preset: 0, current: 0 },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Set counter to 0
      engine.state.counters.set('C1', {
        type: 'counter', counterType: 'CTD', instanceId: 'C1',
        preset: 0, current: 0,
      });

      // One more decrement should try to go to -1
      engine.setInput('I:0.0', true);
      engine.scan();

      const counter = engine.getState().counters.get('C1');
      expect(counter?.current).toBe(0);  // clamped to 0
      expect(counter?.und).toBe(true);   // underflow flag set
    });

    it('clears OV and UND bits on reset', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          {
            type: 'counter', counterType: 'CTU', instanceId: 'C1',
            preset: 9999, current: 0, resetAddress: 'I:reset',
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Manually set OV bit
      engine.state.counters.set('C1', {
        type: 'counter', counterType: 'CTU', instanceId: 'C1',
        preset: 9999, current: 9999, ov: true,
      });

      // Reset
      engine.setInput('I:reset', true);
      engine.scan();

      const counter = engine.getState().counters.get('C1');
      expect(counter?.current).toBe(0);
      expect(counter?.ov).toBe(false);
    });
  });

  // ─── Integer Math Mode ────────────────────────────────────────────

  describe('Integer Math Mode', () => {
    it('truncates float result when integerMode=true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'math', operator: 'DIV', inputA: 'I:a', inputB: 'I:b', outputAddress: 'M:result' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100, integerMode: true });
      engine.setInput('I:a', 10);
      engine.setInput('I:b', 3);
      engine.scan();
      expect(engine.getMemory('M:result')).toBe(3); // Math.trunc(3.333...) = 3
    });

    it('keeps float result when integerMode is not set', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'math', operator: 'DIV', inputA: 'I:a', inputB: 'I:b', outputAddress: 'M:result' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 }); // no integerMode
      engine.setInput('I:a', 10);
      engine.setInput('I:b', 3);
      engine.scan();
      expect(engine.getMemory('M:result')).toBeCloseTo(3.333, 2);
    });

    it('clamps result to 32-bit signed integer range', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'math', operator: 'MUL', inputA: 'I:a', inputB: 'I:b', outputAddress: 'M:result' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100, integerMode: true });
      engine.setInput('I:a', 2000000000);
      engine.setInput('I:b', 2);  // 4 billion > 2^31-1
      engine.scan();
      expect(engine.getMemory('M:result')).toBe(2147483647); // clamped to INT32_MAX
    });
  });
});
