#!/usr/bin/env node
/**
 * VL Workflow Engine — Test Suite
 */

const { Engine, ExpressionEvaluator, ExecutionContext, Registry,
        parseParamDeclaration, parseVariableDeclaration, parseServiceSignature } = require('../index');
const fs = require('fs');
const path = require('path');

let passed = 0, failed = 0;

function test(name, fn) {
  try {
    fn();
    console.log(`  ✓ ${name}`);
    passed++;
  } catch (e) {
    console.log(`  ✗ ${name}: ${e.message}`);
    failed++;
  }
}

function assert(cond, msg) { if (!cond) throw new Error(msg || 'Assertion failed'); }
function assertEqual(a, b, msg) { if (a !== b) throw new Error(msg || `Expected ${b}, got ${a}`); }

// ═══════════════════════════════════════════════════════════════
console.log('\n── Expression Evaluator ──');

test('literal string', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: {}, variables: {} });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('hello'), 'hello');
});

test('escape ==', () => {
  const ctx = new ExecutionContext({ workflowID: 'test' });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('==foo'), '=foo');
});

test('variable reference', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { name: 'Dragon' } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=name'), 'Dragon');
});

test('$var reference', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', variables: { '$count': 42 } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=$count'), 42);
});

test('nested property', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', variables: { '$obj': { a: { b: 3 } } } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=$obj.a.b'), 3);
});

test('array index', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', variables: { '$arr': [10, 20, 30] } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=$arr[1]'), 20);
});

test('.length', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', variables: { '$arr': [1, 2, 3] } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=$arr.length'), 3);
});

test('arithmetic', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { x: 10 } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=x + 5'), 15);
});

test('string concatenation', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { name: 'VL' } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('="Hello " + name'), 'Hello VL');
});

test('comparison ==', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { x: 5 } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=x == 5'), true);
});

test('ternary', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { x: true } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=x ? "yes" : "no"'), 'yes');
});

test('logical AND/OR', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { a: true, b: false } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=a && b'), false);
  assertEqual(ev.evaluateValue('=a || b'), true);
});

test('NOT', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { a: false } });
  const ev = new ExpressionEvaluator(ctx);
  assertEqual(ev.evaluateValue('=!a'), true);
});

test('setVariable deep path', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', variables: { '$data': {} } });
  const ev = new ExpressionEvaluator(ctx);
  ev.setVariable('$data.name', 'test');
  assertEqual(ctx.variables['$data'].name, 'test');
});

