import { describe, it, expect, beforeEach } from 'vitest';
import { LadderEngine } from './engine';
import type { Program, Rung } from './types';

describe('LadderEngine', () => {
  let engine: LadderEngine;

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

  // ─── I/O Operations ────────────────────────────────────────────

  describe('setInput / getInput', () => {
    it('sets and gets boolean inputs', () => {
      engine.setInput('I:0.0', true);
      expect(engine.getInput('I:0.0')).toBe(true);

      engine.setInput('I:0.0', false);
      expect(engine.getInput('I:0.0')).toBe(false);
    });

    it('sets and gets numeric inputs with raw values', () => {
      engine.setInput('SENSOR:dist', 15.5, 15.5);
      expect(engine.getInput('SENSOR:dist')).toBe(15.5);
    });

    it('returns false for unknown inputs', () => {
      expect(engine.getInput('UNKNOWN')).toBe(false);
    });
  });

  describe('setMemory / getMemory', () => {
    it('stores and retrieves memory bits', () => {
      engine.setMemory('M:0.0', true);
      expect(engine.getMemory('M:0.0')).toBe(true);

      engine.setMemory('M:0.0', false);
      expect(engine.getMemory('M:0.0')).toBe(false);
    });

    it('defaults unknown memory to false', () => {
      expect(engine.getMemory('M:99.99')).toBe(false);
    });
  });

  // ─── Contact Evaluation ────────────────────────────────────────

  describe('contacts', () => {
    it('NO contact passes when input is true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      const program: Program = { name: 'test', rungs: [rung], cycleTime: 100 };
      engine.loadProgram(program);

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

    it('NO contact blocks when input is false', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      const program: Program = { name: 'test', rungs: [rung], cycleTime: 100 };
      engine.loadProgram(program);

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

    it('NC contact passes when input is false', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NC', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      const program: Program = { name: 'test', rungs: [rung], cycleTime: 100 };
      engine.loadProgram(program);

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

    it('NC contact blocks when input is true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NC', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      const program: Program = { name: 'test', rungs: [rung], cycleTime: 100 };
      engine.loadProgram(program);

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

    it('contact with comparison condition evaluates correctly', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'contact',
            contactType: 'NO',
            address: 'SENSOR:dist',
            condition: { op: '<', value: 20 },
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'MOTOR:B' },
        ],
      };
      const program: Program = { name: 'test', rungs: [rung], cycleTime: 100 };
      engine.loadProgram(program);

      // Distance 15 < 20 → true → motor on
      engine.setInput('SENSOR:dist', 15, 15);
      engine.scan();
      expect(engine.getOutput('MOTOR:B')).toBe(true);

      // Distance 25 < 20 → false → motor off
      engine.setInput('SENSOR:dist', 25, 25);
      engine.scan();
      expect(engine.getOutput('MOTOR:B')).toBe(false);
    });
  });

  // ─── Coil Types ────────────────────────────────────────────────

  describe('coils', () => {
    it('OUTPUT coil follows rung logic', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      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(false);
    });

    it('SET coil latches on and stays until RESET', () => {
      const setRung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'SET', address: 'Q:0.0' },
        ],
      };
      const resetRung: Rung = {
        id: 'r2',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.1' },
          { type: 'coil', coilType: 'RESET', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [setRung, resetRung], cycleTime: 100 });

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

      // Input goes away but output stays latched
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

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

    it('TOGGLE coil toggles on activation', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'TOGGLE', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Rising edge: first toggle (false → true)
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Falling edge: deactivate (no toggle)
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Rising edge: second toggle (false → true)
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });

    it('TOGGLE coil only fires on rising edge — not every scan while held', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'TOGGLE', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Rising edge: false → true, first toggle
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Held high — multiple scans should NOT toggle again
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Falling edge: true → false (no toggle)
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Rising edge again: false → true, second toggle
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Held high again — should NOT toggle
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });

    it('OUTPUT coil with value sets numeric output', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'MOTOR:B', value: 75 },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('MOTOR:B')).toBe(75);
    });
  });

  // ─── Series Logic (AND) ────────────────────────────────────────
  // ─── Parallel Logic (OR/AND) ────────────────────────────────────
  describe('parallel branches (OR/AND logic)', () => {
    it('any branch true energizes output (OR logic)', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'branch',
            id: 'b1',
            logic: 'OR',
            paths: [
              [{ type: 'contact', contactType: 'NO', address: 'I:0.0' }],
              [{ type: 'contact', contactType: 'NO', address: 'I:0.1' }],
            ],
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

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

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

    it('AND branch requires all paths true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'branch',
            id: 'b1',
            logic: 'AND',
            paths: [
              [{ type: 'contact', contactType: 'NO', address: 'I:0.0' }],
              [{ type: 'contact', contactType: 'NO', address: 'I:0.1' }],
            ],
          },
          { 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:0.1', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

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

  // ─── Parallel Logic (OR) ───────────────────────────────────────
  // ─── Parallel Logic (OR/AND) ────────────────────────────────────
  describe('parallel branches (OR/AND logic)', () => {
    it('any branch true energizes output (OR logic)', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'branch',
            id: 'b1',
            logic: 'OR',
            paths: [
              [{ type: 'contact', contactType: 'NO', address: 'I:0.0' }],
              [{ type: 'contact', contactType: 'NO', address: 'I:0.1' }],
            ],
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

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

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

    it('AND branch requires all paths true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'branch',
            id: 'b1',
            logic: 'AND',
            paths: [
              [{ type: 'contact', contactType: 'NO', address: 'I:0.0' }],
              [{ type: 'contact', contactType: 'NO', address: 'I:0.1' }],
            ],
          },
          { 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:0.1', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

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

  // ─── Timers ────────────────────────────────────────────────────

  describe('timers', () => {
    it('TON timer delays output activation', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          {
            type: 'timer',
            timerType: 'TON',
            instanceId: 'T1',
            preset: 500,
            accumulated: 0,
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      engine.setInput('I:0.0', true);
      engine.state.running = true;

      // Cycle 1: accumulated = 100, not yet at 500
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Cycle 2: accumulated = 200
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Cycle 3: accumulated = 300
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Cycle 4: accumulated = 400
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Cycle 5: accumulated = 500 >= preset
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });
  });

  // ─── Counters ──────────────────────────────────────────────────

  describe('counters', () => {
    it('CTU counter fires when count reaches preset', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'counter',
            counterType: 'CTU',
            instanceId: 'C1',
            preset: 3,
            current: 0,
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.state.running = true;

      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false); // count=1

      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false); // count=2

      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true); // count=3 >= preset
    });

    it('counter resets when reset address is true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'counter',
            counterType: 'CTU',
            instanceId: 'C1',
            preset: 3,
            current: 0,
            resetAddress: 'I:0.1',
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.state.running = true;

      engine.scan();
      engine.scan();
      engine.setInput('I:0.1', true);
      engine.scan();
      // After reset, count goes to 0
      const counter = engine.getState().counters.get('C1');
      expect(counter?.current).toBe(0);
    });
  });

  // ─── Logic Gates ──────────────────────────────────────────────

  describe('logic gates', () => {
    it('AND gate requires all inputs true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'gate',
            gateType: 'AND',
            inputs: ['I:0.0', 'I:0.1'],
            outputAddress: 'G:AND',
          },
          { 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:0.1', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });

    it('OR gate requires at least one input true', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          {
            type: 'gate',
            gateType: 'OR',
            inputs: ['I:0.0', 'I:0.1'],
            outputAddress: 'G:OR',
          },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:0.0', false);
      engine.setInput('I:0.1', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });
  });

  // ─── Multi-Rung Programs ───────────────────────────────────────

  describe('multi-rung programs', () => {
    it('rungs are evaluated top-to-bottom', () => {
      const program: Program = {
        name: 'multi',
        cycleTime: 100,
        rungs: [
          {
            id: 'r1',
            enabled: true,
            series: [
              { type: 'contact', contactType: 'NO', address: 'I:0.0' },
              { type: 'coil', coilType: 'OUTPUT', address: 'M:0.0' },
            ],
          },
          {
            id: 'r2',
            enabled: true,
            series: [
              { type: 'contact', contactType: 'NO', address: 'M:0.0' },
              { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
            ],
          },
        ],
      };
      engine.loadProgram(program);

      engine.setInput('I:0.0', true);
      engine.state.running = true;
      engine.scan();

      // Rung 1 sets M:0.0 = true
      expect(engine.getOutput('M:0.0')).toBe(true);
      // Rung 2 reads M:0.0 and sets Q:0.0 = true
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });
  });

  // ─── Engine Control ────────────────────────────────────────────

  describe('start / stop', () => {
    it('scan() works without running flag for manual use', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:0.0', true);

      // Manual scan works regardless of running flag
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);
      expect(engine.getState().cycle).toBe(1);
    });

    it('disabled rungs are skipped', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: false,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });

  // ─── Disabled Rungs ────────────────────────────────────────────

  describe('disabled rungs', () => {
    it('disabled rungs are skipped', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: false,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.setInput('I:0.0', true);
      engine.state.running = true;
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
    });
  });
});