import fs from 'fs/promises';
import path from 'path';
import { WorkflowExecutor } from './src/vl/workflow-executor.js';
import { prepareCheckpointForRerun } from './src/server/routes/workflow.js';
import { ToolRegistry } from './src/core/tool-registry.js';
import { createReadFileTool } from './src/tools/read-file.js';

let passed = 0;
let failed = 0;
function test(name, fn) {
  return Promise.resolve()
    .then(fn)
    .then(() => {
      console.log(`  ✓ ${name}`);
      passed++;
    })
    .catch((err) => {
      console.log(`  ✗ ${name}: ${err.message}`);
      failed++;
    });
}
function assertEqual(a, b, msg) {
  if (a !== b) throw new Error(msg || `Expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);
}
function assert(cond, msg) {
  if (!cond) throw new Error(msg || 'Assertion failed');
}

const tmpDir = '/tmp/vlcode-lite-workflow-executor-test';
const outputPath = path.join(tmpDir, 'rerun-output.txt');
const complexDir = path.join(tmpDir, 'complex-rerun');
const complexSummaryPath = path.join(complexDir, 'Artifacts/summary.txt');
const complexAuditPath = path.join(complexDir, 'Artifacts/audit.txt');
const registryReadPath = path.join(tmpDir, 'tool-read.txt');
const workflow = {
  version: '3.16',
  name: 'ExecutorRerunSmoke',
  steps: [
    { id: 'Set_Default', target: '$msg', value: '=\"first\"', next: 'Write_Output' },
    { id: 'Write_Output', target: '/rerun-output.txt', value: '=$msg', mode: 'overwrite', next: 'Stop_End' },
    { id: 'Stop_End' },
  ],
};
const actualRegistryToolWorkflow = {
  version: '3.16',
  name: 'ExecutorActualRegistryTool',
  steps: [
    {
      id: 'Tool_010_ReadFile',
      input: { file_path: '="tool-read.txt"' },
      out: {
        '$toolRead': '=_result',
        '$toolReadRaw': '=_toolResult.result',
      },
      next: 'Stop_End',
    },
    { id: 'Stop_End' },
  ],
};
const complexWorkflow = {
  version: '3.16',
  name: 'ExecutorComplexRerun',
  steps: [
    { id: 'Set_010_Numbers', target: '$nums', value: '=[2,4,6,8]', next: 'Set_020_Sum' },
    { id: 'Set_020_Sum', target: '$sum', value: '=0', next: 'Set_030_Audit' },
    { id: 'Set_030_Audit', target: '$audit', value: '=[]', next: 'Fork_040_Prepare' },
    { id: 'Fork_040_Prepare', children: ['Set_041_BranchA', 'Set_042_BranchB'], next: 'Loop_050_Sum' },
    { id: 'Set_041_BranchA', target: '$branchA', value: '=\"branch-A-ready\"', next: 'Write_043_BranchA' },
    { id: 'Write_043_BranchA', target: '=\"Artifacts/branch-a.txt\"', value: '=$branchA' },
    { id: 'Set_042_BranchB', target: '$branchB', value: '=\"branch-B-ready\"', next: 'Write_044_BranchB' },
    { id: 'Write_044_BranchB', target: '=\"Artifacts/branch-b.txt\"', value: '=$branchB' },
    { id: 'Loop_050_Sum', source: '$nums', mode: 'serial', children: ['Set_051_Add', 'Set_052_AuditEntry'], next: 'Pause_060_Review' },
    { id: 'Set_051_Add', target: '$sum', value: '=$sum + _item' },
    { id: 'Set_052_AuditEntry', target: '$audit[_index]', value: '=\"i=\" + _index + \";n=\" + _item + \";sum=\" + $sum' },
    { id: 'Pause_060_Review', reason: 'Review', next: 'Fork_070_Finalize' },
    { id: 'Fork_070_Finalize', children: ['Write_071_Summary', 'Write_072_Audit'], next: 'Stop_080_End' },
    { id: 'Write_071_Summary', target: '=\"Artifacts/summary.txt\"', value: '=\"sum=\" + $sum + \";count=\" + $nums.length' },
    { id: 'Write_072_Audit', target: '=\"Artifacts/audit.txt\"', value: '=$audit' },
    { id: 'Stop_080_End' },
  ],
};
const toolWorkflow = {
  version: '3.16',
  name: 'ExecutorToolStep',
  steps: [
    {
      id: 'Tool_010_EchoTool',
      tool: 'EchoTool',
      input: { text: '="hello"', nested: { source: '="workflow"' } },
      out: {
        '$echoed': '=_result.echoed',
        '$toolSource': '=_result.meta.source',
      },
      next: 'Stop_End',
    },
    { id: 'Stop_End' },
  ],
};
const toolAllowErrorWorkflow = {
  version: '3.16',
  name: 'ExecutorToolAllowError',
  steps: [
    {
      id: 'Tool_010_FailTool',
      tool: 'FailTool',
      allowError: true,
      input: { reason: '="expected"' },
      out: {
        '$toolOk': '=_result.ok',
        '$toolError': '=_result.error',
      },
      next: 'Stop_End',
    },
    { id: 'Stop_End' },
  ],
};
const toolMessageWorkflow = {
  version: '3.16',
  name: 'ExecutorToolMessage',
  steps: [
    {
      id: 'Tool_010_MessageTool',
      tool: 'MessageTool',
      input: { query: '="inspect"' },
      out: {
        '$toolOk': '=_result.ok',
      },
      next: 'Stop_End',
    },
    { id: 'Stop_End' },
  ],
};

console.log('\n── Workflow Executor Regression ──');

await fs.mkdir(tmpDir, { recursive: true });
await fs.rm(outputPath, { force: true });
await fs.rm(complexDir, { recursive: true, force: true });
await fs.mkdir(complexDir, { recursive: true });
await fs.writeFile(registryReadPath, 'registry-tool-ok', 'utf8');

await test('executeFrom handles checkpoint.artifacts object for rerun overrides', async () => {
  let checkpoint = null;
  const first = new WorkflowExecutor({ workDir: tmpDir, model: 'claude-opus-4-6' });
  await first.execute(workflow, {}, {
    onCheckpoint: (cp) => { checkpoint = cp; },
  });

  assert(checkpoint, 'checkpoint missing after initial execution');
  assertEqual(await fs.readFile(outputPath, 'utf8'), 'first');
  assert(checkpoint.artifacts && !Array.isArray(checkpoint.artifacts), 'checkpoint.artifacts should be an object');

  const rerun = new WorkflowExecutor({ workDir: tmpDir, model: 'claude-opus-4-6' });
  await rerun.executeFrom(
    workflow,
    { ...checkpoint, currentStepID: 'Write_Output' },
    { '$msg': 'edited' },
    {}
  );

  assertEqual(await fs.readFile(outputPath, 'utf8'), 'edited');
});

await test('prepareCheckpointForRerun clears fork/loop resume state for complex reruns', async () => {
  let checkpoint = null;
  const first = new WorkflowExecutor({ workDir: complexDir, model: 'claude-opus-4-6' });
  let resumed = false;

  await first.execute(complexWorkflow, {}, {
    onCheckpoint: (cp) => { checkpoint = cp; },
    onPause(info) {
      if (resumed) return;
      resumed = true;
      setTimeout(() => first.resume(info.nodeId), 10);
    },
  });

  assert(checkpoint, 'complex checkpoint missing after initial execution');
  assertEqual(await fs.readFile(complexSummaryPath, 'utf8'), 'sum=20;count=4');

  const forkCheckpoint = prepareCheckpointForRerun(complexWorkflow, checkpoint, 'Fork_070_Finalize');
  const forkRerun = new WorkflowExecutor({ workDir: complexDir, model: 'claude-opus-4-6' });
  await forkRerun.executeFrom(complexWorkflow, forkCheckpoint, { '$sum': 99 }, {});
  assertEqual(await fs.readFile(complexSummaryPath, 'utf8'), 'sum=99;count=4');

  const loopCheckpoint = prepareCheckpointForRerun(complexWorkflow, checkpoint, 'Loop_050_Sum');
  const loopRerun = new WorkflowExecutor({ workDir: complexDir, model: 'claude-opus-4-6' });
  let rerunPaused = false;
  await loopRerun.executeFrom(complexWorkflow, loopCheckpoint, {
    '$nums': [3, 3, 3],
    '$sum': 0,
    '$audit': [],
  }, {
    onPause(info) {
      if (rerunPaused) return;
      rerunPaused = true;
      setTimeout(() => loopRerun.resume(info.nodeId), 10);
    },
  });

  assertEqual(await fs.readFile(complexSummaryPath, 'utf8'), 'sum=9;count=3');
  assertEqual(
    await fs.readFile(complexAuditPath, 'utf8'),
    '["i=0;n=3;sum=3","i=1;n=3;sum=6","i=2;n=3;sum=9"]'
  );
});

await test('Tool_* executes local tool registry and maps _result outputs', async () => {
  const calls = [];
  const toolRegistry = {
    async execute(name, input) {
      calls.push({ name, input });
      return { result: { echoed: String(input.text).toUpperCase(), meta: { source: input.nested?.source || '' } } };
    },
  };
  const executor = new WorkflowExecutor({ workDir: tmpDir, model: 'claude-opus-4-6', toolRegistry });
  const events = [];

  await executor.execute(toolWorkflow, {}, {
    onToolStart: (info) => events.push({ type: 'start', ...info }),
    onToolDone: (info) => events.push({ type: 'done', ...info }),
  });

  assertEqual(calls.length, 1, 'tool should be invoked once');
  assertEqual(calls[0].name, 'EchoTool');
  assertEqual(calls[0].input.text, 'hello');
  assertEqual(calls[0].input.nested.source, 'workflow');
  assertEqual(executor._ctx.variables.$echoed, 'HELLO');
  assertEqual(executor._ctx.variables.$toolSource, 'workflow');
  assert(events.some((event) => event.type === 'start' && event.name === 'EchoTool'), 'missing tool start event');
  assert(events.some((event) => event.type === 'done' && event.name === 'EchoTool'), 'missing tool done event');
});

await test('Tool_* allowError captures tool failure into _result without aborting workflow', async () => {
  const toolRegistry = {
    async execute() {
      return { error: 'tool-failed' };
    },
  };
  const executor = new WorkflowExecutor({ workDir: tmpDir, model: 'claude-opus-4-6', toolRegistry });
  const errors = [];

  await executor.execute(toolAllowErrorWorkflow, {}, {
    onToolError: (info) => errors.push(info),
  });

  assertEqual(executor._ctx.variables.$toolOk, false);
  assertEqual(executor._ctx.variables.$toolError, 'tool-failed');
  assert(errors.some((event) => event.name === 'FailTool' && event.allowError === true), 'missing allowError tool event');
});

await test('Tool_* executes an actual registered VLCode tool via ToolRegistry', async () => {
  const toolRegistry = new ToolRegistry();
  toolRegistry.register('ReadFile', createReadFileTool({ workDir: tmpDir }));
  const executor = new WorkflowExecutor({ workDir: tmpDir, model: 'claude-opus-4-6', toolRegistry });

  await executor.execute(actualRegistryToolWorkflow, {}, {});

  assert(String(executor._ctx.variables.$toolRead || '').includes('registry-tool-ok'), 'ReadFile result missing expected content');
  assert(String(executor._ctx.variables.$toolReadRaw || '').includes('registry-tool-ok'), 'raw tool result missing expected content');
});

await test('Tool_* forwards tool runtime messages through onToolMessage', async () => {
  const toolRegistry = new ToolRegistry();
  toolRegistry.register('MessageTool', {
    description: 'Emit runtime messages for workflow testing',
    parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
    async execute(input, runtime) {
      runtime.info?.('starting inspection', { query: input.query });
      runtime.progress?.('halfway', { percent: 50 });
      return { result: { ok: true } };
    },
  });
  const executor = new WorkflowExecutor({ workDir: tmpDir, model: 'claude-opus-4-6', toolRegistry });
  const messages = [];

  await executor.execute(toolMessageWorkflow, {}, {
    onToolMessage: (info) => messages.push(info),
  });

  assertEqual(executor._ctx.variables.$toolOk, true);
  assert(messages.some((info) => info.name === 'MessageTool' && info.message === 'starting inspection'), 'missing info tool message');
  assert(messages.some((info) => info.name === 'MessageTool' && info.message === 'halfway'), 'missing progress tool message');
});

await test('DocCenter path 1 is pinned to bundled local VL syntax', async () => {
  const originalFetch = globalThis.fetch;
  const executor = new WorkflowExecutor({ workDir: tmpDir, model: 'claude-opus-4-6', cookie: 'test-cookie' });
  const docs = { '1': 'remote syntax placeholder', '100': 'remote meta prompt placeholder' };

  globalThis.fetch = async (url, options = {}) => {
    if (String(url).includes('SERVICE_DocCenter_GetDocList')) {
      return {
        async json() {
          return {
            data: [
              { path: '1', _id: 1 },
              { path: '100', _id: 100 },
            ],
          };
        },
      };
    }
    const body = JSON.parse(options.body || '{}');
    return {
      async json() {
        return {
          data: {
            currentContent: body.docId === 1
              ? 'Current version: `// VL_VERSION:3.6`'
              : 'remote meta prompt',
          },
        };
      },
    };
  };

  try {
    await executor._resolveDocCenterDocs(docs, { onText() {} });
  } finally {
    globalThis.fetch = originalFetch;
  }

  assert(docs['1'].includes('Current version: `// VL_VERSION:3.5`'), 'bundled local syntax should override remote path 1');
  assertEqual(docs['100'], 'remote meta prompt');
});

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