import React, { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { 
  Settings2, 
  Play, 
  Download, 
  Plus, 
  Trash2, 
  CheckCircle2, 
  Loader2,
  ChevronRight,
  AlertCircle,
  Globe,
  ShieldCheck,
  Slash,
  Eye,
  Key,
  Activity,
  Clock,
  Hash,
  Coins,
  AlertTriangle
} from 'lucide-react';
import yaml from 'js-yaml';
import * as XLSX from 'xlsx';
import { toast } from 'sonner';

import { Toaster } from '@/components/ui/sonner';

import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';

import { DifyInput, TestCase, DifyConfig, AppMode, NodeTraceStep, NodeStatus, TestResult } from './types';

// Parse a Dify SSE stream (workflow/advanced-chat) into a TestResult.
// Emits live updates via onUpdate after each meaningful event so the UI
// can reflect node-level progress as it arrives.
async function consumeDifyStream(
  body: ReadableStream<Uint8Array>,
  appMode: AppMode,
  signal: AbortSignal,
  onUpdate: (partial: TestResult) => void
): Promise<TestResult> {
  const reader = body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  const trace: NodeTraceStep[] = [];
  const traceByExecId = new Map<string, NodeTraceStep>();
  let answer = '';
  let workflowRunId: string | undefined;
  let taskId: string | undefined;
  let messageId: string | undefined;
  let finalStatus: NodeStatus | string | undefined;
  let finalOutputs: any;
  let totalTokens: number | undefined;
  let elapsedTime: number | undefined;
  let streamError: string | undefined;

  const snapshot = (): TestResult => ({
    data: finalOutputs !== undefined ? { outputs: finalOutputs } : undefined,
    answer: appMode === 'chat' ? answer : undefined,
    task_id: taskId,
    id: messageId,
    workflowRunId,
    trace: trace.length ? [...trace] : undefined,
    finalStatus,
    totalTokens,
    elapsedTime,
    error: streamError
  });

  const handleEvent = (evt: any) => {
    const data = evt?.data ?? {};
    taskId = taskId || evt?.task_id;
    workflowRunId = workflowRunId || evt?.workflow_run_id || data?.workflow_run_id;
    switch (evt?.event) {
      case 'workflow_started': {
        trace.length = 0;
        traceByExecId.clear();
        workflowRunId = data.id || workflowRunId;
        break;
      }
      case 'node_started': {
        const step: NodeTraceStep = {
          execId: data.id,
          nodeId: data.node_id,
          nodeType: data.node_type,
          title: data.title ?? data.node_id,
          index: typeof data.index === 'number' ? data.index : trace.length + 1,
          status: 'running',
          inputs: data.inputs,
          startedAt: data.created_at
        };
        traceByExecId.set(step.execId, step);
        trace.push(step);
        break;
      }
      case 'node_finished': {
        const step = traceByExecId.get(data.id);
        const patch: Partial<NodeTraceStep> = {
          status: (data.status as NodeStatus) ?? 'succeeded',
          inputs: data.inputs ?? step?.inputs,
          outputs: data.outputs,
          error: data.error ?? null,
          elapsedTime: typeof data.elapsed_time === 'number' ? data.elapsed_time : undefined,
          tokens: data.execution_metadata?.total_tokens,
          finishedAt: data.finished_at
        };
        if (step) {
          Object.assign(step, patch);
        } else {
          trace.push({
            execId: data.id,
            nodeId: data.node_id,
            nodeType: data.node_type,
            title: data.title ?? data.node_id,
            index: typeof data.index === 'number' ? data.index : trace.length + 1,
            ...patch
          } as NodeTraceStep);
        }
        break;
      }
      case 'message': {
        if (typeof evt.answer === 'string') answer += evt.answer;
        messageId = messageId || evt.message_id || evt.id;
        break;
      }
      case 'message_end': {
        messageId = messageId || evt.message_id || evt.id;
        break;
      }
      case 'workflow_finished': {
        finalStatus = data.status;
        finalOutputs = data.outputs;
        totalTokens = typeof data.total_tokens === 'number' ? data.total_tokens : totalTokens;
        elapsedTime = typeof data.elapsed_time === 'number' ? data.elapsed_time : elapsedTime;
        if (data.error) streamError = data.error;
        break;
      }
      case 'error': {
        streamError = evt.message || 'Stream error';
        finalStatus = finalStatus || 'failed';
        break;
      }
      default:
        break;
    }
  };

  try {
    while (true) {
      if (signal.aborted) {
        try { await reader.cancel(); } catch {/* noop */}
        throw Object.assign(new Error('aborted'), { name: 'AbortError' });
      }
      const { value, done } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });

      let sep: number;
      // SSE frames end with blank line; tolerate both \n\n and \r\n\r\n
      while ((sep = buffer.search(/\r?\n\r?\n/)) !== -1) {
        const rawFrame = buffer.slice(0, sep);
        buffer = buffer.slice(sep + (buffer[sep] === '\r' ? 4 : 2));
        const dataLines: string[] = [];
        for (const line of rawFrame.split(/\r?\n/)) {
          if (!line || line.startsWith(':')) continue; // comment / keep-alive
          if (line.startsWith('data:')) {
            dataLines.push(line.slice(5).trimStart());
          }
        }
        if (!dataLines.length) continue;
        const payload = dataLines.join('\n');
        if (payload === '[DONE]') continue;
        try {
          const evt = JSON.parse(payload);
          handleEvent(evt);
          onUpdate(snapshot());
        } catch {
          // ignore malformed frames rather than blowing up the whole run
        }
      }
    }
  } catch (err: any) {
    if (err?.name === 'AbortError') throw err;
    streamError = streamError || err?.message || 'Stream read error';
  }

  return snapshot();
}