test('evaluateDeep', () => {
  const ctx = new ExecutionContext({ workflowID: 'test', params: { x: 10 } });
  const ev = new ExpressionEvaluator(ctx);
  const result = ev.evaluateDeep({ a: '=x', b: 'literal', c: [1, '=x'] });
  assertEqual(result.a, 10);
  assertEqual(result.b, 'literal');
  assertEqual(result.c[1], 10);
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── Registry ──');

test('parseParamDeclaration', () => {
  const d = parseParamDeclaration('userId(STRING)');
  assertEqual(d.name, 'userId');
  assertEqual(d.type, 'STRING');
});

test('parseParamDeclaration with default', () => {
  const d = parseParamDeclaration('maxRetries(INT) = 3');
  assertEqual(d.name, 'maxRetries');
  assertEqual(d.default, 3);
});

test('parseVariableDeclaration', () => {
  const d = parseVariableDeclaration('$items([OBJECT])');
  assertEqual(d.name, '$items');
  assertEqual(d.type, '[OBJECT]');
});

test('parseServiceSignature', () => {
  const s = parseServiceSignature('PlannerService(prd(STRING)) RETURN plan(OBJECT)');
  assertEqual(s.name, 'PlannerService');
  assertEqual(s.parameters.length, 1);
  assertEqual(s.returns.length, 1);
});

test('Registry lookup', () => {
  const reg = new Registry({
    docs: { '1': 'VL Syntax' },
    apis: [{ id: 'api1', method: 'GET', url: '/test' }],
    schemas: { 'S1': { type: 'object' } }
  });
  assert(reg.hasDoc('1'));
  assert(reg.hasAPI('api1'));
  assert(reg.hasSchema('S1'));
  assertEqual(reg.getDocDescription('1'), 'VL Syntax');
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── Engine ──');

test('validate meta-direct workflow', () => {
  const wfPath = path.join(__dirname, '../../VLClaw/workflows/meta-direct.json');
  if (!fs.existsSync(wfPath)) { console.log('    (skipped: workflow file not found)'); return; }
  const wf = JSON.parse(fs.readFileSync(wfPath, 'utf8'));
  const engine = new Engine(wf);
  const errors = engine.validate();
  assert(errors.length === 0, `Validation errors: ${errors.join('; ')}`);
});

test('validate 6file-codegen workflow', () => {
  const wfPath = path.join(__dirname, '../../VLClaw/workflows/6file-codegen.json');
  if (!fs.existsSync(wfPath)) { console.log('    (skipped: workflow file not found)'); return; }
  const wf = JSON.parse(fs.readFileSync(wfPath, 'utf8'));
  const engine = new Engine(wf);
  const errors = engine.validate();
  assert(errors.length === 0, `Validation errors: ${errors.join('; ')}`);
});

test('find entry nodes', () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: [] },
    steps: [
      { id: 'Set_001', target: '$x', value: '=1', next: 'Set_002' },
      { id: 'Set_002', target: '$y', value: '=2', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const entries = engine._findEntryNodeIDs();
  assertEqual(entries.length, 1);
  assertEqual(entries[0], 'Set_001');
});

test('execute simple Set → Stop workflow', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['x(INT)'], vars: ['$result(INT)'] },
    steps: [
      { id: 'Set_001', target: '$result', value: '=x + 10', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({ x: 5 });
  assertEqual(ctx.variables['$result'], 15);
});

test('execute Branch workflow', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['mode(STRING)'], vars: ['$out(STRING)'] },
    steps: [
      { id: 'Branch_001', cases: [['=mode == "a"', 'Set_A'], ['ELSE', 'Set_B']], next: 'Stop_End' },
      { id: 'Set_A', target: '$out', value: '="chose A"', next: 'RETURN' },
      { id: 'Set_B', target: '$out', value: '="chose B"', next: 'RETURN' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({ mode: 'a' });
  assertEqual(ctx.variables['$out'], 'chose A');
});

test('execute Check_* (condition/if_true/if_false)', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['hasErrors(BOOL)'], vars: ['$out(STRING)'] },
    steps: [
      { id: 'Check_NeedRepair', condition: '=hasErrors', if_true: 'Set_Repair', if_false: 'Set_OK', next: 'Stop_End' },
      { id: 'Set_Repair', target: '$out', value: '="repair"', next: 'RETURN' },
      { id: 'Set_OK', target: '$out', value: '="ok"', next: 'RETURN' },
      { id: 'Stop_End' }
    ]
  });
  const ctx1 = await engine.execute({ hasErrors: true });
  assertEqual(ctx1.variables['$out'], 'repair');
  const ctx2 = await engine.execute({ hasErrors: false });
  assertEqual(ctx2.variables['$out'], 'ok');
});

test('execute Loop workflow (serial)', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$items([STRING])', '$total(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$items', value: '=["a","b","c"]', next: 'Set_Total' },
      { id: 'Set_Total', target: '$total', value: '=0', next: 'Loop_001' },
      { id: 'Loop_001', source: '$items', mode: 'serial', children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$total', value: '=$total + 1' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$total'], 3);
});

