/**
 * VL AutoTest - Workflow Builder
 * Builds Spec 3.16 Workflow JSONs for the VL Workflow Engine.
 *
 * Three workflow types:
 *   1. Generation  — ParseAppContext → GenTestCases → Stop
 *   2. Evaluation  — EvalResults → GenReport → Stop
 *   3. Fix         — AnalyzeFailures → GenVLFixes → Stop
 *
 * Each build*() returns { wfJson, params } where:
 *   - wfJson is the static workflow structure (nodes, edges, registry)
 *   - params is the dynamic key-value map to pass as runParams.params
 */

import { PARSE_CONTEXT_SYSTEM, GEN_CASES_SYSTEM } from './test-generator.js';
import { EVAL_SYSTEM } from './test-evaluator.js';
import { TEST_FIX_SYSTEM, VL_FIX_SYSTEM } from './auto-fixer.js';

export class WorkflowBuilder {

  // ─── Phase 1: Generation Workflow ──────────────────────────────────

  /**
   * Build the Generation workflow: ParseAppContext → GenTestCases → Stop
   *
   * @param {Object} opts
   * @param {string} opts.metadataContext - buildMetadataContext() output
   * @param {string} opts.baseUrl - Application preview URL
   * @param {number} opts.maxCases - Max test cases to generate (default 8)
   * @param {string} opts.appId - Current app identifier
   * @param {string} opts.componentInfo - Component info block (instanceIds etc.)
   * @returns {{ wfJson: Object, params: Object }}
   */
  static buildGenerationWorkflow({ metadataContext, baseUrl, maxCases = 8, appId = '', componentInfo = '' }) {
    const wfJson = {
      version: '3.16',
      name: `AutoTest_Generate_${appId || 'App'}`,
      description: 'AutoTest generation workflow: parse app structure -> generate test cases',
      registry: {
        params: [
          'parseSystemPrompt(STRING)',
          'genSystemPrompt(STRING)',
          'metadataCtx(STRING)',
          'componentInfo(STRING)',
          'baseUrl(STRING)',
          'maxCases(STRING)',
          'appId(STRING)',
        ],
        services: [],
        apis: [],
        components: [],
        vars: [
          '$planRaw(STRING)',
          '$casesRaw(STRING)',
        ],
        files: { inputs: [], artifacts: [] },
      },
      steps: [
        {
          id: 'LLM_ParseAppContext',
          meta: { title: 'Parse app structure + extract component IDs' },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=parseSystemPrompt' },
              { role: 'user', content: '=metadataCtx' },
            ],
          },
          out: { '$planRaw': '=_result' },
          print: '="TestPlan generation complete (" + $planRaw.length + " chars)"',
          next: 'LLM_GenTestCases',
        },
        {
          id: 'LLM_GenTestCases',
          meta: { title: 'Generate test cases (based on component IDs)' },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=genSystemPrompt' },
              {
                role: 'user',
                content: '="TestPlan:\\n" + $planRaw + "\\n\\nAvailable component info:\\n" + componentInfo + "\\n\\nApp URL: " + baseUrl + "\\nGenerate at most " + maxCases + " workflow test cases"',
              },
            ],
          },
          out: { '$casesRaw': '=_result' },
          print: '="TestCases generation complete (" + $casesRaw.length + " chars)"',
          next: 'Stop_Done',
        },
        {
          id: 'Stop_Done',
          meta: { title: 'Done' },
        },
      ],
    };

    const params = {
      parseSystemPrompt: PARSE_CONTEXT_SYSTEM,
      genSystemPrompt: GEN_CASES_SYSTEM,
      metadataCtx: metadataContext,
      componentInfo: componentInfo,
      baseUrl: baseUrl || '',
      maxCases: String(maxCases),
      appId: appId || '',
    };

    return { wfJson, params };
  }

  /**
   * Build the Parse-only workflow: ParseAppContext → Stop
   *
   * @param {Object} opts
   * @param {string} opts.metadataContext
   * @param {string} opts.appId
   * @returns {{ wfJson: Object, params: Object }}
   */
  static buildParseAppContextWorkflow({ metadataContext, appId = '' }) {
    const wfJson = {
      version: '3.16',
      name: `AutoTest_Parse_${appId || 'App'}`,
      description: 'AutoTest parse workflow: extract page structure and navigation plan',
      registry: {
        params: [
          'parseSystemPrompt(STRING)',
          'metadataCtx(STRING)',
        ],
        services: [],
        apis: [],
        components: [],
        vars: [
          '$planRaw(STRING)',
        ],
        files: { inputs: [], artifacts: [] },
      },
      steps: [
        {
          id: 'LLM_ParseAppContext',
          meta: { title: 'Parse app structure + extract component IDs' },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=parseSystemPrompt' },
              { role: 'user', content: '=metadataCtx' },
            ],
          },
          out: { '$planRaw': '=_result' },
          print: '="TestPlan generation complete (" + $planRaw.length + " chars)"',
          next: 'Stop_Done',
        },
        {
          id: 'Stop_Done',
          meta: { title: 'Done' },
        },
      ],
    };

    return {
      wfJson,
      params: {
        parseSystemPrompt: PARSE_CONTEXT_SYSTEM,
        metadataCtx: metadataContext,
      },
    };
  }

  /**
   * Plan case-generation targets for parallel fanout.
   * Prefers page-level parallelism; falls back to critical paths when there is
   * only one page but multiple critical paths. Result count is capped by maxCases.
   *
   * @param {Object} testPlan
   * @param {number} maxCases
   * @returns {Array<{id:string, kind:string, label:string, instruction:string, maxCases:number}>}
   */
  static planCaseGenerationTargets(testPlan, maxCases = 8) {
    const safeMaxCases = Math.max(1, Number(maxCases) || 1);
    const pages = Array.isArray(testPlan?.pages)
      ? testPlan.pages.filter((page) => page?.path)
      : [];
    const criticalPaths = Array.isArray(testPlan?.criticalPaths)
      ? testPlan.criticalPaths.filter((cp) => cp?.name)
      : [];

    let rawTargets = [];
    if (pages.length > 1) {
      rawTargets = pages.map((page, index) => ({
        id: this._sanitizeTargetId(`page_${index + 1}_${page.path}`),
        kind: 'page',
        label: page.name || page.path,
        instruction: `Generate only workflow test cases for page "${page.path}". Use this page's navPath when navigation is required. Avoid duplicating flows that belong to other pages.`,
      }));
    } else if (criticalPaths.length > 1) {
      rawTargets = criticalPaths.map((cp, index) => ({
        id: this._sanitizeTargetId(`critical_${index + 1}_${cp.name}`),
        kind: 'criticalPath',
        label: cp.name,
        instruction: `Generate only workflow test cases for the critical path "${cp.name}". Prioritize this path's intended end-to-end behavior.`,
      }));
    } else {
      rawTargets = [{
        id: 'app',
        kind: 'app',
        label: testPlan?.appName || testPlan?.appId || 'App',
        instruction: 'Generate the highest-value workflow test cases for the whole app.',
      }];
    }

    if (rawTargets.length > safeMaxCases) {
      rawTargets = rawTargets.slice(0, safeMaxCases);
    }

    const base = Math.floor(safeMaxCases / rawTargets.length);
    const remainder = safeMaxCases % rawTargets.length;

    return rawTargets.map((target, index) => ({
      ...target,
      maxCases: Math.max(1, base + (index < remainder ? 1 : 0)),
    }));
  }

  /**
   * Build a Generate-only workflow for an already parsed TestPlan.
   * When multiple targets are planned, it fans out into parallel LLM branches.
   *
   * @param {Object} opts
   * @param {Object} opts.testPlan
   * @param {string} opts.baseUrl
   * @param {number} opts.maxCases
   * @param {string} opts.appId
   * @param {string} opts.componentInfo
   * @returns {{ wfJson: Object, params: Object, outputVars: string[], targets: Array }}
   */
  static buildCaseGenerationWorkflow({ testPlan, baseUrl, maxCases = 8, appId = '', componentInfo = '', targets = null }) {
    const plannedTargets = Array.isArray(targets) && targets.length > 0
      ? targets
      : this.planCaseGenerationTargets(testPlan, maxCases);
    const targetsToUse = plannedTargets.map((target) => ({ ...target }));
    if (targetsToUse.length > 1) {
      return this.buildParallelCaseGenerationWorkflow({ testPlan, baseUrl, maxCases, appId, componentInfo, targets: targetsToUse });
    }

    const [target] = targetsToUse;
    const outputVar = '$casesRaw';
    const wfJson = {
      version: '3.16',
      name: `AutoTest_GenCases_${appId || 'App'}`,
      description: 'AutoTest case generation workflow for a parsed TestPlan',
      registry: {
        params: [
          'genSystemPrompt(STRING)',
          'testPlanStr(STRING)',
          'componentInfo(STRING)',
          'baseUrl(STRING)',
          'targetInstruction(STRING)',
        ],
        services: [],
        apis: [],
        components: [],
        vars: [
          `${outputVar}(STRING)`,
        ],
        files: { inputs: [], artifacts: [] },
      },
      steps: [
        {
          id: 'LLM_GenTestCases',
          meta: { title: `Generate test cases (${target.label})` },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=genSystemPrompt' },
              {
                role: 'user',
                content: '="TestPlan:\\n" + testPlanStr + "\\n\\nAvailable component info:\\n" + componentInfo + "\\n\\nApp URL: " + baseUrl + "\\n" + targetInstruction',
              },
            ],
          },
          out: { [outputVar]: '=_result' },
          print: `="TestCases generation complete (${target.label}, " + ${outputVar}.length + " chars)"`,
          next: 'Stop_Done',
        },
        {
          id: 'Stop_Done',
          meta: { title: 'Done' },
        },
      ],
    };

    return {
      wfJson,
      params: {
        genSystemPrompt: GEN_CASES_SYSTEM,
        testPlanStr: JSON.stringify(testPlan),
        componentInfo: componentInfo || '',
        baseUrl: baseUrl || '',
        targetInstruction: this._buildTargetInstruction(target),
      },
      outputVars: [outputVar],
      targets: targetsToUse,
    };
  }

  /**
   * Build a parallel fanout workflow for per-page/per-path case generation.
   *
   * @param {Object} opts
   * @param {Object} opts.testPlan
   * @param {string} opts.baseUrl
   * @param {number} opts.maxCases
   * @param {string} opts.appId
   * @param {string} opts.componentInfo
   * @param {Array}  opts.targets
   * @returns {{ wfJson: Object, params: Object, outputVars: string[], targets: Array }}
   */
  static buildParallelCaseGenerationWorkflow({ testPlan, baseUrl, maxCases = 8, appId = '', componentInfo = '', targets = null }) {
    const plannedTargets = Array.isArray(targets) && targets.length > 0
      ? targets
      : this.planCaseGenerationTargets(testPlan, maxCases);

    if (plannedTargets.length <= 1) {
      return this.buildCaseGenerationWorkflow({ testPlan, baseUrl, maxCases, appId, componentInfo, targets: plannedTargets });
    }

    const outputVars = [];
    const params = {
      genSystemPrompt: GEN_CASES_SYSTEM,
      testPlanStr: JSON.stringify(testPlan),
      componentInfo: componentInfo || '',
      baseUrl: baseUrl || '',
    };

    const steps = [{
      id: 'Fork_GenCaseTargets',
      meta: { title: `Generate test cases in parallel (${plannedTargets.length} targets)` },
      children: [],
      next: 'Stop_Done',
    }];

    for (const [index, target] of plannedTargets.entries()) {
      const stepSuffix = String(index + 1).padStart(3, '0');
      const stepId = `LLM_GenTestCases_${stepSuffix}`;
      const outputVar = `$casesRaw_${target.id || stepSuffix}`;
      const instructionParam = `targetInstruction_${stepSuffix}`;

      outputVars.push(outputVar);
      params[instructionParam] = this._buildTargetInstruction(target);
      steps[0].children.push(stepId);
      steps.push({
        id: stepId,
        meta: { title: `Generate ${target.kind} cases: ${target.label}` },
        in: {
          stream: true,
          messages: [
            { role: 'system', content: '=genSystemPrompt' },
            {
              role: 'user',
              content: `="TestPlan:\\n" + testPlanStr + "\\n\\nAvailable component info:\\n" + componentInfo + "\\n\\nApp URL: " + baseUrl + "\\n" + ${instructionParam}`,
            },
          ],
        },
        out: { [outputVar]: '=_result' },
        print: `="TestCases generation complete (${target.label}, " + ${outputVar}.length + " chars)"`,
        next: 'RETURN',
      });
    }

    steps.push({
      id: 'Stop_Done',
      meta: { title: 'Done' },
    });

    const wfJson = {
      version: '3.16',
      name: `AutoTest_GenCasesParallel_${appId || 'App'}`,
      description: 'AutoTest case generation workflow with page/path fanout',
      registry: {
        params: [
          'genSystemPrompt(STRING)',
          'testPlanStr(STRING)',
          'componentInfo(STRING)',
          'baseUrl(STRING)',
          ...plannedTargets.map((_, index) => `targetInstruction_${String(index + 1).padStart(3, '0')}(STRING)`),
        ],
        services: [],
        apis: [],
        components: [],
        vars: outputVars.map((name) => `${name}(STRING)`),
        files: { inputs: [], artifacts: [] },
      },
      steps,
    };

    return { wfJson, params, outputVars, targets: plannedTargets };
  }

  static _buildTargetInstruction(target) {
    return `Generate at most ${target.maxCases} workflow test cases.\n${target.instruction}`;
  }

  static _sanitizeTargetId(value) {
    return String(value || 'target')
      .replace(/[^a-zA-Z0-9]+/g, '_')
      .replace(/^_+|_+$/g, '')
      .slice(0, 48) || 'target';
  }

  // ─── Phase 2: Evaluation Workflow ──────────────────────────────────

  /**
   * Build the Evaluation workflow: EvalResults → GenReport → Stop
   *
   * @param {Object} opts
   * @param {Array}  opts.testCases - Test case definitions
   * @param {Array}  opts.runResults - Playwright execution results
   * @param {string} opts.appName - Application name for report header
   * @param {Array}  opts.componentIds - Component IDs list for coverage stats
   * @returns {{ wfJson: Object, params: Object }}
   */
  static buildEvaluationWorkflow({ testCases, runResults, appName = '', componentIds = [] }) {
    // Build condensed evaluation input (only failed/ambiguous cases)
    const evalInput = runResults.map(r => {
      const tc = testCases.find(t => t.id === r.caseId);
      if (!tc) return null;

      const stepsInfo = r.steps?.map((s, i) =>
        `Step ${i + 1}: ${s.action} "${s.selector || ''}" - ${s.status}${s.error ? ` (${s.error})` : ''}`
      ).join('\n') || '[no step info]';

      const snapshotInfo = r.snapshots?.length > 0
        ? r.snapshots.map(s => `[${s.type}]:\n${s.snapshot?.slice(0, 2000)}`).join('\n\n')
        : '[no snapshots]';

      return {
        caseId: tc.id,
        name: tc.name,
        priority: tc.priority,
        page: tc.page,
        expectedResult: tc.expectedResult,
        pass: r.pass,
        error: r.error || null,
        stepsInfo,
        snapshotInfo: snapshotInfo.slice(0, 3000),
      };
    }).filter(Boolean);

    const wfJson = {
      version: '3.16',
      name: 'AutoTest_Evaluate',
      description: 'AutoTest evaluation workflow: evaluate test results -> generate report',
      registry: {
        params: [
          'evalSystemPrompt(STRING)',
          'evalInput(STRING)',
          'reportSystemPrompt(STRING)',
          'reportContext(STRING)',
        ],
        services: [],
        apis: [],
        components: [],
        vars: [
          '$evalRaw(STRING)',
          '$reportRaw(STRING)',
        ],
        files: { inputs: [], artifacts: [] },
      },
      steps: [
        {
          id: 'LLM_EvalResults',
          meta: { title: 'Evaluate test results' },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=evalSystemPrompt' },
              { role: 'user', content: '=evalInput' },
            ],
          },
          out: { '$evalRaw': '=_result' },
          print: '="Evaluation complete (" + $evalRaw.length + " chars)"',
          next: 'LLM_GenReport',
        },
        {
          id: 'LLM_GenReport',
          meta: { title: 'Generate test report' },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=reportSystemPrompt' },
              { role: 'user', content: '="Evaluation results:\\n" + $evalRaw + "\\n\\n" + reportContext' },
            ],
          },
          out: { '$reportRaw': '=_result' },
          print: '="Report generation complete"',
          next: 'Stop_Done',
        },
        {
          id: 'Stop_Done',
          meta: { title: 'Done' },
        },
      ],
    };

    // Report generation system prompt
    const reportSystemPrompt = `Generate a detailed Markdown test report based on evaluation results. Structure:
1. **Summary**: app name, test time, pass rate
2. **Statistics table**: total/passed/failed, grouped by priority
3. **Per-case result table**: ID / Name / Priority / Page / Status / Reason
4. **Failed case detailed analysis**: steps and error messages for each failed case
5. **Component ID coverage stats**: which components were tested, which were not covered
6. **Improvement suggestions**

Write clearly and professionally. Output the Markdown report directly, no code fence.`;

    const passCount = runResults.filter(r => r.pass).length;
    const failCount = runResults.length - passCount;

    const params = {
      evalSystemPrompt: EVAL_SYSTEM,
      evalInput: JSON.stringify(evalInput, null, 0),
      reportSystemPrompt,
      reportContext: `App: ${appName}\nPassed: ${passCount}, Failed: ${failCount}\nComponent ID list: ${componentIds.join(', ')}`,
    };

    return { wfJson, params };
  }

  // ─── Phase 3: Fix Workflow ────────────────────────────────────────

  /**
   * Build the Fix workflow: AnalyzeFailures → GenVLFixes → Stop
   *
   * @param {Object} opts
   * @param {string} opts.failureSummary - JSON string of failure analysis data
   * @param {string} opts.vlSourcesContext - Relevant VL source file contents
   * @param {string} opts.metadataContext - Metadata context string
   * @returns {{ wfJson: Object, params: Object }}
   */
  static buildFixWorkflow({ failureSummary, vlSourcesContext = '', metadataContext = '' }) {
    const wfJson = {
      version: '3.16',
      name: 'AutoTest_Fix',
      description: 'Auto-fix workflow: analyze failures -> generate fix suggestions',
      registry: {
        params: [
          'testFixPrompt(STRING)',
          'vlFixPrompt(STRING)',
          'failureSummary(STRING)',
          'vlSources(STRING)',
          'metadataCtx(STRING)',
        ],
        services: [],
        apis: [],
        components: [],
        vars: [
          '$testFixRaw(STRING)',
          '$vlFixRaw(STRING)',
        ],
        files: { inputs: [], artifacts: [] },
      },
      steps: [
        {
          id: 'LLM_AnalyzeFailures',
          meta: { title: 'Analyze test failure causes' },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=testFixPrompt' },
              { role: 'user', content: '=failureSummary' },
            ],
          },
          out: { '$testFixRaw': '=_result' },
          print: '="Failure analysis complete"',
          next: 'LLM_GenVLFixes',
        },
        {
          id: 'LLM_GenVLFixes',
          meta: { title: 'Generate VL source code fixes' },
          in: {
            stream: true,
            messages: [
              { role: 'system', content: '=vlFixPrompt' },
              {
                role: 'user',
                content: '="Failure analysis results:\\n" + $testFixRaw + "\\n\\nVL source files:\\n" + vlSources + "\\n\\nApp structure:\\n" + metadataCtx',
              },
            ],
          },
          out: { '$vlFixRaw': '=_result' },
          print: '="VL fix generation complete"',
          next: 'Stop_Done',
        },
        {
          id: 'Stop_Done',
          meta: { title: 'Done' },
        },
      ],
    };

    const params = {
      testFixPrompt: TEST_FIX_SYSTEM,
      vlFixPrompt: VL_FIX_SYSTEM,
      failureSummary,
      vlSources: vlSourcesContext,
      metadataCtx: metadataContext,
    };

    return { wfJson, params };
  }

  // ─── Visualization: Full Pipeline DAG ─────────────────────────────

  /**
   * Build a visualization-only workflow that represents the complete
   * AutoTest pipeline (including local-execution pseudo-nodes).
   * Used by the DAG viewer — not submitted to the engine.
   *
   * @returns {Object} Spec 3.16 workflow JSON
   */
  static buildVisualizationWorkflow() {
    return {
      version: '3.16',
      name: 'AutoTest_Pipeline',
      description: 'VL AutoTest full pipeline (including local execution nodes)',
      registry: {
        params: [],
        services: [],
        apis: [],
        components: [],
        vars: [],
        files: { inputs: [], artifacts: [] },
      },
      steps: [
        {
          id: 'Local_Upload',
          meta: { title: 'ZIP upload + extract', local: true },
          next: 'Local_Metadata',
        },
        {
          id: 'Local_Metadata',
          meta: { title: 'Metadata extraction', local: true },
          next: 'Local_Compile',
        },
        {
          id: 'Local_Compile',
          meta: { title: 'VL compile', local: true },
          next: 'Local_NavDiscovery',
        },
        {
          id: 'Local_NavDiscovery',
          meta: { title: 'Navigation discovery', local: true },
          next: 'LLM_ParseAppContext',
        },
        {
          id: 'LLM_ParseAppContext',
          meta: { title: 'Parse app structure (WF Engine)' },
          next: 'LLM_GenTestCases',
        },
        {
          id: 'LLM_GenTestCases',
          meta: { title: 'Generate test cases (WF Engine)' },
          next: 'Local_PlaywrightExec',
        },
        {
          id: 'Local_PlaywrightExec',
          meta: { title: 'Playwright test execution', local: true },
          next: 'LLM_EvalResults',
        },
        {
          id: 'LLM_EvalResults',
          meta: { title: 'Evaluate test results (WF Engine)' },
          next: 'LLM_GenReport',
        },
        {
          id: 'LLM_GenReport',
          meta: { title: 'Generate test report (WF Engine)' },
          next: 'Branch_HasFailures',
        },
        {
          id: 'Branch_HasFailures',
          meta: { title: 'Any failed cases?' },
          next: 'Stop_Done',
          children: ['LLM_AnalyzeFailures'],
        },
        {
          id: 'LLM_AnalyzeFailures',
          meta: { title: 'Analyze failure causes (WF Engine)' },
          parent: 'Branch_HasFailures',
          next: 'LLM_GenVLFixes',
        },
        {
          id: 'LLM_GenVLFixes',
          meta: { title: 'Generate VL fixes (WF Engine)' },
          next: 'Stop_Done',
        },
        {
          id: 'Stop_Done',
          meta: { title: 'Done' },
        },
      ],
    };
  }

  // ─── Test Case → Workflow 3.16 (for DAG visualization) ──────────

  /**
   * Map test step action to a valid Spec 3.16 node ID prefix.
   * Spec 3.16 + VLCode extension supports: Service_, API_, Component_, LLM_, Tool_, Set_, Write_, Branch_, Loop_, Stop_, Pause_, Fork_
   */
  static _actionToPrefix(action) {
    switch (action) {
      case 'navigate':       return 'Set_Navigate';
      case 'click':          return 'Set_Click';
      case 'fill':           return 'Set_Fill';
      case 'press':          return 'Set_Press';
      case 'hover':          return 'Set_Hover';
      case 'scroll':         return 'Set_Scroll';
      case 'wait':           return 'Set_Wait';
      case 'select':         return 'Set_Select';
      case 'assert_visible': return 'Branch_AssertVisible';
      case 'assert_text':    return 'Branch_AssertText';
      case 'assert_hidden':  return 'Branch_AssertHidden';
      case 'screenshot':     return 'Write_Screenshot';
      default:               return 'Set_Action';
    }
  }

  /**
   * Convert a test case (steps array) into a valid Workflow 3.16 JSON
   * for rendering in the workflow editor.
   *
   * Node IDs use valid Spec 3.16 prefixes:
   *   Set_   → navigate, click, fill, wait, press, hover, select, scroll
   *   Branch_→ assert_visible, assert_text, assert_hidden
   *   Write_ → screenshot
   *   Pause_ → pause/wait for user
   *   Fork_  → parallel branches
   *   Stop_  → end node
   *
   * @param {Object} testCase - Test case with .id, .name, .steps[]
   * @returns {Object} Spec 3.16 workflow JSON
   */
  static testCaseToWorkflow(testCase) {
    const testSteps = testCase.steps || [];

    // Generate unique node IDs with valid prefixes
    const nodeIds = testSteps.map((s, i) => `${this._actionToPrefix(s.action)}_${i + 1}`);

    const steps = testSteps.map((s, i) => {
      const nodeId = nodeIds[i];
      const nextId = i < testSteps.length - 1 ? nodeIds[i + 1] : 'Stop_Done';

      const step = {
        id: nodeId,
        meta: {
          title: s.description || `${s.action} ${s.selector || s.value || ''}`,
        },
        next: nextId,
      };

      // Set_ nodes use target + value
      if (nodeId.startsWith('Set_')) {
        step.target = s.selector ? `$selector_${i + 1}` : undefined;
        step.value = s.selector
          ? `="${s.action}: ${(s.selector || '').replace(/"/g, '\\"')}${s.value ? ' → ' + s.value.replace(/"/g, '\\"') : ''}"`
          : `="${s.action}${s.value ? ': ' + s.value.replace(/"/g, '\\"') : ''}"`;
      }

      // Branch_ nodes use cases
      if (nodeId.startsWith('Branch_')) {
        step.cases = [
          [`=${s.selector ? '"' + s.selector.replace(/"/g, '\\"') + '"' : '"element"'} visible`, nextId],
        ];
      }

      // Write_ nodes use target (file path) + value
      if (nodeId.startsWith('Write_')) {
        step.target = `/screenshots/step_${i + 1}.png`;
        step.value = '="screenshot saved"';
      }

      return step;
    });

    // Add Stop node (no next allowed per spec)
    steps.push({ id: 'Stop_Done', meta: { title: 'Done' } });

    return {
      version: '3.16',
      name: testCase.id,
      description: testCase.name,
      registry: {
        params: [],
        services: [],
        apis: [],
        components: [],
        vars: testSteps.map((s, i) =>
          s.selector ? `$selector_${i + 1}(STRING)` : null
        ).filter(Boolean),
        files: { inputs: [], artifacts: [] },
      },
      steps,
    };
  }
}