export default function App() {
  const [step, setStep] = useState(1);
  const [inputs, setInputs] = useState<DifyInput[]>([]);
  const [testCases, setTestCases] = useState<TestCase[]>([]);
  const [isExecuting, setIsExecuting] = useState(false);
  const [isStopping, setIsStopping] = useState(false);
  const [captureTrace, setCaptureTrace] = useState(false);
  const [traceDialogResult, setTraceDialogResult] = useState<TestResult | null>(null);
  const [difyConfig, setDifyConfig] = useState<DifyConfig>({
    apiKey: '',
    apiUrl: 'https://api.dify.ai/v1',
    appMode: 'chat'
  });

  const canCaptureTrace = difyConfig.appMode === 'workflow' || difyConfig.appMode === 'chat';

  const excelInputRef = useRef<HTMLInputElement>(null);
  const abortControllerRef = useRef<AbortController | null>(null);

  // Step 1: Fetch Parameters from API
  const fetchParameters = async () => {
    if (!difyConfig.apiKey || !difyConfig.apiUrl) {
      toast.error('API Key 和 API URL 必填');
      return;
    }

    setIsExecuting(true);
    const baseUrl = difyConfig.apiUrl.replace(/\/$/, '');

    try {
      // authoritative app mode detection using /info
      const infoResponse = await fetch(`${baseUrl}/info`, {
        headers: { 'Authorization': `Bearer ${difyConfig.apiKey}` }
      });
      
      let detectedMode: AppMode = 'workflow';
      if (infoResponse.ok) {
        const infoData = await infoResponse.json();
        // authoritative check: advanced-chat or workflow
        if (infoData.mode === 'advanced-chat') {
          detectedMode = 'chat';
        } else {
          detectedMode = 'workflow';
        }
      }

      if (detectedMode !== difyConfig.appMode) {
        toast.error(`模式冲突：检测到此应用为 ${detectedMode === 'chat' ? 'Advanced-Chat/ChatFlow' : 'Workflow'} 类型，而您选择了 ${difyConfig.appMode.toUpperCase()}。请更正后重试。`);
        return;
      }

      const response = await fetch(`${baseUrl}/parameters`, {
        headers: {
          'Authorization': `Bearer ${difyConfig.apiKey}`
        }
      });

      if (response.status === 404) {
        setInputs([]);
        setTestCases([{
          id: crypto.randomUUID(),
          inputs: {},
          times: 1,
          status: 'idle',
          progress: 0,
          results: []
        }]);
        setStep(2);
        toast.success(`已成功连接到 ${detectedMode === 'chat' ? 'Advanced-Chat' : 'Workflow'} 应用`);
        return;
      }

      const data = await response.json();
      if (!response.ok) throw new Error(data.message || 'Failed to fetch parameters');

      const extractedInputs: DifyInput[] = data.user_input_form?.map((item: any) => {
        const type = Object.keys(item)[0];
        const config = item[type];
        return {
          name: config.variable,
          type: type,
          label: config.label || config.variable
        };
      }) || [];

      if (extractedInputs.length === 0) {
        toast.info('未发现用户输入字段，但配置已加载。');
      }

      setInputs(extractedInputs);
      // Step 2 starts with an empty table as requested
      setTestCases([]);
      setStep(2);
      toast.success('API 连接成功，配置已加载');
    } catch (error: any) {
      console.error('Fetch error:', error);
      toast.error(error.message || '连接失败');
    } finally {
      setIsExecuting(false);
    }
  };

  // Step 2: Manage Test Cases
  const downloadExcelTemplate = () => {
    try {
      if (inputs.length === 0 && difyConfig.appMode !== 'chat') {
        toast.error('未检测到输入变量');
        return;
      }

      // Generate headers based on app mode and inputs
      const headers: string[] = [];
      if (difyConfig.appMode === 'chat') {
        headers.push('query');
      }
      
      inputs.forEach(input => {
        headers.push(input.label || input.name);
      });
      
      headers.push('times');

      // Create a single empty row as example
      const exampleRow = headers.map(h => {
        if (h === 'times') return 1;
        return '';
      });

      const ws = XLSX.utils.aoa_to_sheet([headers, exampleRow]);
      const wb = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(wb, ws, "测试模板");
      
      // Filename includes app mode for clarity
      const filename = `dify_${difyConfig.appMode}_template.xlsx`;
      XLSX.writeFile(wb, filename);
      toast.success('模板已生成并下载');
    } catch (error) {
      console.error('Template Download Error:', error);
      toast.error('模板生成失败');
    }
  };

  const handleExcelImport = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      try {
        const data = new Uint8Array(event.target?.result as ArrayBuffer);
        const workbook = XLSX.read(data, { type: 'array' });
        const sheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[sheetName];
        const jsonData = XLSX.utils.sheet_to_json(worksheet) as any[];

        if (jsonData.length === 0) {
          toast.error('Excel 文件为空');
          return;
        }

        const newTestCases: TestCase[] = jsonData.map(row => {
          const testCaseInputs: Record<string, any> = {};
          inputs.forEach(input => {
            // Match column by label or name
            const value = row[input.label] ?? row[input.name] ?? '';
            testCaseInputs[input.name] = value;
          });

          return {
            id: crypto.randomUUID(),
            inputs: testCaseInputs,
            query: difyConfig.appMode === 'chat' ? (row['query'] || row['Query'] || '') : undefined,
            times: parseInt(row['times'] || row['Times'] || '1') || 1,
            status: 'idle',
            progress: 0,
            results: []
          };
        });

        setTestCases(prev => [...prev, ...newTestCases]);
        toast.success(`已从 Excel 导入 ${newTestCases.length} 个测试用例`);
        // Reset input value to allow re-upload of same file
        if (excelInputRef.current) excelInputRef.current.value = '';
      } catch (error) {
        console.error('Excel import error:', error);
        toast.error('Excel 文件解析失败');
      }
    };
    reader.readAsArrayBuffer(file);
  };

  const addTestCase = () => {
    const newCase: TestCase = {
      id: crypto.randomUUID(),
      inputs: {},
      query: difyConfig.appMode === 'chat' ? '' : undefined,
      times: 1,
      status: 'idle',
      progress: 0,
      results: []
    };
    inputs.forEach(input => {
      newCase.inputs[input.name] = '';
    });
    setTestCases([...testCases, newCase]);
  };

  const addVariable = (name: string) => {
    if (!name || inputs.some(i => i.name === name)) return;
    const newVariable: DifyInput = {
      name,
      type: 'text',
      label: name
    };
    setInputs([...inputs, newVariable]);
    // Also update existing test cases to have this input key
    setTestCases(testCases.map(c => ({
      ...c,
      inputs: { ...c.inputs, [name]: '' }
    })));
  };

  const removeVariable = (name: string) => {
    setInputs(inputs.filter(i => i.name !== name));
    setTestCases(testCases.map(c => {
      const newInputs = { ...c.inputs };
      delete newInputs[name];
      return { ...c, inputs: newInputs };
    }));
  };

  const removeTestCase = (id: string) => {
    setTestCases(testCases.filter(c => c.id !== id));
  };

  const updateTestCase = (id: string, field: string, value: any, isInput = true) => {
    setTestCases(testCases.map(c => {
      if (c.id === id) {
        if (field === 'query') {
          return { ...c, query: value };
        }
        if (isInput) {
          return { ...c, inputs: { ...c.inputs, [field]: value } };
        } else {
          return { ...c, [field]: value };
        }
      }
      return c;
    }));
  };

  const stopTests = () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
      setIsStopping(true);
      toast.info('正在停止任务...');
    }
  };

  const runTests = async () => {
    if (!difyConfig.apiKey) {
      toast.error('必须输入 Dify API Key');
      return;
    }

    if (testCases.length === 0) {
      toast.error('请先添加至少一个测试用例');
      return;
    }

    setIsExecuting(true);
    setIsStopping(false);
    abortControllerRef.current = new AbortController();

    const updatedCases = [...testCases].map(c => ({ ...c, status: 'running' as const, progress: 0, results: [] as TestResult[] }));
    setTestCases(updatedCases);

    const baseUrl = difyConfig.apiUrl.replace(/\/$/, '');
    let endpoint = '';
    if (difyConfig.appMode === 'chat') {
      endpoint = `${baseUrl}/chat-messages`;
    } else if (difyConfig.appMode === 'completion') {
      endpoint = `${baseUrl}/completion-messages`;
    } else {
      endpoint = `${baseUrl}/workflows/run`;
    }

    // Only workflow + advanced-chat emit node-level events. Completion apps don't.
    const useStreaming = captureTrace && canCaptureTrace;
    let stoppedManually = false;

    for (let i = 0; i < updatedCases.length; i++) {
      if (abortControllerRef.current.signal.aborted) {
        stoppedManually = true;
        break;
      }

      const testCase = updatedCases[i];
      const times = testCase.times || 1;
      const results: TestResult[] = [];

      for (let t = 0; t < times; t++) {
        if (abortControllerRef.current.signal.aborted) {
          stoppedManually = true;
          break;
        }

        try {
          const body: any = {
            inputs: testCase.inputs,
            response_mode: useStreaming ? 'streaming' : 'blocking',
            user: 'batch-test-user'
          };

          if (difyConfig.appMode === 'chat') {
            body.query = testCase.query || '';
          }

          const response = await fetch(endpoint, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer ${difyConfig.apiKey}`
            },
            body: JSON.stringify(body),
            signal: abortControllerRef.current.signal
          });

          if (useStreaming) {
            if (!response.ok || !response.body) {
              let errMsg = `HTTP ${response.status}`;
              try {
                const errData = await response.json();
                errMsg = errData.message || errMsg;
              } catch {/* non-json */}
              throw new Error(errMsg);
            }

            const streamed = await consumeDifyStream(
              response.body,
              difyConfig.appMode,
              abortControllerRef.current.signal,
              (partial) => {
                // live update: publish in-flight trace so the UI ticks per node
                const partialResults: TestResult[] = [...results, partial];
                setTestCases(prev => prev.map(c => c.id === testCase.id ? {
                  ...c,
                  progress: Math.round(((t + (partial.trace?.length ? 0.5 : 0.1)) / times) * 100),
                  results: partialResults
                } : c));
              }
            );

            results.push(streamed);
          } else {
            const data = await response.json();
            if (!response.ok) throw new Error(data.message || 'API Error');
            results.push(data as TestResult);
          }
        } catch (error: any) {
          if (error.name === 'AbortError') {
            stoppedManually = true;
            break;
          }
          console.error(`Error in test case ${i + 1}, iteration ${t + 1}:`, error);
          results.push({ error: error.message });
        }
        
        const currentProgress = Math.round(((t + 1) / times) * 100);
        setTestCases(prev => prev.map(c => c.id === testCase.id ? { ...c, progress: currentProgress, results } : c));
      }

      if (stoppedManually) break;

      setTestCases(prev => prev.map(c => c.id === testCase.id ? { ...c, status: 'completed', progress: 100 } : c));
    }

    setIsExecuting(false);
    setIsStopping(false);
    abortControllerRef.current = null;

    if (!stoppedManually) {
      setStep(3);
      toast.success('批量测试已完成');
    } else {
      setTestCases(prev => prev.map(c => c.status === 'running' ? { ...c, status: 'idle' } : c));
      toast.info('任务已终止');
    }
  };

  // Step 3: Export to Excel
  const exportToExcel = () => {
    const includeTrace = testCases.some(c => c.results.some(r => (r.trace?.length ?? 0) > 0));

    // Discover all dynamic column keys up front so header ordering is stable
    // regardless of whether the first row happens to have them populated.
    const inputKeys = new Set<string>();
    const outputKeys = new Set<string>();
    let sawScalarOutput = false;
    let sawError = false;
    for (const c of testCases) {
      Object.keys(c.inputs).forEach(k => inputKeys.add(k));
      for (const r of c.results) {
        if (r.error) sawError = true;
        if (!r.error) {
          const out = r.data?.outputs ?? r.answer;
          if (out && typeof out === 'object') {
            Object.keys(out).forEach(k => outputKeys.add(k));
          } else if (out !== undefined) {
            sawScalarOutput = true;
          }
        }
      }
    }

    const dataForExport = testCases.flatMap((testCase, index) => {
      return testCase.results.map((result, rIndex) => {
        const row: Record<string, any> = {
          'Test Case': index + 1,
          'Iteration': rIndex + 1,
        };

        if (difyConfig.appMode === 'chat') {
          row['User Query'] = testCase.query || '';
        }

        for (const k of inputKeys) {
          row[k] = testCase.inputs[k] ?? '';
        }

        if (includeTrace) {
          row['Steps'] = result.trace?.length ?? '';
          row['Total Tokens'] = result.totalTokens ?? '';
          row['Elapsed (s)'] = result.elapsedTime != null
            ? Number(result.elapsedTime.toFixed(2))
            : '';
        }

        if (result.error) {
          row['Error'] = result.error;
          for (const k of outputKeys) row[`Output_${k}`] = '';
          if (sawScalarOutput) row['Output'] = '';
          row['Task ID'] = result.task_id || result.id || '';
        } else {
          const outputs = result.data?.outputs ?? result.answer;
          if (outputs && typeof outputs === 'object') {
            for (const k of outputKeys) {
              const v = (outputs as any)[k];
              row[`Output_${k}`] = v === undefined
                ? ''
                : typeof v === 'object' ? JSON.stringify(v) : v;
            }
            if (sawScalarOutput) row['Output'] = '';
          } else {
            for (const k of outputKeys) row[`Output_${k}`] = '';
            if (sawScalarOutput) row['Output'] = outputs ?? '';
          }
          if (sawError) row['Error'] = '';
          row['Task ID'] = result.task_id || result.id || '';
        }

        if (includeTrace) {
          row['Trace'] = result.trace?.length
            ? formatTraceForCell(result.trace, {
                steps: result.trace.length,
                tokens: result.totalTokens,
                elapsed: result.elapsedTime,
                status: result.finalStatus
              })
            : '';
        }

        return row;
      });
    });

    // Explicit header locks column order even if earliest rows have empty traces.
    const header: string[] = [
      'Test Case',
      'Iteration',
      ...(difyConfig.appMode === 'chat' ? ['User Query'] : []),
      ...inputKeys,
      ...(includeTrace ? ['Steps', 'Total Tokens', 'Elapsed (s)'] : []),
      ...Array.from(outputKeys).map(k => `Output_${k}`),
      ...(sawScalarOutput ? ['Output'] : []),
      ...(sawError ? ['Error'] : []),
      'Task ID',
      ...(includeTrace ? ['Trace'] : [])
    ];

    const worksheet = XLSX.utils.json_to_sheet(dataForExport, { header });

    // Widen a few important columns so the sheet is immediately readable.
    const colWidths: { wch: number }[] = header.map(h => {
      if (h === 'Trace') return { wch: 80 };
      if (h.startsWith('Output') || h === 'User Query') return { wch: 40 };
      if (h === 'Error') return { wch: 40 };
      if (h === 'Task ID') return { wch: 36 };
      return { wch: 14 };
    });
    worksheet['!cols'] = colWidths;

    // Turn on wrap-text for the Trace column so the multi-line content renders.
    // NOTE: the default `xlsx` build doesn't persist cell styles on write, so
    // this line is a no-op there; users can toggle wrap once in Excel. If
    // `xlsx-js-style` is swapped in, the style survives the write.
    const traceColIdx = header.indexOf('Trace');
    if (traceColIdx >= 0 && worksheet['!ref']) {
      const range = XLSX.utils.decode_range(worksheet['!ref']);
      for (let R = range.s.r + 1; R <= range.e.r; R++) {
        const ref = XLSX.utils.encode_cell({ r: R, c: traceColIdx });
        const cell = (worksheet as any)[ref];
        if (cell) {
          cell.s = {
            ...(cell.s || {}),
            alignment: { wrapText: true, vertical: 'top' }
          };
        }
      }
    }

    const workbook = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(workbook, worksheet, 'Results');
    XLSX.writeFile(workbook, `dify_batch_test_${new Date().toISOString().split('T')[0]}.xlsx`);
  };

  const getFlatResults = () => {
    return testCases.flatMap((testCase, index) => {
      return testCase.results.map((result, rIndex) => {
        const outputs = result.error 
          ? result.error 
          : (result.data?.outputs || result.answer || JSON.stringify(result));
          
        const outputString = typeof outputs === 'object' 
          ? JSON.stringify(outputs, null, 2) 
          : String(outputs);

        return {
          caseIndex: index + 1,
          iteration: rIndex + 1,
          inputs: testCase.inputs,
          query: testCase.query,
          isError: !!result.error,
          output: outputString,
          taskId: result.task_id || result.id || 'N/A',
          result
        };
      });
    });
  };

  const anyTrace = testCases.some(c => c.results.some(r => (r.trace?.length ?? 0) > 0));

  const ResultsTable = ({ flatResults, inputs, appMode, showTrace }: { flatResults: any[], inputs: DifyInput[], appMode: AppMode, showTrace: boolean }) => (
    <Table>
      <TableHeader className="bg-[#f8fafc] sticky top-0 z-20">
        <TableRow className="hover:bg-transparent border-b border-[#e2e8f0]">
          <TableHead className="w-14 h-12 px-3 font-bold text-[10px] text-[#94a3b8] uppercase tracking-wider">序号</TableHead>
          <TableHead className="w-14 h-12 px-3 font-bold text-[10px] text-[#94a3b8] uppercase tracking-wider">迭代</TableHead>
          {appMode === 'chat' && (
            <TableHead className="min-w-[140px] max-w-[220px] h-12 px-4 font-bold text-[10px] text-[#0f172a] uppercase tracking-wider">用户提问</TableHead>
          )}
          {inputs.map(input => (
            <TableHead key={input.name} className="min-w-[90px] max-w-[160px] h-12 px-4 font-bold text-[10px] text-[#94a3b8] uppercase tracking-wider truncate">{input.label}</TableHead>
          ))}
          <TableHead className="w-24 h-12 px-4 font-bold text-[10px] text-[#94a3b8] uppercase tracking-wider">状态</TableHead>
          {showTrace && (
            <TableHead className="w-[96px] h-12 px-4 font-bold text-[10px] text-[#94a3b8] uppercase tracking-wider">追踪</TableHead>
          )}
          <TableHead className="w-[320px] h-12 px-4 font-bold text-[10px] text-[#94a3b8] uppercase tracking-wider">响应内容</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {flatResults.map((row, idx) => {
          const trace: NodeTraceStep[] | undefined = row.result?.trace;
          const hasTrace = (trace?.length ?? 0) > 0;
          const traceTooltipParts: string[] = [];
          if (row.result?.elapsedTime != null) traceTooltipParts.push(`耗时 ${row.result.elapsedTime.toFixed(2)}s`);
          if (row.result?.totalTokens != null) traceTooltipParts.push(`Tokens ${row.result.totalTokens}`);
          return (
          <TableRow key={idx} className="hover:bg-slate-50/50 border-b border-[#f1f5f9] transition-colors">
            <TableCell className="px-3 py-4 font-mono text-[10px] font-bold text-[#94a3b8]">{String(row.caseIndex).padStart(2, '0')}</TableCell>
            <TableCell className="px-3 py-4 font-mono text-[10px] font-bold text-[#94a3b8]">{String(row.iteration).padStart(2, '0')}</TableCell>
            {appMode === 'chat' && (
              <TableCell className="px-4 py-4 text-xs font-medium text-[#0f172a] max-w-[220px] truncate" title={row.query || ''}>
                {row.query || '-'}
              </TableCell>
            )}
            {inputs.map(input => (
              <TableCell key={input.name} className="px-4 py-4 text-xs text-[#64748b] max-w-[160px] truncate" title={String(row.inputs[input.name] ?? '')}>
                {String(row.inputs[input.name] || '-')}
              </TableCell>
            ))}
            <TableCell className="px-4 py-4">
              {row.isError ? (
                <div className="flex items-center gap-1.5 text-red-500 font-bold text-[9px] uppercase tracking-tight">
                   <div className="h-1 w-1 rounded-full bg-red-500" />
                   失败
                </div>
              ) : (
                <div className="flex items-center gap-1.5 text-[#10b981] font-bold text-[9px] uppercase tracking-tight">
                   <div className="h-1 w-1 rounded-full bg-[#10b981]" />
                   成功
                </div>
              )}
            </TableCell>
            {showTrace && (
              <TableCell className="px-4 py-4 align-middle">
                {hasTrace ? (
                  <button
                    type="button"
                    onClick={() => setTraceDialogResult(row.result)}
                    title={traceTooltipParts.length ? `${trace!.length} 步 · ${traceTooltipParts.join(' · ')}` : `${trace!.length} 步`}
                    className="group inline-flex items-center gap-1 rounded-md border border-slate-200 bg-white px-2 py-1 text-left transition-all hover:border-slate-300 hover:shadow-sm active:scale-[0.98]"
                  >
                    <Activity className="h-3 w-3 text-[#0f172a]" />
                    <span className="text-[10px] font-bold text-[#0f172a]">{trace!.length} 步</span>
                    <ChevronRight className="h-3 w-3 text-slate-300 group-hover:translate-x-0.5 transition-transform" />
                  </button>
                ) : (
                  <span className="text-[10px] text-slate-300">—</span>
                )}
              </TableCell>
            )}
            <TableCell className="px-4 py-4 align-top">
              <div className="w-[300px] max-w-[300px] max-h-40 overflow-auto whitespace-pre-wrap break-all font-mono text-[10px] leading-relaxed bg-slate-50 p-3 rounded-xl border border-slate-100 text-[#334155] shadow-[inset_0_1px_2px_rgba(0,0,0,0.02)]">
                {row.output}
              </div>
            </TableCell>
          </TableRow>
          );
        })}
      </TableBody>
    </Table>
  );

  return (
    <div className="min-h-screen bg-[#f8fafc] font-sans text-[#020817] antialiased">
      <Toaster position="top-center" />
      
      {/* Header / Stepper */}
      <header className="sticky top-0 z-50 border-b border-[#e2e8f0] bg-white/80 backdrop-blur-md py-3.5">
        <div className="relative mx-auto flex w-full max-w-7xl items-center justify-between px-6 sm:px-8 lg:px-10">
          <div className="flex items-center gap-2.5">
            <div className="relative flex h-7 w-7 items-center justify-center rounded-lg bg-[#0f172a] shadow-lg shadow-slate-200">
              <span className="text-[10px] font-bold text-white tracking-tighter">BT</span>
            </div>
            <div className="flex flex-col">
              <h1 className="text-sm font-bold tracking-tight text-[#020817] leading-none">Dify Batch Tester</h1>
            </div>
          </div>
          
          {/* Central Progress Stepper */}
          <div className="absolute left-1/2 hidden -translate-x-1/2 items-center justify-center gap-1.5 md:flex">
            {[
              { id: 1, label: 'API 连接' },
              { id: 2, label: '配置与执行' },
              { id: 3, label: '下载结果' }
            ].map((s, idx) => {
              const isPast = step > s.id;
              const isCurrent = step === s.id;
              const canClick = !isExecuting && !isStopping && (isPast || isCurrent);

              return (
                <div key={s.id} className="flex items-center">
                  <button 
                    onClick={() => canClick && setStep(s.id)}
                    disabled={!canClick}
                    className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition-all duration-200 ${
                      canClick ? 'hover:bg-slate-50 cursor-pointer active:scale-95' : 'cursor-not-allowed'
                    }`}
                  >
                    <div className={`flex h-5 w-5 items-center justify-center rounded-full border text-[9px] font-bold transition-all duration-500 ${
                      isCurrent ? 'border-[#0f172a] bg-[#0f172a] text-white ring-4 ring-slate-100 shadow-sm' : 
                      isPast ? 'border-[#10b981] bg-[#10b981] text-white' : 'border-[#e2e8f0] bg-white text-[#94a3b8]'
                    }`}>
                      {isPast ? '✓' : s.id}
                    </div>
                    <span className={`hidden text-[11px] font-semibold whitespace-nowrap tracking-tight transition-colors duration-300 lg:inline ${
                      isCurrent ? 'text-[#0f172a]' : 'text-[#64748b]'
                    }`}>
                      {s.label}
                    </span>
                  </button>
                  {idx < 2 && (
                    <div className="flex items-center px-2">
                       <div className={`h-[1px] w-4 transition-colors duration-500 ${step > s.id ? 'bg-[#10b981]' : 'bg-[#e2e8f0]'}`} />
                    </div>
                  )}
                </div>
              );
            })}
          </div>

          <div className="flex justify-end">
            <div className="flex items-center gap-2 bg-[#f8fafc] border border-[#e2e8f0] px-2.5 py-1 rounded-md shadow-[inset_0_1px_1px_rgba(0,0,0,0.01)]">
              <div className={`h-1.5 w-1.5 rounded-full animate-pulse ${inputs.length > 0 ? 'bg-[#10b981]' : 'bg-amber-400'}`} />
              <div className="font-mono text-[9px] font-bold tracking-tight text-[#475569] uppercase">
                {inputs.length > 0 ? (
                  <span className="flex items-center gap-1.5">
                    <span className="text-[#94a3b8]">{difyConfig.appMode.toUpperCase()}</span>
                    <span className="h-2.5 w-[1px] bg-[#e2e8f0]" />
                    <span>{inputs.length} 个字段</span>
                  </span>
                ) : '等待连接'}
              </div>
            </div>
          </div>
        </div>
      </header>

      <main
        className={`mx-auto flex w-full max-w-7xl flex-col px-6 py-10 sm:px-8 lg:px-10 ${
          step === 1 ? 'min-h-[calc(100vh-65px)] justify-center' : 'gap-8'
        }`}
      >

        <AnimatePresence mode="wait">
          {step === 1 && (
            <motion.div
              key="step1"
              initial={{ opacity: 0, y: 10, scale: 0.98 }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{ opacity: 0, y: -10, scale: 0.98 }}
              className="flex w-full flex-col items-center justify-center gap-12 lg:flex-row lg:gap-20"
            >
              <div className="text-left max-w-[440px] lg:max-w-[400px]">
                <div className="mb-6 inline-flex h-10 w-10 items-center justify-center rounded-xl bg-[#0f172a] text-white shadow-lg shadow-slate-200">
                  <Play className="h-5 w-5 fill-current" />
                </div>
                <h2 className="text-4xl font-bold tracking-tight text-[#0f172a] mb-6 leading-[1.2]">
                  Dify 批量自动化<br />测试终端
                </h2>
                <div className="space-y-4">
                  <p className="text-base text-slate-500 leading-relaxed font-medium">
                    同步您的 Dify 应用配置，通过自动化批量运行实时评估提示词（Prompt）表现。支持大规模数据批处理与结果导出。
                  </p>
                  <p className="text-sm text-slate-400 leading-relaxed">
                    助力开发者精细化调优 LLM 任务，确保应用在海量数据输入下的稳定性与可靠性。
                  </p>
                </div>
              </div>

              <Card className="w-full max-w-[440px] border-[#e2e8f0] bg-white shadow-2xl shadow-slate-200/60 rounded-[40px] overflow-hidden border-t border-l border-white/50">
                <div className="p-10 space-y-10">
                  {/* Mode Selection Toggle */}
                  <div className="flex flex-col items-center gap-5">
                    <div className="bg-[#f1f5f9] p-1 rounded-full flex gap-1 border border-[#e2e8f0]">
                      {(['chat', 'workflow'] as AppMode[]).map((mode) => (
                        <button
                          key={mode}
                          type="button"
                          onClick={() => setDifyConfig({ ...difyConfig, appMode: mode })}
                          className={`px-6 py-2 rounded-full text-[10px] font-bold uppercase tracking-wider transition-all duration-300 ${
                            difyConfig.appMode === mode 
                              ? 'bg-white text-[#0f172a] shadow-sm ring-1 ring-slate-200/50' 
                              : 'text-[#64748b] hover:text-[#0f172a]'
                          }`}
                        >
                          {mode === 'chat' ? '高级对话 (Chat)' : '工作流 (Workflow)'}
                        </button>
                      ))}
                    </div>
                  </div>

                  {/* Input Rows */}
                  <div className="space-y-6">
                    <div className="space-y-2">
                       <Label htmlFor="step1-apiUrl" className="text-[10px] pl-1 font-bold text-[#94a3b8] uppercase tracking-[0.1em]">API 端点 URL</Label>
                       <div className="relative group">
                         <div className="absolute left-3 top-1/2 -translate-y-1/2 text-[#cbd5e1] group-focus-within:text-[#64748b] transition-colors">
                            <Globe className="h-4 w-4" />
                         </div>
                         <Input 
                           id="step1-apiUrl" 
                           value={difyConfig.apiUrl} 
                           onChange={(e) => setDifyConfig({ ...difyConfig, apiUrl: e.target.value })}
                           placeholder="https://api.dify.ai/v1"
                           className="h-12 pl-10 bg-slate-50 border-none rounded-xl focus:ring-2 focus:ring-slate-200 transition-all font-medium placeholder:text-slate-300"
                         />
                       </div>
                    </div>

                    <div className="space-y-2">
                       <Label htmlFor="step1-apiKey" className="text-[10px] pl-1 font-bold text-[#94a3b8] uppercase tracking-[0.1em]">应用密钥 (API Key)</Label>
                       <div className="relative group">
                         <div className="absolute left-3 top-1/2 -translate-y-1/2 text-[#cbd5e1] group-focus-within:text-[#64748b] transition-colors">
                            <Key className="h-4 w-4" />
                         </div>
                         <Input 
                           id="step1-apiKey" 
                           type="password"
                           value={difyConfig.apiKey} 
                           onChange={(e) => setDifyConfig({ ...difyConfig, apiKey: e.target.value })}
                           placeholder="app-xxxxxxxxxxxxxxxxxxxxxxxx"
                           className="h-12 pl-10 bg-slate-50 border-none rounded-xl focus:ring-2 focus:ring-slate-200 transition-all font-medium placeholder:text-slate-300"
                         />
                       </div>
                    </div>
                  </div>

                  <div className="space-y-4">
                    <Button 
                      onClick={fetchParameters} 
                      disabled={isExecuting}
                      className="w-full h-14 bg-[#0f172a] hover:bg-[#1e293b] text-white font-bold rounded-2xl gap-3 shadow-xl shadow-slate-200 transition-all active:scale-[0.98]"
                    >
                       {isExecuting ? <Loader2 className="h-5 w-5 animate-spin" /> : <ChevronRight className="h-5 w-5" />}
                      {isExecuting ? '正在连接...' : '连接到 Dify 应用'}
                    </Button>
                    
                    <div className="flex items-center justify-center gap-2 text-[10px] font-medium text-[#94a3b8]">
                       <ShieldCheck className="h-3 w-3" />
                       <span>模式将通过 API 响应自动验证</span>
                    </div>
                  </div>
                </div>
              </Card>
            </motion.div>
          )}

          {step === 2 && (
            <motion.div
              key="step2"
              initial={{ opacity: 0, x: 20 }}
              animate={{ opacity: 1, x: 0 }}
              exit={{ opacity: 0, x: -20 }}
              className="space-y-8"
            >
              <div className="flex flex-wrap items-end justify-between gap-4">
                <div>
                  <h2 className="text-3xl font-bold tracking-tight text-[#0f172a]">配置测试用例</h2>
                  <p className="text-slate-500 mt-2 text-sm font-medium">为每个测试用例填写输入字段并指定重复次数。</p>
                </div>
                <div className="flex flex-wrap items-center gap-3">
                  <Button 
                    onClick={() => {
                      setTestCases([{
                        id: crypto.randomUUID(),
                        inputs: Object.fromEntries(inputs.map(i => [i.name, ''])),
                        times: 1,
                        status: 'idle',
                        progress: 0,
                        results: []
                      }]);
                      toast.success('测试用例已清空');
                    }} 
                    variant="ghost" 
                    disabled={isExecuting}
                    className="text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-full h-10 px-4 transition-all"
                  >
                    全部清除
                  </Button>
                  <div className="flex bg-white rounded-full border border-slate-200 p-1 shadow-sm h-10">
                    <Button 
                      onClick={addTestCase} 
                      variant="ghost" 
                      disabled={isExecuting}
                      className="gap-2 rounded-full h-full px-4 text-xs font-bold text-[#0f172a]"
                    >
                      <Plus className="h-4 w-4" />
                      添加
                    </Button>
                    <Button 
                      onClick={() => excelInputRef.current?.click()} 
                      variant="ghost" 
                      disabled={isExecuting}
                      className="gap-2 rounded-full h-full px-4 text-xs font-bold text-[#64748b] border-l border-slate-100"
                    >
                      <Download className="h-4 w-4 rotate-180" />
                      导入
                    </Button>
                    <Button 
                      onClick={downloadExcelTemplate} 
                      variant="ghost" 
                      disabled={isExecuting}
                      className="gap-2 rounded-full h-full px-4 text-xs font-bold text-[#64748b] border-l border-slate-100"
                    >
                      <Download className="h-4 w-4" />
                      模板
                    </Button>
                  </div>
                  <input 
                    type="file" 
                    ref={excelInputRef} 
                    className="hidden" 
                    accept=".xlsx,.xls,.csv"
                    onChange={handleExcelImport}
                  />
                  {canCaptureTrace && (
                    <button
                      type="button"
                      role="switch"
                      aria-checked={captureTrace}
                      disabled={isExecuting}
                      onClick={() => setCaptureTrace(v => !v)}
                      title="开启后将使用流式响应捕获每个节点的执行详情（速度略慢）"
                      className={`flex items-center gap-2 h-10 rounded-full border px-4 text-xs font-bold transition-all ${
                        captureTrace
                          ? 'bg-[#0f172a] text-white border-[#0f172a] shadow-lg shadow-slate-200'
                          : 'bg-white text-[#64748b] border-slate-200 hover:text-[#0f172a] hover:border-slate-300'
                      } ${isExecuting ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer active:scale-[0.98]'}`}
                    >
                      <Activity className="h-4 w-4" />
                      <span>节点追踪</span>
                      <span
                        className={`ml-1 flex h-4 w-7 items-center rounded-full p-0.5 transition-colors ${
                          captureTrace ? 'bg-white/30' : 'bg-slate-200'
                        }`}
                      >
                        <span
                          className={`h-3 w-3 rounded-full bg-white shadow transition-transform ${
                            captureTrace ? 'translate-x-3' : 'translate-x-0'
                          }`}
                        />
                      </span>
                    </button>
                  )}
                  {isExecuting ? (
                    <Button 
                      onClick={stopTests} 
                      disabled={isStopping}
                      variant="destructive"
                      className="gap-2 rounded-full h-10 px-6 shadow-lg shadow-red-100 transition-all font-bold"
                    >
                      {isStopping ? <Loader2 className="h-4 w-4 animate-spin" /> : <Slash className="h-4 w-4" />}
                      {isStopping ? '停止中' : '终止任务'}
                    </Button>
                  ) : (
                    <Button 
                      onClick={runTests} 
                      disabled={isExecuting || testCases.length === 0}
                      className="gap-2 bg-[#0f172a] hover:bg-[#1e293b] rounded-full h-10 px-8 shadow-lg shadow-slate-200 transition-all font-bold"
                    >
                      <Play className="h-4 w-4" />
                      开始运行
                    </Button>
                  )}
                </div>
              </div>

              <Card className="border-[#e2e8f0] shadow-xl shadow-slate-200/40 overflow-hidden rounded-[24px] bg-white">
                <div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e2e8f0] px-8 py-5 bg-white">
                  <h3 className="text-base font-bold text-[#0f172a]">用例矩阵</h3>
                  <div className="flex items-center gap-2 bg-[#f8fafc] border border-[#e2e8f0] px-3 py-1 rounded-full">
                    <div className="h-1.5 w-1.5 rounded-full bg-[#10b981]" />
                    <div className="font-mono text-[10px] font-bold tracking-tight text-[#475569] uppercase flex items-center gap-2">
                       <span>{difyConfig.appMode}</span>
                       <span className="h-2.5 w-[1px] bg-[#e2e8f0]" />
                       <span>{testCases.length} CASE(S)</span>
                    </div>
                  </div>
                </div>
                <div className="overflow-x-auto">
                <Table>
                  {testCases.length > 0 && (
                    <TableHeader className="bg-[#f1f5f9]">
                      <TableRow className="hover:bg-transparent border-b border-[#e2e8f0]">
                        <TableHead className="w-16 h-10 px-6 font-medium text-[#64748b]">序号</TableHead>
                        {difyConfig.appMode === 'chat' && (
                          <TableHead className="min-w-[200px] h-10 px-6 font-semibold text-[#0f172a]">User Query</TableHead>
                        )}
                        {inputs.map(input => (
                          <TableHead key={input.name} className="h-10 px-6 font-medium text-[#64748b]">
                            <div className="flex items-center justify-between group">
                              <span>{input.label}</span>
                              <button 
                                onClick={() => removeVariable(input.name)}
                                className="ml-2 opacity-0 group-hover:opacity-100 transition-opacity hover:text-red-500"
                              >
                                <Trash2 className="h-3 w-3" />
                              </button>
                            </div>
                          </TableHead>
                        ))}
                        <TableHead className="w-24 h-10 px-6 text-center font-medium text-[#64748b]">次数</TableHead>
                        <TableHead className="w-48 h-10 px-6 font-medium text-[#64748b]">执行进度</TableHead>
                        <TableHead className="w-16 h-10 px-6 font-medium text-[#64748b]"></TableHead>
                      </TableRow>
                    </TableHeader>
                  )}
                  <TableBody>
                    {testCases.length > 0 ? (
                      testCases.map((testCase, index) => (
                        <TableRow key={testCase.id} className="hover:bg-[#f8fafc]/50 border-b border-[#e2e8f0]">
                          <TableCell className="px-6 font-mono text-sm text-[#64748b]">{String(index + 1).padStart(2, '0')}</TableCell>
                          {difyConfig.appMode === 'chat' && (
                            <TableCell className="px-6 py-4">
                              <Input 
                                placeholder="输入用户消息..."
                                disabled={isExecuting}
                                value={testCase.query || ''}
                                onChange={(e) => updateTestCase(testCase.id, 'query', e.target.value)}
                                className="border-none bg-slate-50 focus:ring-2 focus:ring-slate-100 transition-all h-9 rounded-lg font-medium placeholder:text-slate-300 text-sm"
                              />
                            </TableCell>
                          )}
                          {inputs.map(input => (
                            <TableCell key={input.name} className="px-6 py-4">
                              <Input 
                                placeholder={`输入 ${input.label}`}
                                disabled={isExecuting}
                                value={testCase.inputs[input.name] || ''}
                                onChange={(e) => updateTestCase(testCase.id, input.name, e.target.value)}
                                className="border-none bg-slate-50 focus:ring-2 focus:ring-slate-100 transition-all h-9 rounded-lg font-medium placeholder:text-slate-300 text-sm"
                              />
                            </TableCell>
                          ))}
                          <TableCell className="px-6 py-4">
                            <Input 
                              type="number" 
                              min="1"
                              disabled={isExecuting}
                              value={testCase.times}
                              onChange={(e) => updateTestCase(testCase.id, 'times', parseInt(e.target.value) || 1, false)}
                              className="w-20 text-center border-none bg-slate-50 focus:ring-2 focus:ring-slate-100 h-9 rounded-lg font-mono font-bold"
                            />
                          </TableCell>
                          <TableCell className="px-6 py-4">
                            {testCase.status === 'idle' ? (
                              <Badge variant="secondary" className="bg-[#f8fafc] text-[#94a3b8] border border-[#e2e8f0] font-bold text-[9px] uppercase tracking-wider px-2 py-0.5 rounded-md">Pending</Badge>
                            ) : (() => {
                              const lastResult = testCase.results[testCase.results.length - 1];
                              // Only surface the node label while a step is actively running,
                              // so it clears between iterations instead of showing a stale one.
                              const runningStep =
                                testCase.status === 'running'
                                  ? lastResult?.trace?.find(s => s.status === 'running')
                                  : undefined;
                              const done = testCase.results.length;
                              const total = testCase.times;
                              return (
                                <div className="flex flex-col gap-1.5 min-w-24">
                                  <Progress value={testCase.progress} className="h-1.5 bg-[#f1f5f9] [&>div]:bg-[#0f172a] rounded-full" />
                                  <span className="text-[9px] font-bold text-[#64748b] uppercase tracking-tighter">
                                    {testCase.status === 'running'
                                      ? `In Progress (${done}/${total})`
                                      : `Complete (${total}/${total})`}
                                  </span>
                                  {runningStep?.title && (
                                    <span className="flex items-center gap-1 text-[9px] text-slate-400 max-w-[200px]">
                                      <Activity className="h-2.5 w-2.5 shrink-0" />
                                      <span className="truncate">{runningStep.title}</span>
                                    </span>
                                  )}
                                </div>
                              );
                            })()}
                          </TableCell>
                          <TableCell className="px-6 py-4">
                            <Button 
                              variant="ghost" 
                              size="icon" 
                              disabled={isExecuting}
                              onClick={() => removeTestCase(testCase.id)}
                              className="text-[#64748b] hover:text-red-500 hover:bg-red-50"
                            >
                              <Trash2 className="h-4 w-4" />
                            </Button>
                          </TableCell>
                        </TableRow>
                      ))
                    ) : (
                      <TableRow>
                        <TableCell colSpan={10} className="h-64 text-center">
                          <div className="flex flex-col items-center justify-center gap-4">
                            <div className="h-12 w-12 rounded-full bg-slate-50 flex items-center justify-center text-slate-300">
                              <Plus className="h-6 w-6" />
                            </div>
                            <div className="space-y-1">
                              <p className="text-sm font-medium text-slate-900">暂无测试用例</p>
                              <p className="text-xs text-slate-500">点击下方按钮手动添加或从 Excel 导入</p>
                            </div>
                            <Button 
                              onClick={addTestCase} 
                              size="sm"
                              className="mt-2 bg-[#0f172a] hover:bg-[#1e293b] gap-2 rounded-full h-10 px-6 font-bold shadow-lg shadow-slate-100 transition-all"
                            >
                              <Plus className="h-4 w-4" />
                              添加首个用例
                            </Button>
                          </div>
                        </TableCell>
                      </TableRow>
                    )}
                  </TableBody>
                </Table>
                </div>
              </Card>

              {testCases.length > 5 && (
                <div className="flex justify-center">
                  <Button 
                    onClick={addTestCase} 
                    variant="ghost" 
                    className="gap-2 text-slate-400 hover:text-[#0f172a] hover:bg-slate-50 rounded-full px-6 transition-all font-bold text-xs"
                  >
                    <Plus className="h-4 w-4" />
                    继续添加用例
                  </Button>
                </div>
              )}
            </motion.div>
          )}

          {step === 3 && (
            <motion.div
              key="step3"
              initial={{ opacity: 0, y: 20 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -20 }}
              className="space-y-10"
            >
              <div className="text-center">
                <div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-green-50 text-[#10b981]">
                  <CheckCircle2 className="h-10 w-10" />
                </div>
                <h2 className="text-4xl font-bold tracking-tight text-[#020817]">执行完成</h2>
                <p className="text-[#64748b] text-lg mt-4 max-w-2xl mx-auto leading-relaxed">
                  所有 {testCases.length} 个测试用例已成功执行。结果已在下方展示，您可以进行预览或直接下载导出。
                </p>
              </div>

              <div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full">
                <Card className="bg-white border-[#e2e8f0] rounded-[24px] shadow-xl shadow-slate-200/40 overflow-hidden">
                  <CardHeader className="pb-3 px-8 pt-8">
                    <CardDescription className="uppercase text-[10px] font-bold tracking-[0.2em] text-[#94a3b8]">总测试用例数</CardDescription>
                    <CardTitle className="text-4xl font-bold text-[#020817] font-mono">{testCases.length}</CardTitle>
                  </CardHeader>
                </Card>
                <Card className="bg-white border-[#e2e8f0] rounded-[24px] shadow-xl shadow-slate-200/40 overflow-hidden">
                  <CardHeader className="pb-3 px-8 pt-8">
                    <CardDescription className="uppercase text-[10px] font-bold tracking-[0.2em] text-[#94a3b8]">成功执行次数</CardDescription>
                    <CardTitle className="text-4xl font-bold text-[#10b981] font-mono">
                      {testCases.reduce((acc, c) => acc + c.results.filter(r => !r.error).length, 0)}
                    </CardTitle>
                  </CardHeader>
                </Card>
                <Card className="bg-white border-[#e2e8f0] rounded-[24px] shadow-xl shadow-slate-200/40 overflow-hidden">
                  <CardHeader className="pb-3 px-8 pt-8">
                    <CardDescription className="uppercase text-[10px] font-bold tracking-[0.2em] text-[#94a3b8]">失败次数</CardDescription>
                    <CardTitle className="text-4xl font-bold text-red-500 font-mono">
                      {testCases.reduce((acc, c) => acc + c.results.filter(r => r.error).length, 0)}
                    </CardTitle>
                  </CardHeader>
                </Card>
              </div>

              <Card className="border-[#e2e8f0] shadow-xl shadow-slate-200/40 overflow-hidden rounded-[24px] bg-white">
                <div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e2e8f0] px-8 py-6 bg-white">
                  <h3 className="text-base font-bold text-[#0f172a]">结果透视</h3>
                  <div className="flex items-center gap-3">
                    <Button 
                      onClick={exportToExcel}
                      size="sm"
                      className="h-10 px-6 gap-2 bg-[#0f172a] hover:bg-[#1e293b] rounded-full shadow-lg shadow-slate-200 text-xs font-bold transition-all active:scale-[0.98]"
                    >
                      <Download className="h-4 w-4" />
                      导出 EXCEL 报告
                    </Button>
                  </div>
                </div>
                <div className="max-h-[600px] overflow-auto">
                  <ResultsTable flatResults={getFlatResults()} inputs={inputs} appMode={difyConfig.appMode} showTrace={anyTrace} />
                </div>
              </Card>

              <div className="flex flex-wrap justify-center gap-4 pt-4 pb-12">
                <Button 
                  variant="outline" 
                   className="h-12 px-10 rounded-full border-slate-200 text-[#0f172a] font-bold hover:bg-slate-50 transition-all active:scale-95"
                  onClick={() => setStep(2)}
                >
                  返回编辑用例
                </Button>
                <Button 
                  variant="outline" 
                  className="h-12 px-10 rounded-full border-slate-200 text-[#0f172a] font-bold hover:bg-slate-50 transition-all active:scale-95"
                  onClick={() => {
                    setStep(1);
                    setInputs([]);
                    setTestCases([]);
                  }}
                >
                  清空并重新开始
                </Button>
              </div>
            </motion.div>
          )}
        </AnimatePresence>
        {/* Removed Footer Decoration for Sleek Theme */}
      </main>

      {/* Footer Decoration REMOVED to match Sleek Interface minimal look */}

      <Dialog open={!!traceDialogResult} onOpenChange={(open) => !open && setTraceDialogResult(null)}>
        <DialogContent className="sm:max-w-4xl max-w-[calc(100%-2rem)] w-full max-h-[85vh] overflow-hidden flex flex-col gap-0 p-0 rounded-[24px] border-[#e2e8f0]">
          <DialogHeader className="flex flex-col gap-2 px-8 py-5 border-b border-[#e2e8f0]">
            <DialogTitle className="flex items-center gap-2 text-lg font-bold text-[#0f172a]">
              <Activity className="h-5 w-5" />
              节点执行追踪
            </DialogTitle>
            {traceDialogResult?.workflowRunId && (
              <DialogDescription className="font-mono text-xs text-slate-500">
                Run ID: {traceDialogResult.workflowRunId}
              </DialogDescription>
            )}
            <div className="mt-1 flex flex-wrap gap-2">
              <Badge variant="secondary" className="gap-1.5 bg-slate-100 text-[#0f172a] font-mono text-[10px]">
                <Hash className="h-3 w-3" />
                {traceDialogResult?.trace?.length ?? 0} STEPS
              </Badge>
              {traceDialogResult?.totalTokens != null && (
                <Badge variant="secondary" className="gap-1.5 bg-slate-100 text-[#0f172a] font-mono text-[10px]">
                  <Coins className="h-3 w-3" />
                  {traceDialogResult.totalTokens} TOKENS
                </Badge>
              )}
              {traceDialogResult?.elapsedTime != null && (
                <Badge variant="secondary" className="gap-1.5 bg-slate-100 text-[#0f172a] font-mono text-[10px]">
                  <Clock className="h-3 w-3" />
                  {traceDialogResult.elapsedTime.toFixed(2)}s
                </Badge>
              )}
              {traceDialogResult?.finalStatus && (
                <Badge
                  variant="secondary"
                  className={`gap-1.5 font-mono text-[10px] ${
                    traceDialogResult.finalStatus === 'succeeded'
                      ? 'bg-emerald-50 text-emerald-700 border border-emerald-100'
                      : 'bg-red-50 text-red-700 border border-red-100'
                  }`}
                >
                  {String(traceDialogResult.finalStatus).toUpperCase()}
                </Badge>
              )}
            </div>
          </DialogHeader>
          <div className="min-h-0 flex-1 overflow-auto px-8 py-5 space-y-3 bg-[#f8fafc]">
            {traceDialogResult?.trace?.length ? (
              traceDialogResult.trace.map((step, i) => (
                <div key={step.execId || i} className="rounded-2xl border border-[#e2e8f0] bg-white p-4 shadow-sm">
                  <div className="flex flex-wrap items-center gap-2">
                    <span className="font-mono text-[11px] font-bold text-[#94a3b8]">#{String(step.index ?? i + 1).padStart(2, '0')}</span>
                    <Badge variant="secondary" className="bg-slate-100 text-[#0f172a] font-mono text-[9px] uppercase tracking-wider">
                      {step.nodeType}
                    </Badge>
                    <span className="text-sm font-semibold text-[#0f172a] truncate max-w-[260px]">{step.title}</span>
                    <span className={`ml-auto flex items-center gap-1 rounded-full px-2 py-0.5 text-[9px] font-bold uppercase tracking-wider ${
                      step.status === 'succeeded' ? 'bg-emerald-50 text-emerald-700'
                        : step.status === 'running' ? 'bg-amber-50 text-amber-700'
                        : step.status === 'failed' || step.status === 'exception' ? 'bg-red-50 text-red-700'
                        : 'bg-slate-100 text-slate-600'
                    }`}>
                      {step.status === 'running' && <Loader2 className="h-3 w-3 animate-spin" />}
                      {step.status}
                    </span>
                  </div>
                  <div className="mt-2 flex flex-wrap gap-3 font-mono text-[10px] text-[#64748b]">
                    {step.elapsedTime != null && (
                      <span className="flex items-center gap-1"><Clock className="h-3 w-3" />{step.elapsedTime.toFixed(2)}s</span>
                    )}
                    {step.tokens != null && (
                      <span className="flex items-center gap-1"><Coins className="h-3 w-3" />{step.tokens} tokens</span>
                    )}
                    {step.nodeId && (
                      <span className="text-slate-400">id: {step.nodeId}</span>
                    )}
                  </div>
                  {step.error && (
                    <div className="mt-3 rounded-lg border border-red-100 bg-red-50 p-3 text-[11px] font-mono text-red-700 whitespace-pre-wrap break-all">
                      <div className="mb-1 flex items-center gap-1 font-bold text-[9px] uppercase tracking-wider"><AlertTriangle className="h-3 w-3" /> Error</div>
                      {step.error}
                    </div>
                  )}
                  {step.inputs !== undefined && (
                    <details className="mt-3 group">
                      <summary className="cursor-pointer text-[10px] font-bold uppercase tracking-wider text-[#94a3b8] hover:text-[#0f172a]">Inputs</summary>
                      <pre className="mt-2 max-h-48 overflow-auto rounded-lg bg-slate-50 border border-slate-100 p-3 font-mono text-[10px] leading-relaxed text-[#334155] whitespace-pre-wrap break-all">{safeStringify(step.inputs)}</pre>
                    </details>
                  )}
                  {step.outputs !== undefined && (
                    <details className="mt-2 group" open={i === (traceDialogResult?.trace?.length ?? 0) - 1}>
                      <summary className="cursor-pointer text-[10px] font-bold uppercase tracking-wider text-[#94a3b8] hover:text-[#0f172a]">Outputs</summary>
                      <pre className="mt-2 max-h-48 overflow-auto rounded-lg bg-slate-50 border border-slate-100 p-3 font-mono text-[10px] leading-relaxed text-[#334155] whitespace-pre-wrap break-all">{safeStringify(step.outputs)}</pre>
                    </details>
                  )}
                </div>
              ))
            ) : (
              <div className="flex h-40 items-center justify-center text-sm text-slate-400">
                无追踪数据
              </div>
            )}
          </div>
          <div className="flex justify-end gap-2 px-8 py-4 border-t border-[#e2e8f0] bg-white">
            <Button
              variant="outline"
              onClick={() => setTraceDialogResult(null)}
              className="rounded-full h-9 px-6 font-bold"
            >
              关闭
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  );
}