test('execute parallel children', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$a(INT)', '$b(INT)'] },
    steps: [
      { id: 'Noop_Fork', children: ['Set_A', 'Set_B'], next: 'Stop_End' },
      { id: 'Set_A', target: '$a', value: '=1' },
      { id: 'Set_B', target: '$b', value: '=2' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$a'], 1);
  assertEqual(ctx.variables['$b'], 2);
});

test('event emission', async () => {
  const events = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Set_001', target: '$x', value: '=42', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  // Patch to capture events
  const origExecute = engine.execute.bind(engine);
  const ctx = await engine.execute({});
  // Events were emitted on ctx
  // Let's test with listener
  const events2 = [];
  const engine2 = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Set_001', target: '$x', value: '=42', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  // We need to hook before execute... redesign needed
  // For now just verify it doesn't crash
  assert(ctx.status === 'stopped');
});

test('conditional skip (step.if)', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['skip(BOOL)'], vars: ['$x(INT)'] },
    steps: [
      { id: 'Set_001', if: '=!skip', target: '$x', value: '=99', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({ skip: true });
  assertEqual(ctx.variables['$x'], null); // Skipped, stays null
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── Checkpoint & ExecuteFrom ──');

test('checkpoint produces serializable JSON', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['x(INT)'], vars: ['$result(INT)'] },
    steps: [
      { id: 'Set_001', target: '$result', value: '=x + 10', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({ x: 5 });
  const cp = ctx.checkpoint();
  assert(cp._type === 'vl_workflow_checkpoint', 'checkpoint type');
  assert(cp._version === 1, 'checkpoint version');
  assertEqual(cp.params.x, 5);
  assertEqual(cp.variables['$result'], 15);
  assert(cp.completedSteps.includes('Set_001'), 'Set_001 in completedSteps');
  // Should survive JSON round-trip
  const cp2 = JSON.parse(JSON.stringify(cp));
  assertEqual(cp2.variables['$result'], 15);
});

test('onCheckpoint callback fires after each step', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$a(INT)', '$b(INT)'] },
    steps: [
      { id: 'Set_A', target: '$a', value: '=1', next: 'Set_B' },
      { id: 'Set_B', target: '$b', value: '=2', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });
  await engine.execute({});
  assertEqual(checkpoints.length, 2); // Set_A and Set_B (Stop doesn't emit)
  assertEqual(checkpoints[0].completedSteps.length, 1);
  assertEqual(checkpoints[1].completedSteps.length, 2);
});

test('executeFrom resumes from a specific step', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['x(INT)'], vars: ['$a(INT)', '$b(INT)'] },
    steps: [
      { id: 'Set_A', target: '$a', value: '=x + 1', next: 'Set_B' },
      { id: 'Set_B', target: '$b', value: '=$a + 10', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  // Simulate: Set_A already ran, $a = 6, now resume from Set_B
  const ctx = await engine.executeFrom({
    currentStepID: 'Set_B',
    params: { x: 5 },
    variables: { '$a': 6, '$b': null }
  });
  assertEqual(ctx.variables['$b'], 16); // $a(6) + 10
  assertEqual(ctx.variables['$a'], 6);  // Unchanged — Set_A was not re-run
});

test('executeFrom with variable overrides', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['x(INT)'], vars: ['$a(INT)', '$b(INT)'] },
    steps: [
      { id: 'Set_A', target: '$a', value: '=x + 1', next: 'Set_B' },
      { id: 'Set_B', target: '$b', value: '=$a + 10', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  // Resume from Set_B but override $a to 100
  const ctx = await engine.executeFrom(
    { currentStepID: 'Set_B', params: { x: 5 }, variables: { '$a': 6, '$b': null } },
    {},
    { '$a': 100 }  // override
  );
  assertEqual(ctx.variables['$b'], 110); // overridden $a(100) + 10
});

test('executeFrom with checkpoint round-trip', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: ['x(INT)'], vars: ['$a(INT)', '$b(INT)'] },
    steps: [
      { id: 'Set_A', target: '$a', value: '=x + 1', next: 'Set_B' },
      { id: 'Set_B', target: '$b', value: '=$a + 10', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });

  await engine.execute({ x: 5 });
  // checkpoint after Set_A: $a=6, $b=null, currentStepID should point to next
  const cpAfterA = JSON.parse(JSON.stringify(checkpoints[0]));
  // Modify: set currentStepID to Set_B to resume from there
  cpAfterA.currentStepID = 'Set_B';
  const ctx2 = await engine.executeFrom(cpAfterA);
  assertEqual(ctx2.variables['$b'], 16);
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── Phase 2: Parallel & Loop Resume ──');

test('parallel branches: checkpoint tracks completed branches', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$a(INT)', '$b(INT)'] },
    steps: [
      { id: 'Noop_Fork', children: ['Set_A', 'Set_B'], next: 'Stop_End' },
      { id: 'Set_A', target: '$a', value: '=1' },
      { id: 'Set_B', target: '$b', value: '=2' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });
  await engine.execute({});
  // Should have checkpoints from both branches
  const lastCp = checkpoints[checkpoints.length - 1];
  assert(lastCp.completedBranches['Noop_Fork'], 'should track Fork branches');
  const forkBranches = lastCp.completedBranches['Noop_Fork'];
  assert(forkBranches.includes('Set_A'), 'Set_A should be completed');
  assert(forkBranches.includes('Set_B'), 'Set_B should be completed');
});

test('parallel branches: resume skips completed branches', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$a(INT)', '$b(INT)', '$c(INT)'] },
    steps: [
      { id: 'Noop_Fork', children: ['Set_A', 'Set_B', 'Set_C'], next: 'Stop_End' },
      { id: 'Set_A', target: '$a', value: '=10' },
      { id: 'Set_B', target: '$b', value: '=20' },
      { id: 'Set_C', target: '$c', value: '=30' },
      { id: 'Stop_End' }
    ]
  });

  // Simulate: branches A and B completed, C did not
  const ctx = await engine.executeFrom({
    currentStepID: 'Noop_Fork',
    variables: { '$a': 10, '$b': 20, '$c': null },
    completedSteps: ['Set_A', 'Set_B'],
    completedBranches: { 'Noop_Fork': ['Set_A', 'Set_B'] }
  });
  // Set_A and Set_B should not be re-run (values should stay)
  // Set_C should have been executed
  assertEqual(ctx.variables['$c'], 30);
  // $a and $b should be their original checkpoint values (not re-run)
  assertEqual(ctx.variables['$a'], 10);
  assertEqual(ctx.variables['$b'], 20);
});

