"use client";

/**
 * 测试用例编辑器主体：
 * 对话上下文 → 固定文字脚本轮次 → 整体评估标准。
 */

import {
  closestCenter,
  DndContext,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  type DragEndEvent,
} from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
  ChevronDown,
  ChevronUp,
  GripVertical,
  MessageSquareText,
  MessagesSquare,
  Plus,
  Target,
  Trash2,
  Wrench,
  X,
} from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";

import { SectionCard } from "@/components/editor/section-card";
import { SectionAnchorTabs } from "@/components/editor/section-anchor-tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
  ASSERTION_TYPE_LABEL,
  KEYWORD_MATCH_MODE_LABEL,
  OVERALL_CRITERION_TEXT_MAX,
  TOOL_PARAM_MATCH_MODE_LABEL,
  createEmptyFixedInputTurn,
  createOverallCriterion,
  createReplyBehavior,
  createToolCallBehavior,
  getExpectedBehaviorValidationMessage,
  type AssertionType,
  type ContextRole,
  type ContextTurn,
  type ExpectedBehavior,
  type FixedInputTurn,
  type KeywordMatchMode,
  type OverallCriterion,
  type ReplyExpectedBehavior,
  type TestCaseInputMode,
  type ToolCallExpectedBehavior,
  type ToolParamAssertion,
  type ToolParamMatchMode,
} from "@/data/test-suites";
import { toolsApi, type Tool } from "@/lib/api";
import { cn } from "@/lib/utils";

export type CaseEditorDraft = {
  name: string;
  inputMode: TestCaseInputMode;
  contextTurns: ContextTurn[];
  turns: FixedInputTurn[];
  overallCriteria: OverallCriterion[];
};

const CONTEXT_ROLE_LABEL: Record<ContextRole, string> = {
  agent: "Agent",
  user: "User",
  tool_call: "Tool Call",
  tool_result: "Tool Result",
};

function contextPlaceholder(role: ContextRole): string {
  if (role === "agent") return "Agent 已经说过的话…";
  if (role === "user") return "用户已经说过的话…";
  if (role === "tool_call") return '{\n  "reason": "存在人员受伤"\n}';
  return '{\n  "status": "accepted"\n}';
}

/** 编辑器内使用的工具选项（来自 Agent 工具库） */
export type EditorToolOption = {
  id: string;
  functionName: string;
  label: string;
  parameters: { name: string; description: string }[];
};

type EditorSectionId = "context" | "main" | "overall";

function editorSections(): {
  id: EditorSectionId;
  label: string;
}[] {
  return [
    { id: "context", label: "对话上下文" },
    { id: "main", label: "固定输入" },
    { id: "overall", label: "整体评估" },
  ];
}

/** API 不可用时的兜底工具，保证下拉可选 */
const FALLBACK_TOOLS: EditorToolOption[] = [
  {
    id: "mock_transfer_to_human",
    functionName: "transfer_to_human",
    label: "转人工",
    parameters: [
      { name: "reason", description: "转人工原因" },
      { name: "target", description: "转接目标" },
    ],
  },
  {
    id: "mock_end_conversation",
    functionName: "end_conversation",
    label: "结束会话",
    parameters: [{ name: "reason", description: "结束原因" }],
  },
];

function toolParametersFromDefinition(tool: Tool): EditorToolOption["parameters"] {
  const definition = tool.definition;
  if (definition.type === "http" || definition.type === "client") {
    return definition.config.parameters.map((param) => ({
      name: param.name,
      description: param.description,
    }));
  }
  if (definition.type === "system") {
    return definition.config.captureReason
      ? [{ name: "reason", description: "调用原因" }]
      : [];
  }
  if (definition.type === "mcp") {
    const schema = definition.config.inputSchema;
    const properties =
      schema &&
      typeof schema === "object" &&
      "properties" in schema &&
      schema.properties &&
      typeof schema.properties === "object"
        ? (schema.properties as Record<string, { description?: string }>)
        : {};
    return Object.entries(properties).map(([name, value]) => ({
      name,
      description: value?.description ?? "",
    }));
  }
  return [];
}

function toEditorToolOption(tool: Tool): EditorToolOption {
  return {
    id: tool.id,
    functionName: tool.functionName || tool.name,
    label: tool.name || tool.functionName,
    parameters: toolParametersFromDefinition(tool),
  };
}

