#!/usr/bin/env node
/**
 * Phase 2 Stress Tests — Parallel Branch & Loop Resume
 * Tests edge cases and complex scenarios
 */

const { Engine, ExecutionContext } = require('../index');

let passed = 0, failed = 0;

function test(name, fn) {
  try {
    const result = fn();
    if (result && typeof result.then === 'function') {
      return result.then(() => {
        console.log(`  ✓ ${name}`);
        passed++;
      }).catch(e => {
        console.log(`  ✗ ${name}: ${e.message}`);
        failed++;
      });
    }
    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}`); }

async function runAll() {

// ═══════════════════════════════════════════════════════════════
console.log('\n── Parallel Branch Resume: Edge Cases ──');

await test('3 branches, 0 completed — runs all 3', async () => {
  const events = [];
  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' }
    ]
  }, { onEvent: e => events.push(e) });

  const ctx = await engine.executeFrom({
    currentStepID: 'Noop_Fork',
    variables: { '$a': null, '$b': null, '$c': null },
    completedBranches: {}  // nothing completed
  });
  assertEqual(ctx.variables['$a'], 10);
  assertEqual(ctx.variables['$b'], 20);
  assertEqual(ctx.variables['$c'], 30);
});

await test('3 branches, ALL completed — skips all, moves to next', async () => {
  const stepStarts = [];
  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: 'Set_Final' },
      { id: 'Set_A', target: '$a', value: '=99' },
      { id: 'Set_B', target: '$b', value: '=99' },
      { id: 'Set_C', target: '$c', value: '=99' },
      { id: 'Set_Final', target: '$a', value: '=777', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  }, { onEvent: e => { if (e.type === 'step_start') stepStarts.push(e.stepID); } });

  const ctx = await engine.executeFrom({
    currentStepID: 'Noop_Fork',
    variables: { '$a': 10, '$b': 20, '$c': 30 },
    completedBranches: { 'Noop_Fork': ['Set_A', 'Set_B', 'Set_C'] }
  });
  // All branches skipped, values unchanged from checkpoint (except Set_Final runs)
  assertEqual(ctx.variables['$a'], 777, '$a should be set by Set_Final');
  assertEqual(ctx.variables['$b'], 20, '$b should be unchanged');
  assertEqual(ctx.variables['$c'], 30, '$c should be unchanged');
  // Set_A, Set_B, Set_C should NOT appear in step_starts
  assert(!stepStarts.includes('Set_A'), 'Set_A should not have started');
  assert(!stepStarts.includes('Set_B'), 'Set_B should not have started');
  assert(!stepStarts.includes('Set_C'), 'Set_C should not have started');
  assert(stepStarts.includes('Set_Final'), 'Set_Final should have started');
});

await test('single child branch — also tracks completion', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$x(INT)'] },
    steps: [
      { id: 'Noop_Single', children: ['Set_X'], next: 'Stop_End' },
      { id: 'Set_X', target: '$x', value: '=42' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });

  await engine.execute({});
  const lastCp = checkpoints[checkpoints.length - 1];
  assert(lastCp.completedBranches['Noop_Single'], 'should track single child');
  assert(lastCp.completedBranches['Noop_Single'].includes('Set_X'), 'Set_X should be completed');
});

await test('branch with chain (Set_A → Set_A2), partial completion re-runs chain', async () => {
  // Set_A's chain: Set_A → Set_A2 (via next)
  // If Set_A branch is NOT in completedBranches, the whole chain re-runs
  const stepStarts = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$a(INT)', '$a2(INT)', '$b(INT)'] },
    steps: [
      { id: 'Noop_Fork', children: ['Set_A', 'Set_B'], next: 'Stop_End' },
      { id: 'Set_A', target: '$a', value: '=1', next: 'Set_A2' },
      { id: 'Set_A2', target: '$a2', value: '=2' },  // end of chain (RETURN)
      { id: 'Set_B', target: '$b', value: '=3' },
      { id: 'Stop_End' }
    ]
  }, { onEvent: e => { if (e.type === 'step_start') stepStarts.push(e.stepID); } });

  // Only Set_B completed, Set_A chain did NOT complete
  const ctx = await engine.executeFrom({
    currentStepID: 'Noop_Fork',
    variables: { '$a': null, '$a2': null, '$b': 3 },
    completedBranches: { 'Noop_Fork': ['Set_B'] }
  });
  // Set_A chain should have re-run
  assertEqual(ctx.variables['$a'], 1, 'Set_A should have run');
  assertEqual(ctx.variables['$a2'], 2, 'Set_A2 should have run');
  assertEqual(ctx.variables['$b'], 3, 'Set_B should NOT have re-run');
  assert(stepStarts.includes('Set_A'), 'Set_A should have started');
  assert(stepStarts.includes('Set_A2'), 'Set_A2 should have started');
  assert(!stepStarts.includes('Set_B'), 'Set_B should NOT have started');
});

// ═══════════════════════════════════════════════════════════════
console.log('\n── Loop Resume: Edge Cases ──');

await test('loop with 0 progress — runs all iterations', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$items([INT])', '$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' }
    ]
  });

  const ctx = await engine.executeFrom({
    currentStepID: 'Loop_001',
    variables: { '$items': [1, 2, 3, 4, 5], '$total': 0 },
    loopProgress: {}  // no progress
  });
  assertEqual(ctx.variables['$total'], 5);
});

await test('loop fully completed — runs 0 iterations', async () => {
  const stepStarts = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$items([INT])', '$total(INT)'] },
    steps: [
      { id: 'Loop_001', source: '$items', mode: 'serial', children: ['Set_Inc'], next: 'Set_Done' },
      { id: 'Set_Inc', target: '$total', value: '=$total + 1' },
      { id: 'Set_Done', target: '$total', value: '=$total + 100', next: 'Stop_End' },
      { id: 'Stop_End' }
    ]
  }, { onEvent: e => { if (e.type === 'step_start') stepStarts.push(e.stepID); } });

  const ctx = await engine.executeFrom({
    currentStepID: 'Loop_001',
    variables: { '$items': [1, 2, 3], '$total': 3 },
    loopProgress: { 'Loop_001': 3 }  // all 3 done
  });
  // Loop body should NOT execute, but next (Set_Done) should
  assertEqual(ctx.variables['$total'], 103, 'should skip loop, run Set_Done');
  assert(!stepStarts.includes('Set_Inc'), 'Set_Inc should not have started');
  assert(stepStarts.includes('Set_Done'), 'Set_Done should have started');
});

await test('loop resume: iteration index is correct (_item and _index)', async () => {
  const items = [];
  const indices = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$data([STRING])', '$log([STRING])'] },
    steps: [
      { id: 'Loop_Log', source: '$data', mode: 'serial', children: ['Set_Collect'], next: 'Stop_End' },
      { id: 'Set_Collect', target: '$log', value: '=$log' },  // dummy
      { id: 'Stop_End' }
    ]
  }, {
    customHandlers: {
      Set: (engine, ctx, step) => {
        // Intercept to capture _item and _index
        items.push(ctx.getVariable('_item'));
        indices.push(ctx.getVariable('_index'));
        const { ExpressionEvaluator } = require('../lib/expression');
        const ev = new ExpressionEvaluator(ctx);
        ev.setVariable(step.target, ev.evaluateValue(step.value));
      }
    }
  });

  const ctx = await engine.executeFrom({
    currentStepID: 'Loop_Log',
    variables: { '$data': ['a', 'b', 'c', 'd', 'e'], '$log': [] },
    loopProgress: { 'Loop_Log': 2 }  // skip 'a' and 'b'
  });
  // Should have processed items at index 2,3,4
  assertEqual(items.length, 3, 'should process 3 items');
  assertEqual(items[0], 'c');
  assertEqual(items[1], 'd');
  assertEqual(items[2], 'e');
  assertEqual(indices[0], 2);
  assertEqual(indices[1], 3);
  assertEqual(indices[2], 4);
});

await test('serial loop: checkpoint per iteration', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', 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_Sum' },
      { id: 'Loop_Sum', source: '$items', mode: 'serial', children: ['Set_Add'], next: 'Stop_End' },
      { id: 'Set_Add', target: '$total', value: '=$total + _item' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });

  await engine.execute({});
  // Filter checkpoints that have loop progress for Loop_Sum
  const loopCps = checkpoints.filter(cp => cp.loopProgress['Loop_Sum'] > 0);
  // Should have at least 5 checkpoints from loop iterations
  assert(loopCps.length >= 5, `should have >=5 loop checkpoints, got ${loopCps.length}`);
  // Verify progressive loop progress
  const progresses = loopCps.map(cp => cp.loopProgress['Loop_Sum']);
  for (let i = 1; i < progresses.length; i++) {
    assert(progresses[i] >= progresses[i-1], 'loop progress should be non-decreasing');
  }
  assertEqual(progresses[progresses.length - 1], 5, 'final progress should be 5');
});

await test('parallel loop resume from mid-point — correct items', async () => {
  // Use a deterministic approach: parallel loop with Set that adds _item to $sum
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$nums([INT])', '$sum(INT)'] },
    steps: [
      { id: 'Loop_PSum', source: '$nums', mode: 'parallel', children: ['Set_Sum'], next: 'Stop_End' },
      { id: 'Set_Sum', target: '$sum', value: '=$sum + _item' },
      { id: 'Stop_End' }
    ]
  });

  // Items: [10, 20, 30, 40, 50]. First 2 completed (sum=30). Resume from index 2.
  const ctx = await engine.executeFrom({
    currentStepID: 'Loop_PSum',
    variables: { '$nums': [10, 20, 30, 40, 50], '$sum': 30 },
    loopProgress: { 'Loop_PSum': 2 }
  });
  // Should add 30+40+50 = 120 to existing 30
  assertEqual(ctx.variables['$sum'], 150);
});

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

await test('full workflow: linear → fork → loop → stop, checkpoint round-trip', async () => {
  const checkpoints = [];
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$a(INT)', '$b(INT)', '$items([INT])', '$total(INT)'] },
    steps: [
      { id: 'Set_Init', target: '$items', value: '=[1,2,3]', next: 'Noop_Fork' },
      { id: 'Noop_Fork', children: ['Set_A', 'Set_B'], next: 'Loop_Sum' },
      { id: 'Set_A', target: '$a', value: '=10' },
      { id: 'Set_B', target: '$b', value: '=20' },
      { id: 'Loop_Sum', source: '$items', mode: 'serial', children: ['Set_Add'], next: 'Stop_End' },
      { id: 'Set_Add', target: '$total', value: '=($total || 0) + _item' },
      { id: 'Stop_End' }
    ]
  }, { onCheckpoint: cp => checkpoints.push(cp) });

  const ctx = await engine.execute({});
  assertEqual(ctx.variables['$a'], 10);
  assertEqual(ctx.variables['$b'], 20);
  assertEqual(ctx.variables['$total'], 6); // 1+2+3

  // Verify checkpoint has all state
  const lastCp = checkpoints[checkpoints.length - 1];
  assert(lastCp.completedBranches['Noop_Fork'], 'fork branches tracked');
  assertEqual(lastCp.loopProgress['Loop_Sum'], 3, 'loop fully tracked');

  // Round-trip: resume from Loop_Sum with partial progress
  const resumeCp = JSON.parse(JSON.stringify(lastCp));
  resumeCp.currentStepID = 'Loop_Sum';
  resumeCp.variables['$total'] = 1;  // pretend only iteration 0 ran
  resumeCp.loopProgress['Loop_Sum'] = 1;  // completed 1 iteration

  const ctx2 = await engine.executeFrom(resumeCp);
  // Should run iterations 1,2 (values 2,3) adding 5 to existing 1
  assertEqual(ctx2.variables['$total'], 6); // 1 + 2 + 3
});

await test('resume from Noop_Fork with no completedBranches field at all', async () => {
  // Edge case: old v1 checkpoint without completedBranches
  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' }
    ]
  });

  // No completedBranches at all — should run everything
  const ctx = await engine.executeFrom({
    currentStepID: 'Noop_Fork',
    variables: { '$a': null, '$b': null }
    // no completedBranches field
  });
  assertEqual(ctx.variables['$a'], 1);
  assertEqual(ctx.variables['$b'], 2);
});

await test('resume from Loop with no loopProgress field at all', async () => {
  const engine = new Engine({
    version: '3.15', name: 'test',
    registry: { params: [], vars: ['$items([INT])', '$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' }
    ]
  });

  const ctx = await engine.executeFrom({
    currentStepID: 'Loop_001',
    variables: { '$items': [1, 2, 3], '$total': 0 }
    // no loopProgress field
  });
  assertEqual(ctx.variables['$total'], 3);
});

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

}

runAll();
