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

describe('Edge-Triggered Logic (Timers & Counters)', () => {
  let engine: LadderEngine;

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

  describe('TON (Timer On Delay)', () => {
    it('should delay output until preset is reached', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'timer', timerType: 'TON', 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, timer starts
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(100);

      // Scan 2: Timer at 200
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(200);

      // Scan 3: Timer at 300 >= preset
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Scan 4: Input false, Timer resets
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(0);
    });
  });

  describe('TOF (Timer Off Delay)', () => {
    it('should keep output ON while input is true, then maintain ON during off-delay, then OFF', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'timer', timerType: 'TOF', instanceId: 'T1', preset: 200 , accumulated: 0},
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Scan 1: Input true, Output ON immediately
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Scan 2: Input goes false, timer starts — output stays ON (off-delay active)
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);  // Studio 5000: still ON during off-delay
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(100);

      // Scan 3: Timer at 200 = preset — off-delay complete, output OFF
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false); // Timer done
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(200);
    });
  });

  describe('TP (Timer Pulse)', () => {
    it('should pulse output while input is true and pulse is active', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'timer', timerType: 'TP', instanceId: 'T1', preset: 200 , accumulated: 0},
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Scan 1: Input true, Pulse starts
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(true);

      // Scan 2: Input false, series is broken, output OFF immediately
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getOutput('Q:0.0')).toBe(false);
      expect(engine.getState().timers.get('T1')?.accumulated).toBe(100);
    });
  });

  describe('CTU (Counter Up)', () => {
    it('should increment on rising edge of rung', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'counter', counterType: 'CTU', instanceId: 'C1', preset: 2 , current: 0},
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });

      // Scan 1: Input true, count = 1
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getState().counters.get('C1')?.current).toBe(1);
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Scan 2: Input false, count remains 1
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getState().counters.get('C1')?.current).toBe(1);

      // Scan 3: Input true, count = 2 (Preset reached)
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getState().counters.get('C1')?.current).toBe(2);
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });
  });

  describe('CTD (Counter Down)', () => {
    it('should decrement on rising edge of rung', () => {
      const rung: Rung = {
        id: 'r1',
        enabled: true,
        series: [
          { type: 'contact', contactType: 'NO', address: 'I:0.0' },
          { type: 'counter', counterType: 'CTD', instanceId: 'C1', preset: 1 , current: 0},
          { type: 'coil', coilType: 'OUTPUT', address: 'Q:0.0' },
        ],
      };
      engine.loadProgram({ name: 'test', rungs: [rung], cycleTime: 100 });
      engine.state.counters.set('C1', { address: 'C1', current: 2, ...rung.series[0] } as any);

      // Scan 1: Input true, count = 1 (not done yet: 1 > 0)
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getState().counters.get('C1')?.current).toBe(1);
      expect(engine.getOutput('Q:0.0')).toBe(false);

      // Scan 2: Input false, count remains 1
      engine.setInput('I:0.0', false);
      engine.scan();
      expect(engine.getState().counters.get('C1')?.current).toBe(1);

      // Scan 3: Input true, count = 0 (Done: 0 <= 0, Allen Bradley CTD DN bit)
      engine.setInput('I:0.0', true);
      engine.scan();
      expect(engine.getState().counters.get('C1')?.current).toBe(0);
      expect(engine.getOutput('Q:0.0')).toBe(true);
    });
  });
});