/** 固定输入表单主体（含锚点导航） */
export function NextReplyEditorBody({
  draft,
  onChange,
  availableTools,
}: {
  draft: CaseEditorDraft;
  onChange: (next: CaseEditorDraft) => void;
  /** 可选：由外层传入 Agent 已配置工具；缺省时编辑器自行拉取工具库 */
  availableTools?: EditorToolOption[];
}) {
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const sectionRefs = useRef<Record<EditorSectionId, HTMLElement | null>>({
    context: null,
    main: null,
    overall: null,
  });
  const selectedAnchorRef = useRef<EditorSectionId | null>(null);
  const [activeSection, setActiveSection] = useState<EditorSectionId>("main");
  const [loadedTools, setLoadedTools] = useState<EditorToolOption[]>([]);
  const [contextAddOpen, setContextAddOpen] = useState(false);
  const [expandedBehaviorIds, setExpandedBehaviorIds] = useState<Set<string>>(
    () => new Set(),
  );
  const sections = editorSections();

  const tools = useMemo(() => {
    if (availableTools && availableTools.length > 0) return availableTools;
    if (loadedTools.length > 0) return loadedTools;
    return FALLBACK_TOOLS;
  }, [availableTools, loadedTools]);

  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );

  useEffect(() => {
    if (availableTools && availableTools.length > 0) return;
    let cancelled = false;
    void (async () => {
      try {
        const list = await toolsApi.list();
        if (cancelled) return;
        const mapped = list.map(toEditorToolOption);
        setLoadedTools(mapped.length > 0 ? mapped : FALLBACK_TOOLS);
      } catch {
        if (!cancelled) setLoadedTools(FALLBACK_TOOLS);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [availableTools]);

  useEffect(() => {
    const container = scrollContainerRef.current;
    if (!container) return;
    const scrollContainer: HTMLDivElement = container;

    let animationFrame = 0;

    function updateActiveSection() {
      if (selectedAnchorRef.current) {
        setActiveSection(selectedAnchorRef.current);
        return;
      }

      const containerTop = scrollContainer.getBoundingClientRect().top;
      const activationLine = containerTop + 24;
      const sectionIds: EditorSectionId[] = ["context", "main", "overall"];
      let nextSection: EditorSectionId = sectionIds[0];

      for (const sectionId of sectionIds) {
        const element = sectionRefs.current[sectionId];
        if (element && element.getBoundingClientRect().top <= activationLine) {
          nextSection = sectionId;
        }
      }

      const reachedBottom =
        scrollContainer.scrollHeight > scrollContainer.clientHeight + 8 &&
        scrollContainer.scrollHeight -
          scrollContainer.scrollTop -
          scrollContainer.clientHeight <
          8;
      if (reachedBottom) {
        nextSection = sectionIds[sectionIds.length - 1];
      }

      setActiveSection((current) =>
        current === nextSection ? current : nextSection,
      );
    }

    function scheduleUpdate() {
      window.cancelAnimationFrame(animationFrame);
      animationFrame = window.requestAnimationFrame(updateActiveSection);
    }

    function releaseSelectedAnchor() {
      selectedAnchorRef.current = null;
      scheduleUpdate();
    }

    scheduleUpdate();
    scrollContainer.addEventListener("scroll", scheduleUpdate, {
      passive: true,
    });
    scrollContainer.addEventListener("wheel", releaseSelectedAnchor, {
      passive: true,
    });
    scrollContainer.addEventListener("touchstart", releaseSelectedAnchor, {
      passive: true,
    });
    window.addEventListener("resize", scheduleUpdate);

    return () => {
      window.cancelAnimationFrame(animationFrame);
      scrollContainer.removeEventListener("scroll", scheduleUpdate);
      scrollContainer.removeEventListener("wheel", releaseSelectedAnchor);
      scrollContainer.removeEventListener("touchstart", releaseSelectedAnchor);
      window.removeEventListener("resize", scheduleUpdate);
    };
  }, []);

  function scrollToSection(sectionId: EditorSectionId) {
    const container = scrollContainerRef.current;
    const section = sectionRefs.current[sectionId];
    if (!container || !section) return;

    const containerTop = container.getBoundingClientRect().top;
    const sectionTop = section.getBoundingClientRect().top;

    selectedAnchorRef.current = sectionId;
    setActiveSection(sectionId);
    container.scrollTo({
      top: container.scrollTop + sectionTop - containerTop,
      behavior: "smooth",
    });
  }

  function updateContext(index: number, patch: Partial<ContextTurn>) {
    onChange({
      ...draft,
      contextTurns: draft.contextTurns.map((turn, turnIndex) =>
        turnIndex === index ? { ...turn, ...patch } : turn,
      ),
    });
  }

  function addContextTurn(role: ContextRole) {
    onChange({
      ...draft,
      contextTurns: [
        ...draft.contextTurns,
        {
          role,
          content:
            role === "tool_call" || role === "tool_result" ? "{\n  \n}" : "",
          ...(role === "tool_call" || role === "tool_result"
            ? { toolName: "", toolCallId: "" }
            : {}),
        },
      ],
    });
    setContextAddOpen(false);
  }

  function removeContextTurn(index: number) {
    onChange({
      ...draft,
      contextTurns: draft.contextTurns.filter(
        (_, turnIndex) => turnIndex !== index,
      ),
    });
  }

  function updateTurns(turns: FixedInputTurn[]) {
    onChange({ ...draft, turns });
  }

  function updateTurn(turnId: string, patch: Partial<FixedInputTurn>) {
    updateTurns(
      draft.turns.map((turn) =>
        turn.id === turnId ? { ...turn, ...patch } : turn,
      ),
    );
  }

  function setTurnBehaviors(turnId: string, behaviors: ExpectedBehavior[]) {
    updateTurn(turnId, { behaviors });
  }

  function addBehavior(turnId: string, behavior: ExpectedBehavior) {
    const turn = draft.turns.find((item) => item.id === turnId);
    if (!turn) return;
    setTurnBehaviors(turnId, [...turn.behaviors, behavior]);
    setExpandedBehaviorIds((current) => new Set(current).add(behavior.id));
  }

  function updateBehavior(
    turnId: string,
    behaviorId: string,
    next: ExpectedBehavior,
  ) {
    const turn = draft.turns.find((item) => item.id === turnId);
    if (!turn) return;
    setTurnBehaviors(
      turnId,
      turn.behaviors.map((item) => (item.id === behaviorId ? next : item)),
    );
  }

  function removeBehavior(turnId: string, behaviorId: string) {
    const turn = draft.turns.find((item) => item.id === turnId);
    if (!turn) return;
    setTurnBehaviors(
      turnId,
      turn.behaviors.filter((item) => item.id !== behaviorId),
    );
    setExpandedBehaviorIds((current) => {
      const next = new Set(current);
      next.delete(behaviorId);
      return next;
    });
  }

  function addTurn() {
    updateTurns([...draft.turns, createEmptyFixedInputTurn()]);
  }

  function removeTurn(turnId: string) {
    if (draft.turns.length <= 1) return;
    updateTurns(draft.turns.filter((turn) => turn.id !== turnId));
  }

  function handleTurnDragEnd(event: DragEndEvent) {
    const { active, over } = event;
    if (!over || active.id === over.id) return;
    const oldIndex = draft.turns.findIndex((turn) => turn.id === active.id);
    const newIndex = draft.turns.findIndex((turn) => turn.id === over.id);
    if (oldIndex < 0 || newIndex < 0) return;
    updateTurns(arrayMove(draft.turns, oldIndex, newIndex));
  }

  function toggleBehaviorExpanded(behaviorId: string) {
    setExpandedBehaviorIds((current) => {
      const next = new Set(current);
      if (next.has(behaviorId)) next.delete(behaviorId);
      else next.add(behaviorId);
      return next;
    });
  }

  return (
    <div className="flex min-h-0 min-w-0 flex-1 flex-col">
      <SectionAnchorTabs
        ariaLabel="测试用例分区"
        sections={sections}
        activeSectionId={activeSection}
        onSelect={(sectionId) => scrollToSection(sectionId as EditorSectionId)}
        className="bg-background px-4 pt-3 sm:px-6 sm:pt-4 lg:px-8"
        contentClassName="mx-auto w-full max-w-3xl"
      />

      <div
        ref={scrollContainerRef}
        className="scrollbar-subtle min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-none bg-background px-4 pb-6 pt-3 sm:px-6 sm:pb-8 lg:px-8 lg:pb-10"
      >
        <div className="mx-auto max-w-3xl space-y-3">
          <section
            ref={(element) => {
              sectionRefs.current.context = element;
            }}
            className="scroll-mt-3 space-y-3"
          >
            <SectionCard
              icon={<MessagesSquare size={15} />}
              title="对话上下文"
              description="可选：运行前注入 pipeline 的历史消息与工具调用链"
              action={
                <Popover open={contextAddOpen} onOpenChange={setContextAddOpen}>
                  <PopoverTrigger asChild>
                    <Button
                      type="button"
                      variant="outline"
                      size="sm"
                      className="h-7 gap-1 border-hairline-strong text-xs text-muted-foreground"
                    >
                      <Plus size={13} />
                      添加消息
                    </Button>
                  </PopoverTrigger>
                  <PopoverContent align="end" className="w-60 p-1.5">
                    {(Object.keys(CONTEXT_ROLE_LABEL) as ContextRole[]).map(
                      (role) => (
                        <button
                          key={role}
                          type="button"
                          className="flex w-full items-center rounded-lg px-2.5 py-2 text-left text-sm text-foreground transition-colors hover:bg-surface-strong"
                          onClick={() => addContextTurn(role)}
                        >
                          {CONTEXT_ROLE_LABEL[role]}
                        </button>
                      ),
                    )}
                  </PopoverContent>
                </Popover>
              }
            >
              {draft.contextTurns.length === 0 ? (
                <p className="rounded-xl border border-dashed border-hairline-strong bg-canvas-soft/50 px-3 py-6 text-center text-xs text-muted-soft">
                  默认无上下文；可添加对话消息或工具调用记录
                </p>
              ) : (
                <div className="space-y-2">
                  {draft.contextTurns.map((turn, index) => (
                    <div
                      key={index}
                      className="space-y-2 rounded-xl border border-hairline bg-canvas-soft/30 p-2.5"
                    >
                      <div className="flex items-center gap-2">
                        <Select
                          value={turn.role}
                          onValueChange={(value) =>
                            updateContext(index, {
                              role: value as ContextRole,
                              content:
                                value === "tool_call" || value === "tool_result"
                                  ? "{\n  \n}"
                                  : "",
                              toolName: "",
                              toolCallId: "",
                            })
                          }
                        >
                          <SelectTrigger
                            aria-label={`上下文第 ${index + 1} 条消息类型`}
                            className="h-8 w-[122px] shrink-0 border-hairline-strong bg-background text-xs"
                          >
                            <SelectValue />
                          </SelectTrigger>
                          <SelectContent>
                            {(Object.keys(CONTEXT_ROLE_LABEL) as ContextRole[]).map(
                              (role) => (
                                <SelectItem key={role} value={role}>
                                  {CONTEXT_ROLE_LABEL[role]}
                                </SelectItem>
                              ),
                            )}
                          </SelectContent>
                        </Select>
                        <span className="min-w-0 flex-1 text-[11px] text-muted-soft">
                          第 {index + 1} 条
                        </span>
                        <Button
                          type="button"
                          variant="ghost"
                          size="icon-sm"
                          className="shrink-0 text-muted-soft hover:text-destructive"
                          onClick={() => removeContextTurn(index)}
                          aria-label="删除上下文"
                        >
                          <Trash2 size={14} />
                        </Button>
                      </div>

                      {(turn.role === "tool_call" ||
                        turn.role === "tool_result") && (
                        <div
                          className={cn(
                            "grid gap-2",
                            turn.role === "tool_result"
                              ? "sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_112px]"
                              : "sm:grid-cols-2",
                          )}
                        >
                          <Input
                            value={turn.toolName ?? ""}
                            onChange={(event) =>
                              updateContext(index, { toolName: event.target.value })
                            }
                            placeholder="工具名称，例如 transfer_to_human"
                            aria-label="工具名称"
                            className="h-8 border-hairline-strong bg-background font-mono text-xs"
                          />
                          <Input
                            value={turn.toolCallId ?? ""}
                            onChange={(event) =>
                              updateContext(index, {
                                toolCallId: event.target.value,
                              })
                            }
                            placeholder="Call ID（可选）"
                            aria-label="工具调用 ID"
                            className="h-8 border-hairline-strong bg-background font-mono text-xs"
                          />
                          {turn.role === "tool_result" && (
                            <Select
                              value={turn.isError ? "error" : "success"}
                              onValueChange={(value) =>
                                updateContext(index, {
                                  isError: value === "error",
                                })
                              }
                            >
                              <SelectTrigger
                                aria-label="工具返回状态"
                                className="h-8 border-hairline-strong bg-background text-xs"
                              >
                                <SelectValue />
                              </SelectTrigger>
                              <SelectContent>
                                <SelectItem value="success">成功</SelectItem>
                                <SelectItem value="error">错误</SelectItem>
                              </SelectContent>
                            </Select>
                          )}
                        </div>
                      )}

                      <Textarea
                        value={turn.content}
                        onChange={(event) =>
                          updateContext(index, { content: event.target.value })
                        }
                        rows={
                          turn.role === "tool_call" ||
                          turn.role === "tool_result"
                            ? 4
                            : 2
                        }
                        placeholder={contextPlaceholder(turn.role)}
                        className={cn(
                          "field-sizing-fixed resize-y border-hairline-strong bg-background text-sm",
                          (turn.role === "tool_call" ||
                            turn.role === "tool_result") && "font-mono text-xs",
                        )}
                      />
                    </div>
                  ))}
                </div>
              )}
            </SectionCard>
          </section>

          <section
            ref={(element) => {
              sectionRefs.current.main = element;
            }}
            className="scroll-mt-3 space-y-3"
          >
            <SectionCard
              icon={<MessageSquareText size={15} />}
              title="固定输入"
              description="每轮只填用户输入；可添加回复要求或工具调用等预期行为（AND）。"
            >
              <DndContext
                sensors={sensors}
                collisionDetection={closestCenter}
                onDragEnd={handleTurnDragEnd}
              >
                <SortableContext
                  items={draft.turns.map((turn) => turn.id)}
                  strategy={verticalListSortingStrategy}
                >
                  <div className="divide-y divide-hairline">
                    {draft.turns.map((turn, index) => (
                      <SortableFixedInputTurn
                        key={turn.id}
                        turn={turn}
                        index={index}
                        tools={tools}
                        canDelete={draft.turns.length > 1}
                        expandedBehaviorIds={expandedBehaviorIds}
                        onToggleBehavior={toggleBehaviorExpanded}
                        onChangeUserInput={(userInput) =>
                          updateTurn(turn.id, { userInput })
                        }
                        onAddReply={() =>
                          addBehavior(turn.id, createReplyBehavior("llm"))
                        }
                        onAddToolCall={() =>
                          addBehavior(turn.id, createToolCallBehavior())
                        }
                        onUpdateBehavior={(behavior) =>
                          updateBehavior(turn.id, behavior.id, behavior)
                        }
                        onRemoveBehavior={(behaviorId) =>
                          removeBehavior(turn.id, behaviorId)
                        }
                        onRemove={
                          draft.turns.length > 1
                            ? () => removeTurn(turn.id)
                            : undefined
                        }
                      />
                    ))}
                  </div>
                </SortableContext>
              </DndContext>

              <div className="flex justify-center border-t border-hairline pt-3">
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  className="h-8 gap-1.5 rounded-full border-hairline-strong text-xs text-muted-foreground"
                  onClick={addTurn}
                >
                  <Plus size={14} />
                  添加下一轮
                </Button>
              </div>
            </SectionCard>
          </section>

          <section
            ref={(element) => {
              sectionRefs.current.overall = element;
            }}
            className="scroll-mt-3 space-y-3"
          >
            <SectionCard
              icon={<Target size={15} />}
              title="整体评估标准"
              description="可添加多条独立标准；全部通过时，用例才算通过"
            >
              <OverallCriteriaFields
                value={draft.overallCriteria}
                onChange={(overallCriteria) =>
                  onChange({ ...draft, overallCriteria })
                }
              />
            </SectionCard>
          </section>
        </div>
      </div>
    </div>
  );
}

function OverallCriteriaFields({
  value,
  onChange,
}: {
  value: OverallCriterion[];
  onChange: (value: OverallCriterion[]) => void;
}) {
  function updateCriterion(id: string, patch: Partial<OverallCriterion>) {
    onChange(
      value.map((criterion) =>
        criterion.id === id ? { ...criterion, ...patch } : criterion,
      ),
    );
  }

  return (
    <div className="space-y-3">
      {value.length === 0 ? (
        <div className="rounded-xl border border-dashed border-hairline-strong bg-canvas-soft/50 px-3 py-6 text-center">
          <p className="text-xs text-muted-soft">暂未配置整段对话评估</p>
        </div>
      ) : (
        <div className="space-y-2.5">
          {value.map((criterion, index) => (
            <div
              key={criterion.id}
              className="space-y-2.5 rounded-xl border border-hairline bg-canvas-soft/30 p-3"
            >
              <div className="flex items-center gap-2">
                <span className="text-xs font-medium text-foreground">
                  标准 {index + 1}
                </span>
                <span className="rounded-full border border-hairline px-2 py-0.5 text-[10px] text-muted-soft">
                  LLM 判断
                </span>
                <span className="min-w-0 flex-1" />
                <Button
                  type="button"
                  variant="ghost"
                  size="icon-sm"
                  className="h-7 w-7 text-muted-soft hover:text-destructive"
                  onClick={() =>
                    onChange(value.filter((item) => item.id !== criterion.id))
                  }
                  aria-label={`删除整体评估标准 ${index + 1}`}
                >
                  <Trash2 size={13} />
                </Button>
              </div>
              <Input
                value={criterion.name}
                onChange={(event) =>
                  updateCriterion(criterion.id, { name: event.target.value })
                }
                placeholder="标准名称，例如：正确处理人伤场景"
                aria-label={`整体评估标准 ${index + 1} 名称`}
                className="h-8 border-hairline-strong bg-background text-xs"
              />
              <Textarea
                value={criterion.criteria}
                onChange={(event) =>
                  updateCriterion(criterion.id, {
                    criteria: event.target.value.slice(
                      0,
                      OVERALL_CRITERION_TEXT_MAX,
                    ),
                  })
                }
                rows={4}
                placeholder="描述整段对话结束后应达到的业务目标，以及不应出现的行为…"
                className="field-sizing-fixed min-h-[96px] resize-y border-hairline-strong bg-background text-sm"
              />
              <div className="flex justify-end text-[11px] tabular-nums text-muted-soft">
                {criterion.criteria.length} / {OVERALL_CRITERION_TEXT_MAX}
              </div>
            </div>
          ))}
        </div>
      )}

      <div className="flex items-center justify-between gap-3">
        <p className="text-[11px] leading-4 text-muted-soft">
          运行结果会逐条展示；当前通过规则固定为 AND。
        </p>
        <Button
          type="button"
          variant="outline"
          size="sm"
          className="h-8 shrink-0 gap-1.5 rounded-full border-hairline-strong text-xs text-muted-foreground"
          onClick={() =>
            onChange([
              ...value,
              createOverallCriterion(`评估标准 ${value.length + 1}`),
            ])
          }
        >
          <Plus size={13} />
          添加标准
        </Button>
      </div>
    </div>
  );
}

function SortableFixedInputTurn({
  turn,
  index,
  tools,
  canDelete,
  expandedBehaviorIds,
  onToggleBehavior,
  onChangeUserInput,
  onAddReply,
  onAddToolCall,
  onUpdateBehavior,
  onRemoveBehavior,
  onRemove,
}: {
  turn: FixedInputTurn;
  index: number;
  tools: EditorToolOption[];
  canDelete: boolean;
  expandedBehaviorIds: Set<string>;
  onToggleBehavior: (behaviorId: string) => void;
  onChangeUserInput: (value: string) => void;
  onAddReply: () => void;
  onAddToolCall: () => void;
  onUpdateBehavior: (behavior: ExpectedBehavior) => void;
  onRemoveBehavior: (behaviorId: string) => void;
  onRemove?: () => void;
}) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: turn.id });
  const [addOpen, setAddOpen] = useState(false);

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={cn(
        "group relative py-3.5 first:pt-0 last:pb-1",
        isDragging && "z-10 rounded-xl bg-card shadow-md ring-1 ring-hairline",
      )}
    >
      <div className="mb-2.5 flex items-center gap-1">
        <button
          type="button"
          className={cn(
            "flex h-7 w-6 shrink-0 cursor-grab items-center justify-center rounded text-muted-soft opacity-40 transition-opacity active:cursor-grabbing group-hover:opacity-100",
            isDragging && "opacity-100",
          )}
          aria-label={`拖拽调整第 ${index + 1} 轮顺序`}
          {...attributes}
          {...listeners}
        >
          <GripVertical size={14} />
        </button>

        <div className="min-w-0 flex-1 text-sm font-medium text-foreground">
          第 {index + 1} 轮
        </div>

        {canDelete && onRemove && (
          <Button
            type="button"
            variant="ghost"
            size="icon-sm"
            className="h-7 w-7 shrink-0 text-muted-soft opacity-40 transition-opacity hover:text-destructive group-hover:opacity-100"
            onClick={onRemove}
            aria-label={`删除第 ${index + 1} 轮`}
          >
            <Trash2 size={14} />
          </Button>
        )}
      </div>

      <div className="space-y-3 pl-1">
        <div className="space-y-1.5">
          <div className="text-xs font-medium text-muted-foreground">
            用户输入
          </div>
          <Textarea
            value={turn.userInput}
            onChange={(event) => onChangeUserInput(event.target.value)}
            placeholder="输入本轮用户原话…"
            rows={2}
            className="field-sizing-fixed min-h-[56px] resize-y border-hairline-strong bg-background text-sm"
          />
        </div>

        <div className="space-y-2">
          {turn.behaviors.length > 0 && (
            <div className="text-xs font-medium text-muted-foreground">
              预期行为
            </div>
          )}

          <div className="space-y-2">
            {turn.behaviors.map((behavior) => (
              <ExpectedBehaviorCard
                key={behavior.id}
                behavior={behavior}
                tools={tools}
                expanded={expandedBehaviorIds.has(behavior.id)}
                onToggle={() => onToggleBehavior(behavior.id)}
                onChange={onUpdateBehavior}
                onRemove={() => onRemoveBehavior(behavior.id)}
              />
            ))}
          </div>

          <Popover open={addOpen} onOpenChange={setAddOpen}>
            <PopoverTrigger asChild>
              <button
                type="button"
                className="inline-flex h-7 items-center gap-1 text-xs text-muted-soft transition-colors hover:text-foreground"
              >
                <Plus size={13} />
                添加预期行为
              </button>
            </PopoverTrigger>
            <PopoverContent align="start" className="w-72 p-1.5">
              <div className="px-2.5 py-1.5 text-[11px] font-medium tracking-wide text-muted-soft uppercase">
                添加预期行为
              </div>
              <button
                type="button"
                className="flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-surface-strong"
                onClick={() => {
                  onAddReply();
                  setAddOpen(false);
                }}
              >
                <MessageSquareText
                  size={15}
                  className="mt-0.5 shrink-0 text-muted-foreground"
                />
                <span className="min-w-0">
                  <span className="block text-sm font-medium text-foreground">
                    回复要求
                  </span>
                  <span className="mt-0.5 block text-xs leading-4 text-muted-soft">
                    验证 Agent 回复的内容或语义
                  </span>
                </span>
              </button>
              <button
                type="button"
                className="flex w-full items-start gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-surface-strong"
                onClick={() => {
                  onAddToolCall();
                  setAddOpen(false);
                }}
              >
                <Wrench
                  size={15}
                  className="mt-0.5 shrink-0 text-violet-400/90"
                />
                <span className="min-w-0">
                  <span className="block text-sm font-medium text-foreground">
                    工具调用
                  </span>
                  <span className="mt-0.5 block text-xs leading-4 text-muted-soft">
                    验证 Agent 是否调用指定工具及参数
                  </span>
                </span>
              </button>
            </PopoverContent>
          </Popover>
        </div>
      </div>
    </div>
  );
}

