import { describe, it, expect } from 'vitest';
import { importL5X, parseL5X } from './l5x-importer';
import { exportToL5X, exportProgramToL5X } from './l5x-exporter';
// ─── Minimal L5X fixtures ─────────────────────────────────────────
const minimalL5X = `<?xml version="1.0" encoding="UTF-8"?>
<RSLogix5000Content>
<Controller>
<Programs>
<Program Name="MainProg" Reentrant="false">
<Routine Name="MainProg" Type="RLL">
<RLLContent>
<Rung Number="0" Type="N"><Text><![CDATA[XIC(I:0/0) OTE(O:0/0);]]></Text></Rung>
<Rung Number="1" Type="N"><Text><![CDATA[XIO(I:0/1) OTL(O:0/1);]]></Text></Rung>
<Rung Number="2" Type="N"><Text><![CDATA[XIC(I:0/0) TON(T4:0,.EN,.TT,5.0);]]></Text></Rung>
</RLLContent>
</Routine>
</Program>
</Programs>
</Controller>
</RSLogix5000Content>`;
const complexL5X = `<?xml version="1.0" encoding="UTF-8"?>
<RSLogix5000Content>
<Controller>
<Programs>
<Program Name="ComplexProg" Reentrant="false">
<Routine Name="ComplexProg" Type="RLL">
<RLLContent>
<Rung Number="0" Type="N"><Text><![CDATA[XIC(I:0/0) XIC(I:0/1) GRT(N7:0,100) OTE(O:0/0);]]></Text></Rung>
<Rung Number="1" Type="N"><Text><![CDATA[[XIC(I:0/0),XIC(I:0/1)] OTE(O:0/2);]]></Text></Rung>
<Rung Number="2" Type="N"><Text><![CDATA[XIC(I:0/0) MOV(N7:0,N7:1);]]></Text></Rung>
<Rung Number="3" Type="N"><Text><![CDATA[XIC(I:0/0) ADD(N7:0,N7:1,N7:2);]]></Text></Rung>
<Rung Number="4" Type="N"><Text><![CDATA[XIC(I:0/0) EQU(N7:0,50);]]></Text></Rung>
</RLLContent>
</Routine>
</Program>
</Programs>
</Controller>
</RSLogix5000Content>`;
// ─── Tests ────────────────────────────────────────────────────────
describe('L5X Importer', () => {
describe('importL5X', () => {
it('parses a simple XIC/OTE rung', () => {
const program = importL5X(minimalL5X);
expect(program).not.toBeNull();
expect(program!.name).toBe('MainProg');
expect(program!.rungs.length).toBe(3);
});
it('correctly identifies XIC as NO contact', () => {
const program = importL5X(minimalL5X);
const rung0 = program!.rungs[0];
expect(rung0.series.length).toBe(2);
expect(rung0.series[0].type).toBe('contact');
expect((rung0.series[0] as any).contactType).toBe('NO');
});
it('correctly identifies XIO as NC contact', () => {
const program = importL5X(minimalL5X);
const rung1 = program!.rungs[1];
expect(rung1.series[0].type).toBe('contact');
expect((rung1.series[0] as any).contactType).toBe('NC');
});
it('correctly identifies OTE as OTE coil', () => {
const program = importL5X(minimalL5X);
const rung0 = program!.rungs[0];
const coil = rung0.series[rung0.series.length - 1];
expect(coil.type).toBe('coil');
expect((coil as any).coilType).toBe('OTE');
expect((coil as any).address).toBe('O:0/0');
});
it('correctly identifies OTL as SET coil', () => {
const program = importL5X(minimalL5X);
const rung1 = program!.rungs[1];
const coil = rung1.series[rung1.series.length - 1];
expect((coil as any).coilType).toBe('SET');
});
it('parses TON timer', () => {
const program = importL5X(minimalL5X);
const rung2 = program!.rungs[2];
const timer = rung2.series.find(el => el.type === 'timer');
expect(timer).toBeDefined();
expect((timer as any).timerType).toBe('TON');
expect((timer as any).instanceId).toBe('T4:0');
expect((timer as any).preset).toBe(5000);
});
it('handles complex rungs with multiple contacts', () => {
const program = importL5X(complexL5X);
expect(program).not.toBeNull();
expect(program!.rungs.length).toBe(5);
// Rung 0: XIC XIC GRT OTE
const rung0 = program!.rungs[0];
expect(rung0.series.length).toBeGreaterThanOrEqual(3);
});
it('handles parallel branches [A,B]', () => {
const program = importL5X(complexL5X);
const rung1 = program!.rungs[1];
expect(rung1.series.length).toBeGreaterThanOrEqual(2);
});
it('handles MOV instruction', () => {
const program = importL5X(complexL5X);
const rung2 = program!.rungs[2];
const moveEl = rung2.series.find(e => e.type === 'move');
expect(moveEl).not.toBeNull();
});
it('handles ADD instruction', () => {
const program = importL5X(complexL5X);
const rung3 = program!.rungs[3];
const mathEl = rung3.series.find(e => e.type === 'math');
expect(mathEl).not.toBeNull();
expect((mathEl as any).operator).toBe('ADD');
});
it('handles EQU compare', () => {
const program = importL5X(complexL5X);
const rung4 = program!.rungs[4];
const cmpEl = rung4.series.find(e => e.type === 'compare');
expect(cmpEl).not.toBeNull();
expect((cmpEl as any).op).toBe('==');
});
});
describe('parseL5X', () => {
it('returns array of all programs', () => {
const programs = parseL5X(minimalL5X);
expect(programs.length).toBe(1);
expect(programs[0].name).toBe('MainProg');
});
});
describe('L5X Exporter', () => {
it('exports a program back to valid XML', () => {
const program = importL5X(minimalL5X)!;
const xml = exportProgramToL5X(program);
expect(xml).toContain('<?xml');
expect(xml).toContain('<RSLogix5000Content');
expect(xml).toContain('MainProg');
expect(xml).toContain('XIC');
expect(xml).toContain('OTE');
});
it('exports OTL for SET coil type', () => {
const program = importL5X(minimalL5X)!;
const xml = exportProgramToL5X(program);
expect(xml).toContain('OTL(O:0/1)');
});
it('exports multiple programs', () => {
const programs = parseL5X(minimalL5X);
const xml = exportToL5X(programs);
expect(xml).toContain('<ScheduledProgram Name="MainProg"/>');
});
it('produces round-trip-compatible output', () => {
// Import → Export → Import again
const original = importL5X(minimalL5X)!;
const xml = exportProgramToL5X(original);
const reImported = importL5X(xml);
expect(reImported).not.toBeNull();
expect(reImported!.rungs.length).toBe(original.rungs.length);
// Check that rung series match
for (let i = 0; i < original.rungs.length; i++) {
expect(reImported!.rungs[i].series.length).toBe(original.rungs[i].series.length);
}
});
});
});