test('loop: checkpoint tracks iteration progress', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$items([STRING])', '$total(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$items', value: '=["a","b","c","d","e"]', next: 'Set_Total' },
      { id: 'Set_Total', target: '$total', value: '=0', next: 'Loop_001' },
      { id: 'Loop_001', source: '$items', mode: 'serial', children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$total', value: '=$total + 1' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });

  await engine.execute({});
  // Loop should have recorded progress
  const lastCp = checkpoints[checkpoints.length - 1];
  assertEqual(lastCp.loopProgress['Loop_001'], 5);
});

test('loop: resume from mid-iteration', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$items([STRING])', '$total(INT)'] },
    steps: [
      { id: 'Loop_001', source: '$items', mode: 'serial', children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$total', value: '=$total + 1' },
      { id: 'Stop_End' }
    ]
  });

  // Simulate: loop ran 3 out of 5 iterations, crashed
  const ctx = await engine.executeFrom({
    currentStepID: 'Loop_001',
    variables: { '$items': ['a', 'b', 'c', 'd', 'e'], '$total': 3 },
    loopProgress: { 'Loop_001': 3 }  // completed 0,1,2
  });
  // Should only run iterations 3 and 4 (adding 2 more)
  assertEqual(ctx.variables['$total'], 5); // 3 + 2 = 5
});