function ExpectedBehaviorCard({
  behavior,
  tools,
  expanded,
  onToggle,
  onChange,
  onRemove,
}: {
  behavior: ExpectedBehavior;
  tools: EditorToolOption[];
  expanded: boolean;
  onToggle: () => void;
  onChange: (behavior: ExpectedBehavior) => void;
  onRemove: () => void;
}) {
  const validationMessage = getExpectedBehaviorValidationMessage(behavior);

  if (!expanded) {
    return (
      <button
        type="button"
        onClick={onToggle}
        aria-expanded={false}
        title="展开配置"
        className={cn(
          "group/card w-full rounded-xl border px-3 py-2.5 text-left transition-colors hover:bg-canvas-soft/60",
          behavior.type === "tool_call"
            ? "border-violet-500/25 bg-violet-500/[0.04]"
            : "border-hairline bg-canvas-soft/30",
          validationMessage && "border-destructive/40",
        )}
      >
        <div className="flex items-center gap-3">
          <div className="min-w-0 flex-1">
            <BehaviorSummary behavior={behavior} />
            {validationMessage && (
              <p className="mt-1.5 pl-[18px] text-[11px] text-destructive">
                {validationMessage}
              </p>
            )}
          </div>
          <span className="inline-flex shrink-0 items-center gap-1 text-[11px] text-muted-soft transition-colors group-hover/card:text-foreground">
            展开配置
            <ChevronDown size={14} />
          </span>
        </div>
      </button>
    );
  }

  return (
    <div
      className={cn(
        "rounded-xl border px-3 py-2.5",
        behavior.type === "tool_call"
          ? "border-violet-500/30 bg-violet-500/[0.04]"
          : "border-hairline bg-canvas-soft/40",
        validationMessage && "border-destructive/40",
      )}
    >
      <div className="mb-2.5 flex items-center gap-2">
        <button
          type="button"
          onClick={onToggle}
          aria-expanded={true}
          title="收起配置"
          className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
        >
          {behavior.type === "reply" ? (
            <MessageSquareText size={14} className="shrink-0 text-muted-foreground" />
          ) : (
            <Wrench size={14} className="shrink-0 text-violet-400/90" />
          )}
          <span className="text-xs font-medium text-foreground">
            {behavior.type === "reply" ? "回复要求" : "工具调用"}
          </span>
          <span className="ml-auto inline-flex items-center gap-1 text-[11px] font-normal text-muted-soft">
            收起
            <ChevronUp size={14} />
          </span>
        </button>
        <Button
          type="button"
          variant="ghost"
          size="icon-sm"
          className="h-7 w-7 shrink-0 text-muted-soft hover:text-destructive"
          onClick={onRemove}
          aria-label="删除预期行为"
        >
          <Trash2 size={13} />
        </Button>
      </div>

      {behavior.type === "reply" ? (
        <ReplyBehaviorEditor
          behavior={behavior}
          onChange={onChange}
        />
      ) : (
        <ToolCallBehaviorEditor
          behavior={behavior}
          tools={tools}
          onChange={onChange}
        />
      )}
      {validationMessage && (
        <p className="mt-2 text-[11px] text-destructive">
          {validationMessage}
        </p>
      )}
    </div>
  );
}