function safeStringify(v: unknown): string {
  try {
    return typeof v === 'string' ? v : JSON.stringify(v, null, 2);
  } catch {
    return String(v);
  }
}

// Excel cap is 32,767 chars per cell; leave headroom for safety.
const EXCEL_CELL_LIMIT = 32000;
const TRACE_INLINE_JSON_LIMIT = 300;

function briefInlineJson(v: unknown): string {
  if (v == null) return '';
  let s: string;
  try {
    s = typeof v === 'string' ? v : JSON.stringify(v);
  } catch {
    s = String(v);
  }
  if (s.length > TRACE_INLINE_JSON_LIMIT) {
    return s.slice(0, TRACE_INLINE_JSON_LIMIT) + `…(+${s.length - TRACE_INLINE_JSON_LIMIT} chars)`;
  }
  return s;
}

function formatTraceForCell(
  trace: NodeTraceStep[],
  total: { steps: number; tokens?: number; elapsed?: number; status?: NodeStatus | string }
): string {
  const lines: string[] = [];
  for (const s of trace) {
    const headerParts = [
      `#${s.index ?? '?'} [${s.nodeType}] ${s.title} · ${s.status}`
    ];
    if (s.elapsedTime != null) headerParts.push(`${s.elapsedTime.toFixed(2)}s`);
    if (s.tokens) headerParts.push(`${s.tokens} tok`);
    lines.push(headerParts.join(' · '));

    if (s.status === 'succeeded') {
      if (s.inputs !== undefined) lines.push(`    in : ${briefInlineJson(s.inputs)}`);
      if (s.outputs !== undefined) lines.push(`    out: ${briefInlineJson(s.outputs)}`);
    } else if (s.error) {
      lines.push(`    err: ${briefInlineJson(s.error)}`);
    }
    lines.push('');
  }

  const totalParts: string[] = [`${total.steps} steps`];
  if (total.tokens != null) totalParts.push(`${total.tokens} tok`);
  if (total.elapsed != null) totalParts.push(`${total.elapsed.toFixed(2)}s`);
  if (total.status) totalParts.push(String(total.status));
  lines.push(`TOTAL  ${totalParts.join(' · ')}`);

  let out = lines.join('\n');
  if (out.length > EXCEL_CELL_LIMIT) {
    out = out.slice(0, EXCEL_CELL_LIMIT) + `\n…(truncated, +${out.length - EXCEL_CELL_LIMIT} chars)`;
  }
  return out;
}