test('loop: resume parallel loop from mid-point', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$items([INT])', '$sum(INT)'] },
    steps: [
      { id: 'Loop_P', source: '$items', mode: 'parallel', children: ['Set_Add'], next: 'Stop_End' },
      { id: 'Set_Add', target: '$sum', value: '=$sum + _item' },
      { id: 'Stop_End' }
    ]
  });

  // Simulate: parallel loop processed first 3 items (sum=1+2+3=6), crashed
  const ctx = await engine.executeFrom({
    currentStepID: 'Loop_P',
    variables: { '$items': [1, 2, 3, 4, 5], '$sum': 6 },
    loopProgress: { 'Loop_P': 3 }
  });
  // Should only run items at index 3,4 (values 4,5), adding 9 to existing 6
  assertEqual(ctx.variables['$sum'], 15); // 6 + 4 + 5 = 15
});

test('checkpoint v2 round-trip with parallel and loop state', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$a(INT)', '$b(INT)'] },
    steps: [
      { id: 'Noop_Fork', children: ['Set_A', 'Set_B'], next: 'Stop_End' },
      { id: 'Set_A', target: '$a', value: '=1' },
      { id: 'Set_B', target: '$b', value: '=2' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });

  await engine.execute({});
  const lastCp = checkpoints[checkpoints.length - 1];
  // Verify v2 format
  assertEqual(lastCp._version, 2);
  // JSON round-trip should preserve completedBranches and loopProgress
  const cp2 = JSON.parse(JSON.stringify(lastCp));
  assert(Array.isArray(cp2.completedBranches['Noop_Fork']), 'completedBranches survives JSON');
  assert(typeof cp2.loopProgress === 'object', 'loopProgress survives JSON');
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── v3.16 Features ──');

test('validate: Loop while/source mutual exclusion', () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Loop_Bad', while: '=$x < 10', source: '$items', children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$x', value: '=$x + 1' },
      { id: 'Stop_End' }
    ]
  });
  const errors = engine.validate();
  assert(errors.some(e => e.includes('mutually exclusive')), 'should reject while + source');
});

test('validate: Loop without while or source is allowed (legacy compat)', () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: [] },
    steps: [
      { id: 'Loop_Empty', children: ['Noop_Child'], next: 'Stop_End' },
      { id: 'Noop_Child' },
      { id: 'Stop_End' }
    ]
  });
  const errors = engine.validate();
  assert(!errors.some(e => e.includes('Loop_Empty')), 'Loop without while/source should be allowed');
});

test('validate: BREAK only inside Loop children', () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Set_001', target: '$x', value: '=1', next: 'BREAK' },
      { id: 'Stop_End' }
    ]
  });
  const errors = engine.validate();
  assert(errors.some(e => e.includes('BREAK is only valid inside Loop')), 'should reject BREAK outside loop');
});

test('validate: BREAK inside Loop children is OK', () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$items([INT])', '$x(INT)'] },
    steps: [
      { id: 'Loop_001', source: '$items', children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$x', value: '=$x + 1', next: 'BREAK' },
      { id: 'Stop_End' }
    ]
  });
  const errors = engine.validate();
  assert(!errors.some(e => e.includes('BREAK')), 'BREAK inside Loop children should be valid');
});

test('execute Loop while mode', async () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$x', value: '=0', next: 'Loop_While' },
      { id: 'Loop_While', while: '=$x < 5', children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$x', value: '=$x + 1' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$x'], 5);
});

test('execute Loop while with maxIterations', async () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$x', value: '=0', next: 'Loop_While' },
      { id: 'Loop_While', while: '=$x < 100', maxIterations: 3, children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$x', value: '=$x + 1' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$x'], 3); // capped at 3
});