function BehaviorSummary({ behavior }: { behavior: ExpectedBehavior }) {
  if (behavior.type === "reply") {
    const modeLabel = ASSERTION_TYPE_LABEL[behavior.assertionType];
    const detail =
      behavior.assertionType === "keyword"
        ? behavior.keywords.length > 0
          ? behavior.keywords.join("、")
          : "未配置关键词"
        : behavior.llmCriteria.trim() || "未填写判断标准";
    return (
      <div className="space-y-1">
        <div className="flex items-center gap-1.5 text-xs font-medium text-foreground">
          <MessageSquareText size={13} className="text-muted-foreground" />
          回复要求 · {modeLabel}
          {behavior.assertionType === "keyword" && behavior.negateKeywords
            ? " · 不应包含"
            : ""}
        </div>
        <p className="line-clamp-2 pl-[18px] text-xs leading-4 text-muted-soft">
          {detail}
        </p>
      </div>
    );
  }

  const name = behavior.functionName || "未选择工具";
  const params = behavior.paramAssertions;
  return (
    <div className="space-y-1">
      <div className="flex items-center gap-1.5 text-xs font-medium text-foreground">
        <Wrench size={13} className="text-violet-400/90" />
        {behavior.expectation === "not_called" ? "禁止调用 · " : ""}
        {name}
      </div>
      {behavior.expectation === "not_called" ? (
        <p className="pl-[18px] text-xs text-muted-soft">调用次数必须为 0</p>
      ) : params.length === 0 ? (
        <p className="pl-[18px] text-xs text-muted-soft">
          调用 {behavior.minCalls}
          {behavior.maxCalls === null
            ? " 次以上"
            : behavior.minCalls === behavior.maxCalls
              ? " 次"
              : `–${behavior.maxCalls} 次`}
          ，不校验参数
        </p>
      ) : (
        <div className="space-y-0.5 pl-[18px]">
          {params.map((param) => (
            <p key={param.name} className="text-xs text-muted-soft">
              {param.name} · {TOOL_PARAM_MATCH_MODE_LABEL[param.matchMode]}
              {param.value.trim() ? ` · ${param.value}` : ""}
            </p>
          ))}
        </div>
      )}
    </div>
  );
}