test('execute BREAK exits loop early (serial)', async () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$items([INT])', '$sum(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$items', value: '=[1,2,3,4,5]', next: 'Set_Sum' },
      { id: 'Set_Sum', target: '$sum', value: '=0', next: 'Loop_001' },
      { id: 'Loop_001', source: '$items', mode: 'serial', children: ['Check_Break'], next: 'Stop_End' },
      { id: 'Check_Break', condition: '=_item >= 4', if_true: 'Noop_Break', if_false: 'Set_Add' },
      { id: 'Noop_Break', next: 'BREAK' },
      { id: 'Set_Add', target: '$sum', value: '=$sum + _item', next: 'RETURN' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$sum'], 6); // 1+2+3 = 6, item 4 triggers BREAK
});

test('execute BREAK in while loop', async () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$x', value: '=0', next: 'Loop_While' },
      { id: 'Loop_While', while: '=true', maxIterations: 100, children: ['Set_Inc', 'Check_Break'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$x', value: '=$x + 1', next: 'Check_Break' },
      { id: 'Check_Break', condition: '=$x >= 3', if_true: 'Noop_Break', if_false: 'RETURN' },
      { id: 'Noop_Break', next: 'BREAK' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$x'], 3); // BREAK at x=3
});

test('execute Loop source with maxIterations cap', async () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$items([INT])', '$total(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$items', value: '=[1,2,3,4,5]', next: 'Set_Total' },
      { id: 'Set_Total', target: '$total', value: '=0', next: 'Loop_001' },
      { id: 'Loop_001', source: '$items', mode: 'serial', maxIterations: 3, children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$total', value: '=$total + 1' },
      { id: 'Stop_End' }
    ]
  });
  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$total'], 3); // only 3 of 5 items
});

test('LLM model field: provider/modelId parsing', async () => {
  let capturedParams = null;
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$out(STRING)'] },
    steps: [
      { id: 'LLM_Test', model: 'anthropic/claude-sonnet-4-20250514', in: { messages: [{ role: 'user', content: 'hi' }] }, out: '$out', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const mockLLM = {
    call: async (params) => {
      capturedParams = { ...params };
      return { content: 'hello', model: 'claude-sonnet-4-20250514', usage: {} };
    }
  };
  await engine.execute({}, { llm: mockLLM });
  assertEqual(capturedParams._provider, 'anthropic');
  assertEqual(capturedParams.model, 'claude-sonnet-4-20250514');
});

test('LLM model field: provider-only parsing', async () => {
  let capturedParams = null;
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$out(STRING)'] },
    steps: [
      { id: 'LLM_Test', model: 'openai', in: { messages: [{ role: 'user', content: 'hi' }] }, out: '$out', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const mockLLM = {
    call: async (params) => {
      capturedParams = { ...params };
      return { content: 'hello', model: 'gpt-4o', usage: {} };
    }
  };
  await engine.execute({}, { llm: mockLLM });
  assertEqual(capturedParams._provider, 'openai');
  assert(!capturedParams.model, 'model should not be set for provider-only');
});

test('BREAK in _findEntryNodeIDs does not add as reference', () => {
  const engine = new Engine({
    version: '3.16', name: 'test',
    registry: { params: [], vars: ['$items([INT])'] },
    steps: [
      { id: 'Loop_001', source: '$items', children: ['Set_Inc'], next: 'Stop_End' },
      { id: 'Set_Inc', target: '$x', value: '=1', next: 'BREAK' },
      { id: 'Stop_End' }
    ]
  });
  const entries = engine._findEntryNodeIDs();
  assertEqual(entries.length, 1);
  assertEqual(entries[0], 'Loop_001');
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── User-Initiated Pause ──');

test('ctx.pause() stops execution after current step', async () => {
  const events = [];
  const engine = new Engine({
    version: '3.16', name: 'pause-test',
    registry: { params: [], vars: ['$a(STRING)', '$b(STRING)'] },
    steps: [
      { id: 'Set_A', target: '$a', value: 'hello', next: 'Set_B' },
      { id: 'Set_B', target: '$b', value: 'world', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  }, {
    onEvent: (e) => events.push(e),
    onCheckpoint: () => {}
  });

  // Hook: pause after Set_A step_done, before Set_B begins
  engine._onEvent = (e) => {
    events.push(e);
  };
  // Intercept: when engine tries to execute Set_B, call ctx.pause() first
  const origExec = engine.executeStep.bind(engine);
  let ctx_ref = null;
  engine.executeStep = async function(ctx, step) {
    ctx_ref = ctx;
    if (step.id === 'Set_B') ctx.pause();
    return origExec(ctx, step);
  };

  const ctx = await engine.execute();
  assertEqual(ctx.status, 'paused', 'Status should be paused');
  assertEqual(ctx.variables['$a'], 'hello', '$a should be set');
  assertEqual(ctx.variables['$b'], null, '$b should NOT be set (paused before)');

  // workflow_paused event should be emitted
  const pauseEvt = events.find(e => e.type === 'workflow_paused');
  assert(pauseEvt, 'workflow_paused event should be emitted');
});

test('pause → checkpoint → resume via executeFrom', async () => {
  const engine = new Engine({
    version: '3.16', name: 'pause-resume',
    registry: { params: [], vars: ['$a(STRING)', '$b(STRING)'] },
    steps: [
      { id: 'Set_A', target: '$a', value: 'hello', next: 'Set_B' },
      { id: 'Set_B', target: '$b', value: 'world', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });

  // Pause before Set_B
  const origExec = engine.executeStep.bind(engine);
  engine.executeStep = async function(ctx, step) {
    if (step.id === 'Set_B') ctx.pause();
    return origExec(ctx, step);
  };

  const ctx = await engine.execute();
  assertEqual(ctx.status, 'paused');

  // Take checkpoint and resume from Set_B
  const cp = ctx.checkpoint();
  cp.currentStepID = 'Set_B';

  // Fresh engine (no hook) to resume
  const engine2 = new Engine({
    version: '3.16', name: 'pause-resume',
    registry: { params: [], vars: ['$a(STRING)', '$b(STRING)'] },
    steps: [
      { id: 'Set_A', target: '$a', value: 'hello', next: 'Set_B' },
      { id: 'Set_B', target: '$b', value: 'world', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const ctx2 = await engine2.executeFrom(cp, {});
  assertEqual(ctx2.variables['$a'], 'hello', '$a preserved from checkpoint');
  assertEqual(ctx2.variables['$b'], 'world', '$b set after resume');
});

test('pause → edit overrides → resume with modified input', async () => {
  const engine = new Engine({
    version: '3.16', name: 'pause-edit-resume',
    registry: { params: [], vars: ['$msg(STRING)', '$result(STRING)'] },
    steps: [
      { id: 'Set_Msg', target: '$msg', value: 'original', next: 'Set_Result' },
      { id: 'Set_Result', target: '$result', value: '=$msg', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });

  // Pause before Set_Result
  const origExec = engine.executeStep.bind(engine);
  engine.executeStep = async function(ctx, step) {
    if (step.id === 'Set_Result') ctx.pause();
    return origExec(ctx, step);
  };

  const ctx = await engine.execute();
  assertEqual(ctx.status, 'paused');
  assertEqual(ctx.variables['$msg'], 'original');

  // User edits $msg before resuming
  const cp = ctx.checkpoint();
  cp.currentStepID = 'Set_Result';

  const engine2 = new Engine({
    version: '3.16', name: 'pause-edit-resume',
    registry: { params: [], vars: ['$msg(STRING)', '$result(STRING)'] },
    steps: [
      { id: 'Set_Msg', target: '$msg', value: 'original', next: 'Set_Result' },
      { id: 'Set_Result', target: '$result', value: '=$msg', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  });
  const ctx2 = await engine2.executeFrom(cp, {}, { '$msg': 'user-edited' });
  assertEqual(ctx2.variables['$msg'], 'user-edited', '$msg should be overridden');
  assertEqual(ctx2.variables['$result'], 'user-edited', '$result should use edited $msg');
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── Results ──');
console.log(`\n  ${passed} passed, ${failed} failed\n`);
process.exit(failed > 0 ? 1 : 0);