function ReplyBehaviorEditor({
  behavior,
  onChange,
}: {
  behavior: ReplyExpectedBehavior;
  onChange: (behavior: ReplyExpectedBehavior) => void;
}) {
  const [keywordDraft, setKeywordDraft] = useState("");

  function addKeyword(raw: string) {
    const value = raw.trim();
    if (!value) return;
    if (behavior.keywords.includes(value)) {
      setKeywordDraft("");
      return;
    }
    onChange({ ...behavior, keywords: [...behavior.keywords, value] });
    setKeywordDraft("");
  }

  function removeKeyword(index: number) {
    onChange({
      ...behavior,
      keywords: behavior.keywords.filter((_, itemIndex) => itemIndex !== index),
    });
  }

  return (
    <div className="space-y-2.5">
      <div className="grid gap-2 sm:grid-cols-[88px_minmax(0,1fr)] sm:items-center">
        <div className="text-xs text-muted-soft">判断方式</div>
        <Select
          value={behavior.assertionType}
          onValueChange={(value) =>
            onChange({
              ...behavior,
              assertionType: value as AssertionType,
            })
          }
        >
          <SelectTrigger className="h-8 border-hairline-strong bg-background text-xs">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="keyword">
              {ASSERTION_TYPE_LABEL.keyword}
            </SelectItem>
            <SelectItem value="llm">{ASSERTION_TYPE_LABEL.llm}</SelectItem>
          </SelectContent>
        </Select>
      </div>

      {behavior.assertionType === "keyword" ? (
        <>
          <div className="grid gap-2 sm:grid-cols-[88px_minmax(0,1fr)] sm:items-start">
            <div className="pt-2 text-xs text-muted-soft">关键词</div>
            <div className="space-y-1.5">
              <div className="flex min-h-9 flex-wrap items-center gap-1.5 rounded-xl border border-hairline-strong bg-background px-2 py-1.5">
                {behavior.keywords.map((keyword, keywordIndex) => (
                  <span
                    key={`${keyword}-${keywordIndex}`}
                    className="inline-flex h-6 items-center gap-1 rounded-full bg-surface-strong px-2 text-[11px] text-foreground"
                  >
                    {keyword}
                    <button
                      type="button"
                      className="text-muted-soft hover:text-destructive"
                      onClick={() => removeKeyword(keywordIndex)}
                      aria-label={`删除关键词 ${keyword}`}
                    >
                      <X size={11} />
                    </button>
                  </span>
                ))}
                <Input
                  value={keywordDraft}
                  onChange={(event) => setKeywordDraft(event.target.value)}
                  onKeyDown={(event) => {
                    if (
                      event.key === "Enter" ||
                      event.key === "," ||
                      event.key === "，"
                    ) {
                      event.preventDefault();
                      addKeyword(keywordDraft);
                    } else if (
                      event.key === "Backspace" &&
                      !keywordDraft &&
                      behavior.keywords.length > 0
                    ) {
                      removeKeyword(behavior.keywords.length - 1);
                    }
                  }}
                  placeholder={
                    behavior.keywords.length === 0 ? "输入关键词" : ""
                  }
                  className="h-6 min-w-[96px] flex-1 border-0 bg-transparent px-1 text-xs shadow-none focus-visible:ring-0"
                />
              </div>
              <button
                type="button"
                onClick={() => addKeyword(keywordDraft)}
                className="inline-flex h-6 items-center gap-1 text-[11px] text-muted-soft transition-colors hover:text-foreground"
              >
                <Plus size={12} />
                添加
              </button>
            </div>
          </div>

          <div className="grid gap-2 sm:grid-cols-[88px_minmax(0,1fr)] sm:items-center">
            <div className="text-xs text-muted-soft">关键词要求</div>
            <div className="grid gap-2 sm:grid-cols-2">
              <Select
                value={behavior.negateKeywords ? "not_contain" : "contain"}
                onValueChange={(value) =>
                  onChange({
                    ...behavior,
                    negateKeywords: value === "not_contain",
                  })
                }
              >
                <SelectTrigger
                  aria-label="关键词正向或负向要求"
                  className="h-8 border-hairline-strong bg-background text-xs"
                >
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="contain">应包含</SelectItem>
                  <SelectItem value="not_contain">不应包含</SelectItem>
                </SelectContent>
              </Select>
              <Select
                value={behavior.keywordMatchMode}
                onValueChange={(value) =>
                  onChange({
                    ...behavior,
                    keywordMatchMode: value as KeywordMatchMode,
                  })
                }
              >
                <SelectTrigger
                  aria-label="关键词匹配方式"
                  className="h-8 border-hairline-strong bg-background text-xs"
                >
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="any">
                    {KEYWORD_MATCH_MODE_LABEL.any}
                  </SelectItem>
                  <SelectItem value="all">
                    {KEYWORD_MATCH_MODE_LABEL.all}
                  </SelectItem>
                </SelectContent>
              </Select>
            </div>
          </div>
        </>
      ) : (
        <div className="grid gap-2 sm:grid-cols-[88px_minmax(0,1fr)] sm:items-start">
          <div className="pt-2 text-xs text-muted-soft">判断标准</div>
          <Textarea
            value={behavior.llmCriteria}
            onChange={(event) =>
              onChange({ ...behavior, llmCriteria: event.target.value })
            }
            rows={3}
            placeholder="应确认存在人员受伤，并告知用户正在转人工处理"
            className="field-sizing-fixed min-h-[72px] resize-y border-hairline-strong bg-background text-sm"
          />
        </div>
      )}
    </div>
  );
}

function ToolCallBehaviorEditor({
  behavior,
  tools,
  onChange,
}: {
  behavior: ToolCallExpectedBehavior;
  tools: EditorToolOption[];
  onChange: (behavior: ToolCallExpectedBehavior) => void;
}) {
  const selectedTool =
    tools.find((tool) => tool.id === behavior.toolId) ??
    tools.find((tool) => tool.functionName === behavior.functionName) ??
    null;

  const schemaParams = selectedTool?.parameters ?? [];

  function selectTool(toolId: string) {
    const tool = tools.find((item) => item.id === toolId);
    if (!tool) return;
    onChange({
      ...behavior,
      toolId: tool.id,
      functionName: tool.functionName,
      paramAssertions: [],
    });
  }

  function paramAssertion(name: string): ToolParamAssertion | null {
    return (
      behavior.paramAssertions.find((item) => item.name === name) ?? null
    );
  }

  function setParamMode(name: string, mode: ToolParamMatchMode | "none") {
    const rest = behavior.paramAssertions.filter((item) => item.name !== name);
    if (mode === "none") {
      onChange({ ...behavior, paramAssertions: rest });
      return;
    }
    const existing = paramAssertion(name);
    onChange({
      ...behavior,
      paramAssertions: [
        ...rest,
        {
          name,
          matchMode: mode,
          value: existing?.value ?? "",
        },
      ],
    });
  }

  function setParamValue(name: string, value: string) {
    const existing = paramAssertion(name);
    if (!existing) return;
    onChange({
      ...behavior,
      paramAssertions: behavior.paramAssertions.map((item) =>
        item.name === name ? { ...item, value } : item,
      ),
    });
  }

  return (
    <div className="space-y-2.5">
      <div className="grid gap-2 sm:grid-cols-[88px_minmax(0,1fr)] sm:items-center">
        <div className="text-xs text-muted-soft">调用要求</div>
        <Select
          value={behavior.expectation}
          onValueChange={(value) =>
            onChange({
              ...behavior,
              expectation: value as ToolCallExpectedBehavior["expectation"],
            })
          }
        >
          <SelectTrigger
            aria-label="工具调用要求"
            className="h-8 border-hairline-strong bg-background text-xs"
          >
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="called">应调用</SelectItem>
            <SelectItem value="not_called">不应调用</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <div className="grid gap-2 sm:grid-cols-[88px_minmax(0,1fr)] sm:items-center">
        <div className="text-xs text-muted-soft">预期工具</div>
        <Select
          value={behavior.toolId}
          onValueChange={selectTool}
        >
          <SelectTrigger className="h-8 border-hairline-strong bg-background text-xs">
            <SelectValue placeholder="选择工具…" />
          </SelectTrigger>
          <SelectContent>
            {tools.map((tool) => (
              <SelectItem key={tool.id} value={tool.id}>
                {tool.functionName}
                {tool.label && tool.label !== tool.functionName
                  ? ` · ${tool.label}`
                  : ""}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
      </div>

      {selectedTool && behavior.expectation === "called" && (
        <div className="grid gap-2 sm:grid-cols-[88px_minmax(0,1fr)] sm:items-center">
          <div className="text-xs text-muted-soft">调用次数</div>
          <div className="flex flex-wrap items-center gap-2">
            <Input
              type="number"
              min={1}
              step={1}
              value={behavior.minCalls}
              onChange={(event) => {
                const next = Number(event.target.value);
                onChange({
                  ...behavior,
                  minCalls: Number.isFinite(next) ? Math.max(1, Math.round(next)) : 1,
                });
              }}
              aria-label="最少调用次数"
              className="h-8 w-20 border-hairline-strong bg-background text-xs"
            />
            <span className="text-xs text-muted-soft">至</span>
            <Input
              type="number"
              min={behavior.minCalls}
              step={1}
              value={behavior.maxCalls ?? ""}
              onChange={(event) => {
                const raw = event.target.value;
                const next = Number(raw);
                onChange({
                  ...behavior,
                  maxCalls:
                    raw === "" || !Number.isFinite(next)
                      ? null
                      : Math.max(behavior.minCalls, Math.round(next)),
                });
              }}
              placeholder="不限"
              aria-label="最多调用次数"
              className="h-8 w-20 border-hairline-strong bg-background text-xs"
            />
            <span className="text-xs text-muted-soft">次（上限可留空）</span>
          </div>
        </div>
      )}

      {selectedTool && behavior.expectation === "called" && (
        <div className="space-y-2">
          <div className="text-xs font-medium text-muted-foreground">
            参数要求
          </div>
          {schemaParams.length === 0 ? (
            <p className="text-xs text-muted-soft">该工具无参数可校验</p>
          ) : (
            <div className="space-y-2.5">
              {schemaParams.map((param) => {
                const assertion = paramAssertion(param.name);
                const mode = assertion?.matchMode ?? "none";
                return (
                  <div
                    key={param.name}
                    className="space-y-1.5 rounded-lg border border-hairline/80 bg-background/60 px-2.5 py-2"
                  >
                    <div className="flex flex-wrap items-center gap-2">
                      <span className="min-w-0 flex-1 font-mono text-xs text-foreground">
                        {param.name}
                      </span>
                      <Select
                        value={mode}
                        onValueChange={(value) =>
                          setParamMode(
                            param.name,
                            value as ToolParamMatchMode | "none",
                          )
                        }
                      >
                        <SelectTrigger className="h-7 w-[110px] border-hairline-strong bg-background text-[11px]">
                          <SelectValue />
                        </SelectTrigger>
                        <SelectContent>
                          <SelectItem value="none">不校验</SelectItem>
                          <SelectItem value="exact">
                            {TOOL_PARAM_MATCH_MODE_LABEL.exact}
                          </SelectItem>
                          <SelectItem value="regex">
                            {TOOL_PARAM_MATCH_MODE_LABEL.regex}
                          </SelectItem>
                          <SelectItem value="llm">
                            {TOOL_PARAM_MATCH_MODE_LABEL.llm}
                          </SelectItem>
                        </SelectContent>
                      </Select>
                    </div>
                    {assertion && (
                      assertion.matchMode === "llm" ? (
                        <Textarea
                          value={assertion.value}
                          onChange={(event) =>
                            setParamValue(param.name, event.target.value)
                          }
                          rows={2}
                          placeholder="应表达由于存在人员受伤，需要转人工处理"
                          className="field-sizing-fixed min-h-[56px] resize-y border-hairline-strong bg-background text-xs"
                        />
                      ) : (
                        <Input
                          value={assertion.value}
                          onChange={(event) =>
                            setParamValue(param.name, event.target.value)
                          }
                          placeholder={
                            assertion.matchMode === "regex"
                              ? "^ACC-[0-9]{8}$"
                              : "期望值"
                          }
                          className="h-8 border-hairline-strong bg-background text-xs"
                        />
                      )
                    )}
                  </div>
                );
              })}
            </div>
          )}
          <p className="text-[11px] leading-4 text-muted-soft">
            默认不校验参数；仅主动配置的参数参与断言。
          </p>
        </div>
      )}

      {selectedTool && behavior.expectation === "called" && (
        <div className="space-y-2 rounded-lg border border-violet-500/20 bg-background/60 p-2.5">
          <div>
            <div className="text-xs font-medium text-foreground">
              Mock 工具响应
            </div>
            <p className="mt-0.5 text-[11px] leading-4 text-muted-soft">
              工具被调用后将此结果注入 pipeline，使下一轮可稳定复现。
            </p>
          </div>
          <div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_112px]">
            <Select
              value={behavior.mockResponse.outcome}
              onValueChange={(value) =>
                onChange({
                  ...behavior,
                  mockResponse: {
                    ...behavior.mockResponse,
                    outcome: value as "success" | "error",
                  },
                })
              }
            >
              <SelectTrigger
                aria-label="Mock 工具响应类型"
                className="h-8 border-hairline-strong bg-background text-xs"
              >
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="success">成功响应</SelectItem>
                <SelectItem value="error">错误响应</SelectItem>
              </SelectContent>
            </Select>
            <div className="flex items-center gap-1.5">
              <Input
                type="number"
                min={0}
                max={60000}
                step={100}
                value={behavior.mockResponse.delayMs}
                onChange={(event) => {
                  const next = Number(event.target.value);
                  onChange({
                    ...behavior,
                    mockResponse: {
                      ...behavior.mockResponse,
                      delayMs: Number.isFinite(next)
                        ? Math.min(60000, Math.max(0, Math.round(next)))
                        : 0,
                    },
                  });
                }}
                aria-label="Mock 工具响应延迟"
                className="h-8 border-hairline-strong bg-background text-xs"
              />
              <span className="text-[11px] text-muted-soft">ms</span>
            </div>
          </div>
          <Textarea
            value={behavior.mockResponse.body}
            onChange={(event) =>
              onChange({
                ...behavior,
                mockResponse: {
                  ...behavior.mockResponse,
                  body: event.target.value,
                },
              })
            }
            rows={5}
            aria-label="Mock 工具响应 JSON"
            placeholder={'{\n  "status": "ok"\n}'}
            className="field-sizing-fixed min-h-[112px] resize-y border-hairline-strong bg-background font-mono text-xs"
          />
        </div>
      )}

      {selectedTool && behavior.expectation === "not_called" && (
        <p className="rounded-lg border border-hairline bg-background/60 px-2.5 py-2 text-[11px] leading-4 text-muted-soft">
          本轮该工具的允许调用次数为 0；参数与 Mock 响应无需配置。
        </p>
      )}
    </div>
  );
}
