<script setup lang="ts">
/**
 * QuizBuilder — visual designer for a list of questions, grouped into
 * one or more question banks. The component's scope is the *questions*
 * and the *banks* that group them; quiz-level metadata (title,
 * scoring, …) is owned by the host page.
 *
 * Layout (left → right):
 *   - NAV RAIL (icon-only, always visible):
 *       • `?`  toggles the question-types column.
 *       • cog  (only when banks are enabled) toggles the bank-config
 *              column for the active bank.
 *       • —    divider (banks enabled).
 *       • DB   one icon per bank; the active one is highlighted.
 *       • `+`  adds a new bank, which becomes selected and opens the
 *              config column for inline naming.
 *   - SIDEBAR (toggleable, one of two modes):
 *       • palette — question-type chips / customElementList.
 *       • config  — bank name + description editor.
 *       A close button collapses the column.
 *   - LAYERS (always visible):
 *       The active bank's questions. Drag handle on the left of every
 *       row reorders within the bank. Selecting a row opens the
 *       inspector.
 *   - INSPECTOR (collapses to icon when nothing is selected, expands
 *       to ~280px when a question is selected — mirrors FormBuilder).
 *
 * State lives in `quizBuilderStore` (Pinia). The component accepts an
 * optional `modelValue` schema (v-modelable) and emits the current
 * schema on every mutation.
 */
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue"
import type { Component } from "vue"
import * as LucideIcons from "@lucide/vue"
import {
  Calculator,
  CheckSquare,
  ChevronRight,
  CircleDot,
  CirclePlus,
  Database,
  GraduationCap,
  GripVertical,
  Info,
  Keyboard,
  LayersPlus,
  LayoutGrid,
  Link2,
  ListChecks,
  ListOrdered,
  PenLine,
  Plus,
  Settings,
  Sparkles,
  ToggleRight,
  Trash2,
  Underline,
  X,
} from "@lucide/vue"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip"
import {
  useQuizBuilder,
  type AiCriterion,
  type AiScoringMethod,
  type FillBlank,
  type MatchPair,
  type QuestionBank,
  type QuizBuilderSchema,
  type QuizCategory,
  type QuizOption,
  type QuizQuestion,
  type QuizQuestionType,
} from "@/stores/quizBuilderStore"
import { cn } from "@/lib/utils"

/** A single item inside a `CustomQuestionGroup.questions` array. */
export interface CustomQuestionItem {
  /** Quiz question type. */
  type: QuizQuestionType
  /** Display label shown in the palette and pre-filled on the question. */
  label: string
  /** Optional Lucide icon name (kebab-case or PascalCase). */
  icon?: string
  /**
   * Extra properties merged onto the question when it is added (prompt,
   * helpText, required, options, …).
   */
  config?: Partial<Omit<QuizQuestion, "id" | "type" | "bankId">>
}

/** A collapsible group in `customElementList`. */
export interface CustomQuestionGroup {
  type: "question-group"
  label: string
  questions: CustomQuestionItem[]
}

/** Union accepted by the `customElementList` prop. */
export type CustomQuestionListItem = CustomQuestionGroup | CustomQuestionItem

export interface QuizBuilderProps {
  /** Initial / current schema. Two-way bindable via v-model. */
  modelValue?: Partial<QuizBuilderSchema>
  /**
   * Identifier used to key the quizBuilderStore. Defaults to a stable
   * auto-generated id.
   */
  builderId?: string
  label?: string
  name?: string | null
  item?: Record<string, unknown>
  classList?: string[] | string
  helpText?: string
  errors?: string[]
  hideLabel?: boolean
  required?: boolean
  readonly?: boolean
  disabled?: boolean
  /**
   * Quiz question types to exclude from the palette, e.g.
   * `["free-text"]`. Has no effect when `customElementList` is set.
   */
  hideElementTypes?: QuizQuestionType[] | string[]
  /**
   * Replaces the default question-type chips with a custom list. Each
   * top-level item is either a collapsible `question-group` or a bare
   * question entry.
   */
  customElementList?: CustomQuestionListItem[]
  /** When `true`, all groups in `customElementList` start expanded. */
  openAllSections?: boolean
  /**
   * When `true`, exposes bank-management UI: cog (config), per-bank
   * icons, and the "+" add-bank button. When `false`, all questions
   * implicitly land in the default initial bank. Defaults to `false`.
   */
  enableQuestionBanks?: boolean
  /**
   * When `true`, palette items show a drag handle and must be dragged
   * onto the layers panel. Otherwise items are click-to-add.
   */
  dragToAdd?: boolean
  /**
   * Caps the overall builder height so each column (palette/config,
   * layers, inspector) scrolls independently instead of the whole
   * component growing with its tallest column. Any CSS length. Defaults
   * to `"40rem"`; pass `"none"` to disable the cap.
   */
  maxHeight?: string
}

const props = withDefaults(defineProps<QuizBuilderProps>(), {
  errors: () => [],
  item: () => ({}),
  maxHeight: "40rem",
})

const model = defineModel<Partial<QuizBuilderSchema> | undefined>("modelValue")

let nextAutoId = 0
function makeAutoId(): string {
  nextAutoId += 1
  return `quiz-builder-${Date.now()}-${nextAutoId}`
}
const fallbackId = ref<string>(makeAutoId())
const builderId = computed<string>(() => props.builderId ?? fallbackId.value)

const builder = useQuizBuilder(builderId.value)

function isPartialSchema(value: unknown): value is Partial<QuizBuilderSchema> {
  return typeof value === "object" && value !== null
}

onMounted(() => {
  const seed = model.value
  if (isPartialSchema(seed)) {
    builder.loadSchema(seed)
  }
  // Active bank defaults to the first (always exists).
  activeBankId.value = builder.schema.value.questionBanks[0]!.id
})

watch(
  () => builder.schema.value,
  (next) => {
    model.value = {
      questionBanks: next.questionBanks.map((b) => ({ ...b })),
      questions: next.questions.map((q) => ({
        ...q,
        options: q.options.map((o) => ({ ...o })),
      })),
    }
  },
  { deep: true },
)

/* ------------------------------------------------------------------ */
/* Sidebar mode + active bank                                           */
/* ------------------------------------------------------------------ */

type SidebarMode = "palette" | "config"

const sidebarMode = ref<SidebarMode | null>(null)
/** Active bank id. The schema always has at least one bank. */
const activeBankId = ref<string>("")

const activeBank = computed<QuestionBank | null>(() => {
  const id = activeBankId.value
  return builder.schema.value.questionBanks.find((b) => b.id === id) ?? null
})

function setSidebarMode(mode: SidebarMode): void {
  sidebarMode.value = sidebarMode.value === mode ? null : mode
}

function closeSidebar(): void {
  sidebarMode.value = null
}

function selectBank(bankId: string): void {
  activeBankId.value = bankId
  builder.select(null)
}

// If the active bank is removed elsewhere, fall back to the first.
watch(
  () => builder.schema.value.questionBanks,
  (banks) => {
    if (!banks.find((b) => b.id === activeBankId.value)) {
      activeBankId.value = banks[0]?.id ?? ""
    }
  },
  { deep: true },
)

// Banks are only managed when enabled; if disabled, force config closed.
watch(
  () => props.enableQuestionBanks,
  (enabled) => {
    if (!enabled && sidebarMode.value === "config") {
      sidebarMode.value = null
    }
  },
)

/* ------------------------------------------------------------------ */
/* Palette                                                              */
/* ------------------------------------------------------------------ */

interface QuestionTypeChip {
  type: QuizQuestionType
  label: string
  icon: Component
  description?: string
}

const DEFAULT_CHIPS: QuestionTypeChip[] = [
  {
    type: "single-choice",
    label: "Single choice",
    icon: CircleDot,
    description: "Pick one correct answer",
  },
  {
    type: "multiple-choice",
    label: "Multiple choice",
    icon: CheckSquare,
    description: "Pick one or more correct answers",
  },
  {
    type: "true-false",
    label: "True / False",
    icon: ToggleRight,
    description: "Binary true/false answer",
  },
  {
    type: "text-match",
    label: "Text match",
    icon: Keyboard,
    description: "Short text answer, auto-graded",
  },
  {
    type: "numeric-range",
    label: "Numeric range",
    icon: Calculator,
    description: "Number answer with optional tolerance",
  },
  {
    type: "essay",
    label: "Essay",
    icon: PenLine,
    description: "Long-form text, manual grading",
  },
  {
    type: "matching",
    label: "Matching",
    icon: Link2,
    description: "Match items in left column to right",
  },
  {
    type: "ordering",
    label: "Ordering",
    icon: ListOrdered,
    description: "Arrange items in the correct sequence",
  },
  {
    type: "fill-blank",
    label: "Fill in the blank",
    icon: Underline,
    description: "Complete sentences with missing words",
  },
  {
    type: "matching-categories",
    label: "Matching categories",
    icon: LayoutGrid,
    description: "Group items into hidden categories",
  },
  {
    type: "ai-analysis",
    label: "AI analysis",
    icon: Sparkles,
    description: "Image submission scored by AI",
  },
]

const visibleChips = computed<QuestionTypeChip[]>(() => {
  const hide = Array.isArray(props.hideElementTypes)
    ? new Set<string>(props.hideElementTypes as string[])
    : null
  if (!hide) return DEFAULT_CHIPS
  return DEFAULT_CHIPS.filter((chip) => !hide.has(chip.type))
})

function isQuestionGroup(
  item: CustomQuestionListItem,
): item is CustomQuestionGroup {
  return (item as CustomQuestionGroup).type === "question-group"
}

function getLucideIcon(name: string | undefined): Component | null {
  if (!name) return null
  const pascal = name
    .split(/[-_\s]+/)
    .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
    .join("")
  return (
    ((LucideIcons as Record<string, unknown>)[pascal] as Component | undefined) ??
    null
  )
}

const expandedGroups = reactive<Record<string, boolean>>({})

function toggleGroup(key: string): void {
  expandedGroups[key] = !expandedGroups[key]
}

watch(
  () => props.openAllSections,
  (open) => {
    if (!open) return
    props.customElementList?.forEach((item, index) => {
      if (isQuestionGroup(item)) expandedGroups[`custom-${index}`] = true
    })
  },
  { immediate: true },
)

/* ------------------------------------------------------------------ */
/* Bank management                                                      */
/* ------------------------------------------------------------------ */

function onAddBank(): void {
  const id = builder.addBank()
  activeBankId.value = id
  sidebarMode.value = "config"
}

function onRemoveActiveBank(): void {
  if (!activeBank.value) return
  builder.removeBank(activeBank.value.id)
}

function patchActiveBank(patch: Partial<Omit<QuestionBank, "id">>): void {
  if (!activeBank.value) return
  builder.updateBank(activeBank.value.id, patch)
}

const canRemoveActiveBank = computed<boolean>(
  () => builder.schema.value.questionBanks.length > 1,
)

/* ------------------------------------------------------------------ */
/* Layers                                                               */
/* ------------------------------------------------------------------ */

const scopeQuestions = computed<QuizQuestion[]>(() =>
  builder.schema.value.questions.filter(
    (q) => q.bankId === activeBankId.value,
  ),
)

function onAddQuestion(type: QuizQuestionType): void {
  const id = builder.addQuestion(type, activeBankId.value)
  builder.select(id)
}

function onAddCustomQuestion(item: CustomQuestionItem): void {
  const id = builder.addQuestion(item.type, activeBankId.value)
  builder.updateQuestion(id, {
    prompt: item.label,
    ...(item.config ?? {}),
  })
  builder.select(id)
}

function onQuestionClick(questionId: string): void {
  builder.select(questionId)
}

const selectedQuestion = computed<QuizQuestion | null>(() => {
  const sel = builder.selection.value
  if (!sel.id) return null
  return (
    builder.schema.value.questions.find((q) => q.id === sel.id) ?? null
  )
})

/** True for choice question types — the only ones that use `options[]`. */
const selectedHasOptions = computed<boolean>(() => {
  const q = selectedQuestion.value
  if (!q) return false
  return q.type === "single-choice" || q.type === "multiple-choice"
})

/* ------------------------------------------------------------------ */
/* Inspector tabs                                                      */
/* ------------------------------------------------------------------ */

type InspectorTab = "general" | "options" | "grading"
const inspectorTab = ref<InspectorTab>("general")

/** Patch the nested `feedback` object on the selected question. */
function patchFeedback(patch: Partial<NonNullable<QuizQuestion["feedback"]>>): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    feedback: { ...(q.feedback ?? {}), ...patch },
  })
}

/* ------------------------------------------------------------------ */
/* Type-specific list editors (matching pairs, ordering, blanks, …)    */
/* ------------------------------------------------------------------ */

function makeId(prefix: string): string {
  return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`
}

/* text-match: correctAnswers[] */
function setCorrectAnswerAt(index: number, value: string): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = [...(q.correctAnswers ?? [])]
  next[index] = value
  patchSelectedQuestion({ correctAnswers: next })
}
function addCorrectAnswer(): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({ correctAnswers: [...(q.correctAnswers ?? []), ""] })
}
function removeCorrectAnswer(index: number): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    correctAnswers: (q.correctAnswers ?? []).filter((_, i) => i !== index),
  })
}

/* matching: matchPairs[] */
function setMatchPairField(
  index: number,
  field: keyof MatchPair,
  value: string,
): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = [...(q.matchPairs ?? [])]
  next[index] = { ...next[index]!, [field]: value }
  patchSelectedQuestion({ matchPairs: next })
}
function addMatchPair(): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    matchPairs: [...(q.matchPairs ?? []), { left: "", right: "" }],
  })
}
function removeMatchPair(index: number): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    matchPairs: (q.matchPairs ?? []).filter((_, i) => i !== index),
  })
}

/* ordering: correctOrder[] */
function setOrderItemAt(index: number, value: string): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = [...(q.correctOrder ?? [])]
  next[index] = value
  patchSelectedQuestion({ correctOrder: next })
}
function addOrderItem(): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({ correctOrder: [...(q.correctOrder ?? []), ""] })
}
function removeOrderItem(index: number): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    correctOrder: (q.correctOrder ?? []).filter((_, i) => i !== index),
  })
}

/* fill-blank: blanks[] each with answers[] */
function setBlankAnswerAt(
  blankIdx: number,
  answerIdx: number,
  value: string,
): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = (q.blanks ?? []).map((b, i) => {
    if (i !== blankIdx) return b
    const answers = [...b.answers]
    answers[answerIdx] = value
    return { ...b, answers }
  })
  patchSelectedQuestion({ blanks: next })
}
function addBlankAnswer(blankIdx: number): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = (q.blanks ?? []).map((b, i) =>
    i === blankIdx ? { ...b, answers: [...b.answers, ""] } : b,
  )
  patchSelectedQuestion({ blanks: next })
}
function removeBlankAnswer(blankIdx: number, answerIdx: number): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = (q.blanks ?? []).map((b, i) =>
    i === blankIdx
      ? { ...b, answers: b.answers.filter((_, j) => j !== answerIdx) }
      : b,
  )
  patchSelectedQuestion({ blanks: next })
}
function addBlank(): void {
  const q = selectedQuestion.value
  if (!q) return
  const newBlank: FillBlank = { id: makeId("blank"), answers: [""] }
  patchSelectedQuestion({ blanks: [...(q.blanks ?? []), newBlank] })
}
function removeBlank(index: number): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    blanks: (q.blanks ?? []).filter((_, i) => i !== index),
  })
}

/* matching-categories: categories[] each with items[] */
function setCategoryField(
  index: number,
  field: "label",
  value: string,
): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = (q.categories ?? []).map((c, i) =>
    i === index ? { ...c, [field]: value } : c,
  )
  patchSelectedQuestion({ categories: next })
}
function setCategoryItemAt(
  catIdx: number,
  itemIdx: number,
  value: string,
): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = (q.categories ?? []).map((c, i) => {
    if (i !== catIdx) return c
    const items = [...c.items]
    items[itemIdx] = value
    return { ...c, items }
  })
  patchSelectedQuestion({ categories: next })
}
function addCategory(): void {
  const q = selectedQuestion.value
  if (!q) return
  const count = q.itemsPerCategory ?? 4
  const newCat: QuizCategory = {
    slug: makeId("cat"),
    label: "",
    items: Array.from({ length: count }, () => ""),
  }
  patchSelectedQuestion({ categories: [...(q.categories ?? []), newCat] })
}
function removeCategory(index: number): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    categories: (q.categories ?? []).filter((_, i) => i !== index),
  })
}

/* ai-analysis: aiCriteria[] */
function setCriterionField(
  index: number,
  field: keyof Omit<AiCriterion, "key">,
  value: string | number,
): void {
  const q = selectedQuestion.value
  if (!q) return
  const next = (q.aiCriteria ?? []).map((c, i) =>
    i === index ? { ...c, [field]: value } : c,
  )
  patchSelectedQuestion({ aiCriteria: next })
}
function addCriterion(): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    aiCriteria: [
      ...(q.aiCriteria ?? []),
      { key: makeId("crit"), name: "", description: "", points: 100 },
    ],
  })
}
function removeCriterion(index: number): void {
  const q = selectedQuestion.value
  if (!q) return
  patchSelectedQuestion({
    aiCriteria: (q.aiCriteria ?? []).filter((_, i) => i !== index),
  })
}

function parseNumber(value: string): number {
  const n = Number.parseFloat(value)
  return Number.isFinite(n) ? n : 0
}

function onRemoveQuestion(): void {
  const sel = builder.selection.value
  if (sel.id) builder.removeQuestion(sel.id)
}

function onClearSelection(): void {
  builder.select(null)
}

function patchSelectedQuestion(patch: Partial<Omit<QuizQuestion, "id">>): void {
  const sel = builder.selection.value
  if (sel.id) builder.updateQuestion(sel.id, patch)
}

function onAddOption(): void {
  const sel = builder.selection.value
  if (sel.id) builder.addOption(sel.id)
}

function onRemoveOption(optionId: string): void {
  const sel = builder.selection.value
  if (sel.id) builder.removeOption(sel.id, optionId)
}

function patchOption(
  optionId: string,
  patch: Partial<Omit<QuizOption, "id">>,
): void {
  const sel = builder.selection.value
  if (sel.id) builder.updateOption(sel.id, optionId, patch)
}

function parseScore(value: string): number {
  const n = Number.parseFloat(value)
  return Number.isFinite(n) ? n : 0
}

const outerGridColumns = computed<string>(() => {
  const parts: string[] = ["2.75rem"] // nav rail
  if (sidebarMode.value !== null) parts.push("240px") // palette or config
  parts.push("1fr") // layers
  parts.push(selectedQuestion.value ? "280px" : "2rem") // inspector
  return parts.join(" ")
})

/** Whether the overall height is capped (enables internal column scroll). */
const heightCapped = computed(() => props.maxHeight !== "none")

/* ------------------------------------------------------------------ */
/* Drag ghosts                                                          */
/* ------------------------------------------------------------------ */

function makeDragGhost(label: string): {
  ghost: HTMLElement
  offsetX: number
  offsetY: number
} {
  const ghost = document.createElement("div")
  ghost.style.cssText =
    "position:fixed;pointer-events:none;z-index:9999;" +
    "display:flex;align-items:center;justify-content:space-between;gap:8px;" +
    "min-width:160px;padding:4px 8px;" +
    "border-radius:4px;" +
    "font-size:0.75rem;line-height:1rem;font-family:inherit;" +
    "color:var(--foreground,inherit);" +
    "background:var(--accent,#f1f5f9);" +
    "box-shadow:0 4px 16px rgba(0,0,0,.15);" +
    "opacity:0.95;"

  const labelSpan = document.createElement("span")
  labelSpan.textContent = label
  labelSpan.style.cssText =
    "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"

  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  svg.setAttribute("viewBox", "0 0 24 24")
  svg.setAttribute("width", "12")
  svg.setAttribute("height", "12")
  svg.setAttribute("fill", "none")
  svg.setAttribute("stroke", "currentColor")
  svg.setAttribute("stroke-width", "2")
  svg.setAttribute("stroke-linecap", "round")
  svg.setAttribute("stroke-linejoin", "round")
  svg.style.cssText = "flex-shrink:0;opacity:0.5;"
  for (const [cx, cy] of [
    [9, 5],
    [9, 12],
    [9, 19],
    [15, 5],
    [15, 12],
    [15, 19],
  ]) {
    const c = document.createElementNS("http://www.w3.org/2000/svg", "circle")
    c.setAttribute("cx", String(cx))
    c.setAttribute("cy", String(cy))
    c.setAttribute("r", "1")
    svg.appendChild(c)
  }

  ghost.appendChild(labelSpan)
  ghost.appendChild(svg)
  ghost.style.visibility = "hidden"
  ghost.style.left = "0px"
  ghost.style.top = "0px"
  document.body.appendChild(ghost)

  const GRIP_FROM_RIGHT = 8 + 6
  const offsetX = ghost.offsetWidth - GRIP_FROM_RIGHT
  const offsetY = ghost.offsetHeight / 2
  ghost.style.visibility = ""
  return { ghost, offsetX, offsetY }
}

/* ------------------------------------------------------------------ */
/* Drag-to-add (palette → layers)                                       */
/* ------------------------------------------------------------------ */

interface AddDragItem {
  type: QuizQuestionType
  label: string
  config?: Partial<Omit<QuizQuestion, "id" | "type" | "bankId">>
}

interface AddDragState {
  item: AddDragItem
  ghost: HTMLElement
  pointerId: number
  captureTarget: HTMLElement
  offsetX: number
  offsetY: number
}

const activeAddDrag = ref<AddDragState | null>(null)
const addDropTargetIndex = ref<number | null>(null)

function beginAddDrag(item: AddDragItem, event: PointerEvent): void {
  if (event.button !== 0) return
  if (props.disabled || props.readonly) return
  event.preventDefault()
  event.stopPropagation()

  const { ghost, offsetX, offsetY } = makeDragGhost(item.label)
  ghost.style.left = `${event.clientX - offsetX}px`
  ghost.style.top = `${event.clientY - offsetY}px`
  document.body.style.cursor = "grabbing"

  const target = event.target as HTMLElement
  try {
    target.setPointerCapture(event.pointerId)
  } catch {
    /* jsdom */
  }

  activeAddDrag.value = {
    item,
    ghost,
    pointerId: event.pointerId,
    captureTarget: target,
    offsetX,
    offsetY,
  }
  addDropTargetIndex.value = null

  window.addEventListener("pointermove", onAddPointerMove)
  window.addEventListener("pointerup", onAddPointerUp)
  window.addEventListener("pointercancel", onAddPointerUp)
}

function onAddPointerMove(event: PointerEvent): void {
  const state = activeAddDrag.value
  if (!state) return
  state.ghost.style.left = `${event.clientX - state.offsetX}px`
  state.ghost.style.top = `${event.clientY - state.offsetY}px`

  const el = document.elementFromPoint(event.clientX, event.clientY)
  if (!el) {
    addDropTargetIndex.value = null
    return
  }
  const surfaceEl = (el as HTMLElement).closest(
    "[data-drag-surface='quiz-layers']",
  )
  if (!surfaceEl) {
    addDropTargetIndex.value = null
    return
  }
  const blockEl = (el as HTMLElement).closest("[data-question-block-index]")
  if (blockEl) {
    const idx = Number.parseInt(
      (blockEl as HTMLElement).dataset.questionBlockIndex ?? "-1",
      10,
    )
    if (idx >= 0) {
      addDropTargetIndex.value = idx
      return
    }
  }
  addDropTargetIndex.value = scopeQuestions.value.length
}

function onAddPointerUp(_event: PointerEvent): void {
  const state = activeAddDrag.value
  if (!state) {
    cleanupAddDrag()
    return
  }
  try {
    state.captureTarget.releasePointerCapture(state.pointerId)
  } catch {
    /* ignore */
  }
  if (addDropTargetIndex.value !== null) {
    const targetIdx = addDropTargetIndex.value
    const newId = builder.addQuestion(state.item.type, activeBankId.value)
    if (state.item.config || state.item.label) {
      const patch: Partial<Omit<QuizQuestion, "id">> = {
        ...(state.item.config ?? {}),
      }
      if (state.item.label && !patch.prompt) patch.prompt = state.item.label
      builder.updateQuestion(newId, patch)
    }
    const appendedIdx = scopeQuestions.value.length - 1
    if (targetIdx < appendedIdx) {
      builder.reorderQuestionInBank(activeBankId.value, newId, targetIdx)
    }
    builder.select(newId)
  }
  cleanupAddDrag()
}

function cleanupAddDrag(): void {
  activeAddDrag.value?.ghost.remove()
  document.body.style.cursor = ""
  activeAddDrag.value = null
  addDropTargetIndex.value = null
  window.removeEventListener("pointermove", onAddPointerMove)
  window.removeEventListener("pointerup", onAddPointerUp)
  window.removeEventListener("pointercancel", onAddPointerUp)
}

/* ------------------------------------------------------------------ */
/* Drag-reorder (within layers)                                         */
/* ------------------------------------------------------------------ */

interface ReorderDragState {
  questionId: string
  fromIndex: number
  pointerId: number
  captureTarget: HTMLElement
  ghost: HTMLElement
  offsetX: number
  offsetY: number
}

const activeReorderDrag = ref<ReorderDragState | null>(null)
const reorderDropIndex = ref<number | null>(null)

function beginReorderDrag(
  questionId: string,
  fromIndex: number,
  event: PointerEvent,
): void {
  if (event.button !== 0) return
  if (props.disabled || props.readonly) return
  event.preventDefault()
  event.stopPropagation()

  const q = builder.schema.value.questions.find((qq) => qq.id === questionId)
  const label = q?.prompt || "Question"
  const { ghost, offsetX, offsetY } = makeDragGhost(label)
  ghost.style.left = `${event.clientX - offsetX}px`
  ghost.style.top = `${event.clientY - offsetY}px`
  document.body.style.cursor = "grabbing"

  const target = event.target as HTMLElement
  try {
    target.setPointerCapture(event.pointerId)
  } catch {
    /* jsdom */
  }

  activeReorderDrag.value = {
    questionId,
    fromIndex,
    pointerId: event.pointerId,
    captureTarget: target,
    ghost,
    offsetX,
    offsetY,
  }
  reorderDropIndex.value = fromIndex

  window.addEventListener("pointermove", onReorderPointerMove)
  window.addEventListener("pointerup", onReorderPointerUp)
  window.addEventListener("pointercancel", onReorderPointerUp)
}

function onReorderPointerMove(event: PointerEvent): void {
  const state = activeReorderDrag.value
  if (!state) return
  state.ghost.style.left = `${event.clientX - state.offsetX}px`
  state.ghost.style.top = `${event.clientY - state.offsetY}px`

  const el = document.elementFromPoint(event.clientX, event.clientY)
  if (!el) return
  const surfaceEl = (el as HTMLElement).closest(
    "[data-drag-surface='quiz-layers']",
  )
  if (!surfaceEl) return

  const blockEl = (el as HTMLElement).closest("[data-question-block-index]")
  if (blockEl) {
    const idx = Number.parseInt(
      (blockEl as HTMLElement).dataset.questionBlockIndex ?? "-1",
      10,
    )
    if (idx >= 0) {
      reorderDropIndex.value = idx
      return
    }
  }
  reorderDropIndex.value = scopeQuestions.value.length - 1
}

function onReorderPointerUp(_event: PointerEvent): void {
  const state = activeReorderDrag.value
  if (!state) {
    cleanupReorderDrag()
    return
  }
  try {
    state.captureTarget.releasePointerCapture(state.pointerId)
  } catch {
    /* ignore */
  }
  if (
    reorderDropIndex.value !== null &&
    reorderDropIndex.value !== state.fromIndex
  ) {
    builder.reorderQuestionInBank(
      activeBankId.value,
      state.questionId,
      reorderDropIndex.value,
    )
  }
  cleanupReorderDrag()
}

function cleanupReorderDrag(): void {
  activeReorderDrag.value?.ghost.remove()
  document.body.style.cursor = ""
  activeReorderDrag.value = null
  reorderDropIndex.value = null
  window.removeEventListener("pointermove", onReorderPointerMove)
  window.removeEventListener("pointerup", onReorderPointerUp)
  window.removeEventListener("pointercancel", onReorderPointerUp)
}

onBeforeUnmount(() => {
  cleanupAddDrag()
  cleanupReorderDrag()
})
</script>

<template>
  <div
    :class="
      cn(
        'gxp-quiz-builder grid gap-3 rounded-lg border bg-card',
        heightCapped ? 'overflow-hidden' : '',
        props.classList,
      )
    "
    :style="{
      gridTemplateColumns: outerGridColumns,
      gridTemplateRows: heightCapped ? 'minmax(0, 1fr)' : undefined,
      minHeight: '24rem',
      maxHeight: heightCapped ? props.maxHeight : undefined,
      transition: 'grid-template-columns 200ms ease',
    }"
    data-testid="quiz-builder"
    :data-element-name="props.name ?? undefined"
  >
    <!-- ─── NAV RAIL (icon-only) ──────────────────────────────────────── -->
    <nav
      class="flex flex-col items-center gap-1 border-r p-1.5"
      data-testid="quiz-builder-nav"
    >
      <button
        type="button"
        class="flex h-7 w-7 items-center justify-center rounded-md transition-colors"
        :class="
          sidebarMode === 'palette'
            ? 'bg-accent text-accent-foreground'
            : 'text-muted-foreground hover:bg-accent/40 hover:text-foreground'
        "
        :aria-pressed="sidebarMode === 'palette'"
        title="Question types"
        data-testid="quiz-builder-nav-palette"
        @click="setSidebarMode('palette')"
      >
        <CirclePlus class="h-4 w-4" />
      </button>

      <button
        v-if="props.enableQuestionBanks"
        type="button"
        class="flex h-7 w-7 items-center justify-center rounded-md transition-colors"
        :class="
          sidebarMode === 'config'
            ? 'bg-accent text-accent-foreground'
            : 'text-muted-foreground hover:bg-accent/40 hover:text-foreground'
        "
        :aria-pressed="sidebarMode === 'config'"
        :title="
          activeBank ? `Configure “${activeBank.name}”` : 'Bank configuration'
        "
        data-testid="quiz-builder-nav-config"
        @click="setSidebarMode('config')"
      >
        <Settings class="h-4 w-4" />
      </button>

      <div
        v-if="props.enableQuestionBanks"
        class="my-1 h-px w-5 bg-border/70"
        aria-hidden="true"
      />

      <template v-if="props.enableQuestionBanks">
        <button
          v-for="bank in builder.schema.value.questionBanks"
          :key="bank.id"
          type="button"
          class="flex h-7 w-7 items-center justify-center rounded-md transition-colors"
          :class="
            activeBankId === bank.id
              ? 'bg-accent text-accent-foreground'
              : 'text-muted-foreground hover:bg-accent/40 hover:text-foreground'
          "
          :aria-pressed="activeBankId === bank.id"
          :title="bank.name"
          :data-testid="`quiz-builder-nav-bank-${bank.id}`"
          @click="selectBank(bank.id)"
        >
          <Database class="h-4 w-4" />
        </button>

        <button
          type="button"
          class="mt-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent/40 hover:text-foreground"
          :disabled="props.disabled || props.readonly"
          title="Add bank"
          data-testid="quiz-builder-bank-add"
          @click="onAddBank"
        >
          <LayersPlus class="h-4 w-4" />
        </button>
      </template>
    </nav>

    <!-- ─── SIDEBAR (palette OR bank config) ──────────────────────────── -->
    <aside
      v-if="sidebarMode === 'palette'"
      class="flex min-h-0 flex-col gap-2 overflow-y-auto border-r"
      data-testid="quiz-builder-palette"
    >
      <header
        class="sticky top-0 z-10 flex items-center gap-1.5 border-b bg-card px-3 py-2"
      >
        <h4
          class="flex-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"
        >
          {{ props.label ?? "Question types" }}
        </h4>
        <Button
          variant="ghost"
          size="icon-sm"
          class="-mr-1 shrink-0"
          aria-label="Close palette"
          data-testid="quiz-builder-palette-close"
          @click="closeSidebar"
        >
          <X class="h-3.5 w-3.5" />
        </Button>
      </header>

      <div class="flex flex-col gap-2 px-3 pb-3">
        <!-- Custom palette -->
        <div
          v-if="props.customElementList"
          class="flex flex-col gap-0.5"
          data-testid="quiz-builder-palette-custom"
        >
          <template
            v-for="(item, index) in props.customElementList"
            :key="index"
          >
            <div v-if="isQuestionGroup(item)">
              <button
                type="button"
                class="flex w-full items-center justify-between gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors hover:bg-accent/30"
                :data-testid="`quiz-builder-palette-group-${index}`"
                @click="toggleGroup(`custom-${index}`)"
              >
                <span class="font-medium">{{ item.label }}</span>
                <ChevronRight
                  class="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-150"
                  :class="expandedGroups[`custom-${index}`] ? 'rotate-90' : ''"
                />
              </button>
              <div
                v-if="expandedGroups[`custom-${index}`]"
                class="ml-4 flex flex-col gap-0.5 pb-1"
              >
                <div
                  v-if="props.dragToAdd"
                  v-for="(q, qIndex) in item.questions"
                  :key="`${q.type}-${q.label}-${qIndex}`"
                  class="flex cursor-grab items-center justify-between rounded px-2 py-1 text-xs select-none transition-colors hover:bg-accent/50 active:bg-accent/70 active:cursor-grabbing"
                  :data-testid="`quiz-builder-palette-custom-${q.type}-${qIndex}`"
                  @pointerdown="(e: PointerEvent) => beginAddDrag({ type: q.type, label: q.label, config: q.config }, e)"
                >
                  <span class="flex items-center gap-1.5">
                    <component
                      :is="getLucideIcon(q.icon)"
                      v-if="getLucideIcon(q.icon)"
                      class="h-3 w-3 shrink-0 text-muted-foreground"
                    />
                    {{ q.label }}
                  </span>
                  <GripVertical class="h-3 w-3 shrink-0 text-muted-foreground" />
                </div>
                <button
                  v-else
                  v-for="(q, qIndex) in item.questions"
                  :key="`${q.type}-${q.label}-${qIndex}`"
                  type="button"
                  class="flex items-center justify-between gap-1.5 rounded px-2 py-1 text-left text-xs transition-colors hover:bg-accent/50 active:bg-accent/70 disabled:opacity-50"
                  :data-testid="`quiz-builder-palette-custom-${q.type}-${qIndex}`"
                  :disabled="props.disabled || props.readonly"
                  @click="onAddCustomQuestion(q)"
                >
                  <span class="flex min-w-0 items-center gap-1.5">
                    <component
                      :is="getLucideIcon(q.icon)"
                      v-if="getLucideIcon(q.icon)"
                      class="h-3 w-3 shrink-0 text-muted-foreground"
                    />
                    {{ q.label }}
                  </span>
                  <Plus class="h-3 w-3 shrink-0 text-muted-foreground" />
                </button>
              </div>
            </div>

            <div
              v-else-if="props.dragToAdd"
              class="flex cursor-grab items-center justify-between rounded px-2 py-1 text-xs select-none transition-colors hover:bg-accent/50 active:bg-accent/70 active:cursor-grabbing"
              :data-testid="`quiz-builder-palette-custom-${item.type}-${index}`"
              @pointerdown="(e: PointerEvent) => beginAddDrag({ type: item.type, label: item.label, config: item.config }, e)"
            >
              <span class="flex items-center gap-1.5">
                <component
                  :is="getLucideIcon(item.icon)"
                  v-if="getLucideIcon(item.icon)"
                  class="h-3 w-3 shrink-0 text-muted-foreground"
                />
                {{ item.label }}
              </span>
              <GripVertical class="h-3 w-3 shrink-0 text-muted-foreground" />
            </div>

            <button
              v-else
              type="button"
              class="flex items-center justify-between gap-1.5 rounded px-2 py-1 text-left text-xs transition-colors hover:bg-accent/50 active:bg-accent/70 disabled:opacity-50"
              :data-testid="`quiz-builder-palette-custom-${item.type}-${index}`"
              :disabled="props.disabled || props.readonly"
              @click="onAddCustomQuestion(item)"
            >
              <span class="flex min-w-0 items-center gap-1.5">
                <component
                  :is="getLucideIcon(item.icon)"
                  v-if="getLucideIcon(item.icon)"
                  class="h-3 w-3 shrink-0 text-muted-foreground"
                />
                {{ item.label }}
              </span>
              <Plus class="h-3 w-3 shrink-0 text-muted-foreground" />
            </button>
          </template>
        </div>

        <!-- Default palette -->
        <template v-else>
          <div class="flex flex-col gap-1">
            <div
              v-if="props.dragToAdd"
              v-for="chip in visibleChips"
              :key="chip.type"
              class="flex cursor-grab items-center justify-between gap-1.5 rounded px-2 py-1 text-xs select-none transition-colors hover:bg-accent/50 active:bg-accent/70 active:cursor-grabbing"
              :data-testid="`quiz-builder-palette-${chip.type}`"
              @pointerdown="(e: PointerEvent) => beginAddDrag({ type: chip.type, label: chip.label }, e)"
            >
              <span class="flex min-w-0 items-center gap-1.5">
                <component
                  :is="chip.icon"
                  class="h-3 w-3 shrink-0 text-muted-foreground"
                />
                {{ chip.label }}
              </span>
              <GripVertical class="h-3 w-3 shrink-0 text-muted-foreground" />
            </div>
            <button
              v-else
              v-for="chip in visibleChips"
              :key="chip.type"
              type="button"
              class="flex items-center justify-between gap-1.5 rounded px-2 py-1 text-left text-xs transition-colors hover:bg-accent/50 active:bg-accent/70 disabled:opacity-50"
              :data-testid="`quiz-builder-palette-${chip.type}`"
              :disabled="props.disabled || props.readonly"
              @click="onAddQuestion(chip.type)"
            >
              <span class="flex min-w-0 items-center gap-1.5">
                <component
                  :is="chip.icon"
                  class="h-3 w-3 shrink-0 text-muted-foreground"
                />
                {{ chip.label }}
              </span>
              <Plus class="h-3 w-3 shrink-0 text-muted-foreground" />
            </button>
          </div>
        </template>

        <p v-if="props.helpText" class="text-xs text-muted-foreground">
          {{ props.helpText }}
        </p>
        <ul
          v-if="props.errors.length"
          class="flex flex-col gap-0.5 text-xs text-destructive"
          role="alert"
        >
          <li v-for="(err, i) in props.errors" :key="i">{{ err }}</li>
        </ul>
      </div>
    </aside>

    <aside
      v-else-if="sidebarMode === 'config' && activeBank"
      class="flex min-h-0 flex-col gap-2 overflow-y-auto border-r"
      data-testid="quiz-builder-config"
    >
      <header
        class="sticky top-0 z-10 flex items-center gap-1.5 border-b bg-card px-3 py-2"
      >
        <h4
          class="flex-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"
        >
          Bank settings
        </h4>
        <Button
          variant="ghost"
          size="icon-sm"
          class="-mr-1 shrink-0"
          aria-label="Close bank settings"
          data-testid="quiz-builder-config-close"
          @click="closeSidebar"
        >
          <X class="h-3.5 w-3.5" />
        </Button>
      </header>

      <div class="flex flex-col gap-3 px-3 pb-3">
        <div class="flex flex-col gap-1">
          <Label for="qb-bank-name">Name</Label>
          <Input
            id="qb-bank-name"
            :model-value="activeBank.name"
            data-testid="quiz-builder-config-name"
            @input="
              (e: Event) =>
                patchActiveBank({
                  name: (e.target as HTMLInputElement).value,
                })
            "
          />
        </div>
        <div class="flex flex-col gap-1">
          <Label for="qb-bank-description">Description</Label>
          <Textarea
            id="qb-bank-description"
            :model-value="activeBank.description ?? ''"
            data-testid="quiz-builder-config-description"
            @input="
              (e: Event) =>
                patchActiveBank({
                  description: (e.target as HTMLTextAreaElement).value,
                })
            "
          />
        </div>
        <Button
          v-if="canRemoveActiveBank"
          variant="destructive"
          size="sm"
          class="self-start"
          data-testid="quiz-builder-config-remove"
          @click="onRemoveActiveBank"
        >
          <Trash2 class="h-3.5 w-3.5" />
          Delete bank
        </Button>
        <p
          v-else
          class="text-[11px] text-muted-foreground"
          data-testid="quiz-builder-config-cannot-remove"
        >
          At least one bank must remain.
        </p>
      </div>
    </aside>

    <!-- ─── LAYERS ────────────────────────────────────────────────────── -->
    <section
      class="flex min-h-0 flex-col gap-3 overflow-y-auto p-3"
      data-testid="quiz-builder-canvas"
      data-drag-surface="quiz-layers"
    >
      <header
        v-if="props.enableQuestionBanks && activeBank"
        class="flex items-center justify-between gap-2"
      >
        <div class="flex min-w-0 flex-1 items-center gap-2">
          <Database class="h-4 w-4 shrink-0 text-muted-foreground" />
          <h4
            class="truncate text-sm font-semibold"
            :data-testid="`quiz-builder-bank-${activeBank.id}-name`"
          >
            {{ activeBank.name }}
          </h4>
          <Badge variant="outline" class="font-mono text-[10px]">
            {{ scopeQuestions.length }} question{{
              scopeQuestions.length === 1 ? "" : "s"
            }}
          </Badge>
        </div>
      </header>

      <p
        v-if="scopeQuestions.length === 0"
        class="rounded border border-dashed bg-muted/20 py-6 text-center text-xs text-muted-foreground"
        :class="
          activeAddDrag ? 'border-primary/60 bg-primary/5 text-primary' : ''
        "
        data-testid="quiz-builder-canvas-empty"
      >
        {{
          props.dragToAdd
            ? "Drag a question type from the palette."
            : "Open the palette and pick a question type."
        }}
      </p>

      <ul
        v-else
        class="flex flex-col gap-1 text-xs"
        data-testid="quiz-builder-canvas-list"
      >
        <template v-for="(q, index) in scopeQuestions" :key="q.id">
          <div
            v-if="
              (activeAddDrag && addDropTargetIndex === index) ||
              (activeReorderDrag &&
                reorderDropIndex === index &&
                activeReorderDrag.questionId !== q.id)
            "
            class="pointer-events-none relative"
            style="height: 0; z-index: 5"
            data-testid="quiz-builder-drop-indicator"
          >
            <div class="absolute inset-x-0 -top-px flex items-center">
              <span class="block h-2 w-2 shrink-0 rounded-full bg-primary" />
              <span class="block h-px flex-1 bg-primary/80" />
            </div>
          </div>
          <li
            :data-question-block-index="index"
            class="group/row flex cursor-pointer items-center gap-2 rounded border bg-background px-1.5 py-1.5 transition-colors"
            :class="[
              builder.selection.value.id === q.id
                ? 'border-primary bg-accent/30'
                : 'hover:border-primary/40',
              activeReorderDrag?.questionId === q.id ? 'opacity-40' : '',
            ]"
            :data-testid="`quiz-builder-row-${q.id}`"
            @click="onQuestionClick(q.id)"
          >
            <span
              class="shrink-0 cursor-grab touch-none rounded-sm bg-muted/60 px-0.5 py-0.5 opacity-60 transition-opacity hover:opacity-100 active:cursor-grabbing"
              title="Drag to reorder"
              :data-testid="`quiz-builder-row-${q.id}-handle`"
              @click.stop
              @pointerdown.stop="
                (e: PointerEvent) => beginReorderDrag(q.id, index, e)
              "
            >
              <GripVertical class="h-3 w-3 text-muted-foreground" />
            </span>
            <span class="w-5 text-right tabular-nums text-muted-foreground">
              {{ index + 1 }}.
            </span>
            <span class="flex min-w-0 flex-col">
              <span class="truncate font-medium">{{ q.prompt }}</span>
              <span class="text-[10px] text-muted-foreground">
                {{ q.options.length }} option(s)
              </span>
            </span>
            <Badge variant="secondary" class="ml-auto font-mono text-[9px]">
              {{ q.type }}
            </Badge>
          </li>
        </template>
        <div
          v-if="
            (activeAddDrag && addDropTargetIndex === scopeQuestions.length) ||
            (activeReorderDrag &&
              reorderDropIndex === scopeQuestions.length - 1)
          "
          class="pointer-events-none relative"
          style="height: 0; z-index: 5"
          data-testid="quiz-builder-drop-indicator-end"
        >
          <div class="absolute inset-x-0 -top-px flex items-center">
            <span class="block h-2 w-2 shrink-0 rounded-full bg-primary" />
            <span class="block h-px flex-1 bg-primary/80" />
          </div>
        </div>
      </ul>
    </section>

    <!-- ─── INSPECTOR (collapsed) ─────────────────────────────────────── -->
    <aside
      v-if="!selectedQuestion"
      class="flex min-h-0 items-start justify-center overflow-hidden border-l pt-3"
      data-testid="quiz-builder-inspector"
    >
      <TooltipProvider>
        <Tooltip>
          <TooltipTrigger as-child>
            <span
              class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-muted-foreground"
              tabindex="0"
              aria-label="Select a question on the canvas to edit its properties"
              data-testid="quiz-builder-inspector-empty"
            >
              <Info class="h-4 w-4" />
            </span>
          </TooltipTrigger>
          <TooltipContent side="left">
            Select a question on the canvas to edit its properties
          </TooltipContent>
        </Tooltip>
      </TooltipProvider>
    </aside>

    <!-- ─── INSPECTOR (expanded) ──────────────────────────────────────── -->
    <aside
      v-else
      class="flex min-h-0 flex-col gap-2 overflow-y-auto border-l p-3"
      data-testid="quiz-builder-inspector"
    >
      <header class="flex items-center justify-between gap-2">
        <span
          class="truncate text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"
        >
          Question
        </span>
        <Button
          variant="ghost"
          size="icon-sm"
          class="-mr-1 shrink-0"
          aria-label="Clear selection"
          @click="onClearSelection"
        >
          <X class="h-3.5 w-3.5" />
        </Button>
      </header>

      <!-- Inspector tab nav -->
      <div
        class="flex items-stretch border-b"
        role="tablist"
        data-testid="quiz-builder-inspector-tabs"
      >
        <button
          type="button"
          role="tab"
          class="-mb-px flex flex-1 items-center justify-center gap-1.5 border-b-2 py-1.5 transition-colors"
          :class="
            inspectorTab === 'general'
              ? 'border-primary text-foreground'
              : 'border-transparent text-muted-foreground hover:text-foreground'
          "
          :aria-selected="inspectorTab === 'general'"
          title="General"
          data-testid="quiz-builder-inspector-tab-general"
          @click="inspectorTab = 'general'"
        >
          <Settings class="h-4 w-4" />
        </button>
        <button
          type="button"
          role="tab"
          class="-mb-px flex flex-1 items-center justify-center gap-1.5 border-b-2 py-1.5 transition-colors"
          :class="
            inspectorTab === 'options'
              ? 'border-primary text-foreground'
              : 'border-transparent text-muted-foreground hover:text-foreground'
          "
          :aria-selected="inspectorTab === 'options'"
          title="Options"
          data-testid="quiz-builder-inspector-tab-options"
          @click="inspectorTab = 'options'"
        >
          <ListChecks class="h-4 w-4" />
        </button>
        <button
          type="button"
          role="tab"
          class="-mb-px flex flex-1 items-center justify-center gap-1.5 border-b-2 py-1.5 transition-colors"
          :class="
            inspectorTab === 'grading'
              ? 'border-primary text-foreground'
              : 'border-transparent text-muted-foreground hover:text-foreground'
          "
          :aria-selected="inspectorTab === 'grading'"
          title="Grading"
          data-testid="quiz-builder-inspector-tab-grading"
          @click="inspectorTab = 'grading'"
        >
          <GraduationCap class="h-4 w-4" />
        </button>
      </div>

      <div
        class="flex flex-col gap-3"
        data-testid="quiz-builder-inspector-fields"
        :data-active-tab="inspectorTab"
        role="tabpanel"
      >
        <!-- ─── GENERAL TAB ─────────────────────────────────────── -->
        <div
          v-show="inspectorTab === 'general'"
          class="flex flex-col gap-2"
          data-testid="quiz-builder-inspector-panel-general"
        >
            <div class="flex flex-col gap-1">
              <Label for="qb-prompt">Prompt</Label>
              <Input
                id="qb-prompt"
                :model-value="selectedQuestion.prompt"
                data-testid="quiz-builder-inspector-prompt"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      prompt: (e.target as HTMLInputElement).value,
                    })
                "
              />
            </div>
            <div class="flex flex-col gap-1">
              <Label for="qb-type">Type</Label>
              <Select
                :model-value="selectedQuestion.type"
                @update:model-value="
                  (v: unknown) =>
                    patchSelectedQuestion({ type: v as QuizQuestionType })
                "
              >
                <SelectTrigger
                  id="qb-type"
                  data-testid="quiz-builder-inspector-type"
                >
                  <SelectValue placeholder="Question type" />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem
                    v-for="chip in visibleChips"
                    :key="chip.type"
                    :value="chip.type"
                  >
                    {{ chip.label }}
                  </SelectItem>
                </SelectContent>
              </Select>
            </div>
            <div class="flex flex-col gap-1">
              <Label for="qb-help">Help text</Label>
              <Textarea
                id="qb-help"
                :model-value="selectedQuestion.helpText ?? ''"
                data-testid="quiz-builder-inspector-help"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      helpText: (e.target as HTMLTextAreaElement).value,
                    })
                "
              />
            </div>
            <div
              class="flex items-center justify-between rounded border bg-background px-2 py-1"
            >
              <Label for="qb-required">Required</Label>
              <Switch
                id="qb-required"
                :model-value="Boolean(selectedQuestion.required)"
                data-testid="quiz-builder-inspector-required"
                @update:model-value="
                  (v: boolean) => patchSelectedQuestion({ required: v })
                "
              />
            </div>
        </div>

        <!-- ─── OPTIONS TAB ─────────────────────────────────────── -->
        <div
          v-show="inspectorTab === 'options'"
          class="flex flex-col gap-3"
          data-testid="quiz-builder-inspector-panel-options"
        >
        <Card v-if="selectedHasOptions" class="border-dashed">
          <CardHeader class="py-2">
            <CardTitle
              class="flex items-center justify-between font-mono text-xs uppercase tracking-wide"
            >
              <span>options</span>
              <Button
                variant="outline"
                size="sm"
                data-testid="quiz-builder-inspector-add-option"
                @click="onAddOption"
              >
                <Plus class="h-3.5 w-3.5" />
                Add option
              </Button>
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div
              v-for="opt in selectedQuestion.options"
              :key="opt.id"
              class="flex flex-col gap-1 rounded border bg-background p-2"
              :data-testid="`quiz-builder-inspector-option-${opt.id}`"
            >
              <div class="grid grid-cols-2 gap-1">
                <div class="flex flex-col gap-1">
                  <Label :for="`qb-opt-label-${opt.id}`">Label</Label>
                  <Input
                    :id="`qb-opt-label-${opt.id}`"
                    :model-value="opt.label"
                    :data-testid="`quiz-builder-inspector-option-${opt.id}-label`"
                    @input="
                      (e: Event) =>
                        patchOption(opt.id, {
                          label: (e.target as HTMLInputElement).value,
                        })
                    "
                  />
                </div>
                <div class="flex flex-col gap-1">
                  <Label :for="`qb-opt-value-${opt.id}`">Value</Label>
                  <Input
                    :id="`qb-opt-value-${opt.id}`"
                    :model-value="String(opt.value)"
                    :data-testid="`quiz-builder-inspector-option-${opt.id}-value`"
                    @input="
                      (e: Event) =>
                        patchOption(opt.id, {
                          value: (e.target as HTMLInputElement).value,
                        })
                    "
                  />
                </div>
              </div>
              <div class="grid grid-cols-2 items-center gap-1">
                <div
                  class="flex items-center justify-between rounded border bg-background px-2 py-1"
                >
                  <Label :for="`qb-opt-correct-${opt.id}`">Correct</Label>
                  <Switch
                    :id="`qb-opt-correct-${opt.id}`"
                    :model-value="Boolean(opt.correct)"
                    :data-testid="`quiz-builder-inspector-option-${opt.id}-correct`"
                    @update:model-value="
                      (v: boolean) => patchOption(opt.id, { correct: v })
                    "
                  />
                </div>
                <div class="flex flex-col gap-1">
                  <Label :for="`qb-opt-score-${opt.id}`">Score</Label>
                  <Input
                    :id="`qb-opt-score-${opt.id}`"
                    type="number"
                    :model-value="opt.score ?? 0"
                    :data-testid="`quiz-builder-inspector-option-${opt.id}-score`"
                    @input="
                      (e: Event) =>
                        patchOption(opt.id, {
                          score: parseScore(
                            (e.target as HTMLInputElement).value,
                          ),
                        })
                    "
                  />
                </div>
              </div>
              <Button
                variant="ghost"
                size="sm"
                class="self-end text-destructive"
                :data-testid="`quiz-builder-inspector-option-${opt.id}-remove`"
                @click="onRemoveOption(opt.id)"
              >
                <Trash2 class="h-3.5 w-3.5" />
                Remove option
              </Button>
            </div>
            <p
              v-if="selectedQuestion.options.length === 0"
              class="py-2 text-center text-xs text-muted-foreground"
            >
              No options yet — click "Add option".
            </p>
          </CardContent>
        </Card>

        <!-- multiple-choice: partial credit toggle -->
        <div
          v-if="selectedQuestion.type === 'multiple-choice'"
          class="flex items-center justify-between rounded border bg-background px-2 py-1"
          data-testid="quiz-builder-inspector-allow-partial-credit"
        >
          <Label for="qb-partial">Allow partial credit</Label>
          <Switch
            id="qb-partial"
            :model-value="Boolean(selectedQuestion.allowPartialCredit)"
            @update:model-value="
              (v: boolean) =>
                patchSelectedQuestion({ allowPartialCredit: v })
            "
          />
        </div>

        <!-- true-false: correct answer -->
        <Card
          v-if="selectedQuestion.type === 'true-false'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-true-false"
        >
          <CardHeader class="py-2">
            <CardTitle class="font-mono text-xs uppercase tracking-wide">
              correct answer
            </CardTitle>
          </CardHeader>
          <CardContent class="flex items-center justify-between gap-2 pb-3">
            <Label for="qb-tf">
              {{ selectedQuestion.correctAnswer ? "True" : "False" }}
            </Label>
            <Switch
              id="qb-tf"
              :model-value="Boolean(selectedQuestion.correctAnswer)"
              data-testid="quiz-builder-inspector-true-false-toggle"
              @update:model-value="
                (v: boolean) =>
                  patchSelectedQuestion({ correctAnswer: v })
              "
            />
          </CardContent>
        </Card>

        <!-- text-match: accepted answers + flags -->
        <Card
          v-if="selectedQuestion.type === 'text-match'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-text-match"
        >
          <CardHeader class="py-2">
            <CardTitle
              class="flex items-center justify-between font-mono text-xs uppercase tracking-wide"
            >
              <span>accepted answers</span>
              <Button
                variant="outline"
                size="sm"
                data-testid="quiz-builder-inspector-add-correct-answer"
                @click="addCorrectAnswer"
              >
                <Plus class="h-3.5 w-3.5" />
                Add
              </Button>
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div
              v-for="(ans, i) in selectedQuestion.correctAnswers ?? []"
              :key="i"
              class="flex items-center gap-1"
            >
              <Input
                :model-value="ans"
                :data-testid="`quiz-builder-inspector-correct-answer-${i}`"
                @input="
                  (e: Event) =>
                    setCorrectAnswerAt(
                      i,
                      (e.target as HTMLInputElement).value,
                    )
                "
              />
              <Button
                variant="ghost"
                size="icon-sm"
                class="text-destructive"
                aria-label="Remove"
                @click="removeCorrectAnswer(i)"
              >
                <Trash2 class="h-3.5 w-3.5" />
              </Button>
            </div>
            <div class="grid grid-cols-2 gap-1">
              <div
                class="flex items-center justify-between rounded border bg-background px-2 py-1"
              >
                <Label for="qb-case">Case sensitive</Label>
                <Switch
                  id="qb-case"
                  :model-value="Boolean(selectedQuestion.caseSensitive)"
                  @update:model-value="
                    (v: boolean) =>
                      patchSelectedQuestion({ caseSensitive: v })
                  "
                />
              </div>
              <div
                class="flex items-center justify-between rounded border bg-background px-2 py-1"
              >
                <Label for="qb-trim">Trim whitespace</Label>
                <Switch
                  id="qb-trim"
                  :model-value="
                    selectedQuestion.trimWhitespace !== false
                  "
                  @update:model-value="
                    (v: boolean) =>
                      patchSelectedQuestion({ trimWhitespace: v })
                  "
                />
              </div>
            </div>
          </CardContent>
        </Card>

        <!-- numeric-range: correct answer + tolerance + min/max -->
        <Card
          v-if="selectedQuestion.type === 'numeric-range'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-numeric-range"
        >
          <CardHeader class="py-2">
            <CardTitle class="font-mono text-xs uppercase tracking-wide">
              correct answer
            </CardTitle>
          </CardHeader>
          <CardContent class="grid grid-cols-2 gap-2 pb-3">
            <div class="flex flex-col gap-1">
              <Label for="qb-num-correct">Correct answer</Label>
              <Input
                id="qb-num-correct"
                type="number"
                :model-value="
                  typeof selectedQuestion.correctAnswer === 'number'
                    ? selectedQuestion.correctAnswer
                    : 0
                "
                data-testid="quiz-builder-inspector-numeric-correct"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      correctAnswer: parseNumber(
                        (e.target as HTMLInputElement).value,
                      ),
                    })
                "
              />
            </div>
            <div class="flex flex-col gap-1">
              <Label for="qb-num-tol">Tolerance (±)</Label>
              <Input
                id="qb-num-tol"
                type="number"
                :model-value="selectedQuestion.tolerance ?? 0"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      tolerance: parseNumber(
                        (e.target as HTMLInputElement).value,
                      ),
                    })
                "
              />
            </div>
            <div class="flex flex-col gap-1">
              <Label for="qb-num-min">Range min</Label>
              <Input
                id="qb-num-min"
                type="number"
                :model-value="selectedQuestion.numericRangeMin ?? ''"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      numericRangeMin: parseNumber(
                        (e.target as HTMLInputElement).value,
                      ),
                    })
                "
              />
            </div>
            <div class="flex flex-col gap-1">
              <Label for="qb-num-max">Range max</Label>
              <Input
                id="qb-num-max"
                type="number"
                :model-value="selectedQuestion.numericRangeMax ?? ''"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      numericRangeMax: parseNumber(
                        (e.target as HTMLInputElement).value,
                      ),
                    })
                "
              />
            </div>
          </CardContent>
        </Card>

        <!-- essay: min/max words + rubric -->
        <Card
          v-if="selectedQuestion.type === 'essay'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-essay"
        >
          <CardHeader class="py-2">
            <CardTitle class="font-mono text-xs uppercase tracking-wide">
              essay
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div class="grid grid-cols-2 gap-2">
              <div class="flex flex-col gap-1">
                <Label for="qb-min-words">Min words</Label>
                <Input
                  id="qb-min-words"
                  type="number"
                  :model-value="selectedQuestion.minWords ?? ''"
                  @input="
                    (e: Event) =>
                      patchSelectedQuestion({
                        minWords: parseNumber(
                          (e.target as HTMLInputElement).value,
                        ),
                      })
                  "
                />
              </div>
              <div class="flex flex-col gap-1">
                <Label for="qb-max-words">Max words</Label>
                <Input
                  id="qb-max-words"
                  type="number"
                  :model-value="selectedQuestion.maxWords ?? ''"
                  @input="
                    (e: Event) =>
                      patchSelectedQuestion({
                        maxWords: parseNumber(
                          (e.target as HTMLInputElement).value,
                        ),
                      })
                  "
                />
              </div>
            </div>
            <div class="flex flex-col gap-1">
              <Label for="qb-rubric">Grading rubric</Label>
              <Textarea
                id="qb-rubric"
                :model-value="selectedQuestion.gradingRubric ?? ''"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      gradingRubric: (e.target as HTMLTextAreaElement)
                        .value,
                    })
                "
              />
            </div>
          </CardContent>
        </Card>

        <!-- matching: pairs editor -->
        <Card
          v-if="selectedQuestion.type === 'matching'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-matching"
        >
          <CardHeader class="py-2">
            <CardTitle
              class="flex items-center justify-between font-mono text-xs uppercase tracking-wide"
            >
              <span>pairs</span>
              <Button
                variant="outline"
                size="sm"
                data-testid="quiz-builder-inspector-add-pair"
                @click="addMatchPair"
              >
                <Plus class="h-3.5 w-3.5" />
                Add pair
              </Button>
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div
              v-for="(pair, i) in selectedQuestion.matchPairs ?? []"
              :key="i"
              class="grid grid-cols-[1fr_auto_1fr_auto] items-center gap-1"
              :data-testid="`quiz-builder-inspector-pair-${i}`"
            >
              <Input
                :model-value="pair.left"
                placeholder="Left"
                @input="
                  (e: Event) =>
                    setMatchPairField(
                      i,
                      'left',
                      (e.target as HTMLInputElement).value,
                    )
                "
              />
              <Link2 class="h-3 w-3 text-muted-foreground" />
              <Input
                :model-value="pair.right"
                placeholder="Right"
                @input="
                  (e: Event) =>
                    setMatchPairField(
                      i,
                      'right',
                      (e.target as HTMLInputElement).value,
                    )
                "
              />
              <Button
                variant="ghost"
                size="icon-sm"
                class="text-destructive"
                aria-label="Remove pair"
                @click="removeMatchPair(i)"
              >
                <Trash2 class="h-3.5 w-3.5" />
              </Button>
            </div>
            <div
              class="flex items-center justify-between rounded border bg-background px-2 py-1"
            >
              <Label for="qb-shuffle">Shuffle right side</Label>
              <Switch
                id="qb-shuffle"
                :model-value="selectedQuestion.shuffleMatches !== false"
                @update:model-value="
                  (v: boolean) =>
                    patchSelectedQuestion({ shuffleMatches: v })
                "
              />
            </div>
          </CardContent>
        </Card>

        <!-- ordering: ordered items list -->
        <Card
          v-if="selectedQuestion.type === 'ordering'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-ordering"
        >
          <CardHeader class="py-2">
            <CardTitle
              class="flex items-center justify-between font-mono text-xs uppercase tracking-wide"
            >
              <span>correct order</span>
              <Button
                variant="outline"
                size="sm"
                data-testid="quiz-builder-inspector-add-order-item"
                @click="addOrderItem"
              >
                <Plus class="h-3.5 w-3.5" />
                Add
              </Button>
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div
              v-for="(item, i) in selectedQuestion.correctOrder ?? []"
              :key="i"
              class="flex items-center gap-1"
            >
              <span
                class="w-5 text-right tabular-nums text-muted-foreground"
              >
                {{ i + 1 }}.
              </span>
              <Input
                :model-value="item"
                @input="
                  (e: Event) =>
                    setOrderItemAt(
                      i,
                      (e.target as HTMLInputElement).value,
                    )
                "
              />
              <Button
                variant="ghost"
                size="icon-sm"
                class="text-destructive"
                aria-label="Remove"
                @click="removeOrderItem(i)"
              >
                <Trash2 class="h-3.5 w-3.5" />
              </Button>
            </div>
            <div
              class="flex items-center justify-between rounded border bg-background px-2 py-1"
            >
              <Label for="qb-ord-partial">Allow partial credit</Label>
              <Switch
                id="qb-ord-partial"
                :model-value="Boolean(selectedQuestion.partialCreditEnabled)"
                @update:model-value="
                  (v: boolean) =>
                    patchSelectedQuestion({ partialCreditEnabled: v })
                "
              />
            </div>
          </CardContent>
        </Card>

        <!-- fill-blank: blanks editor -->
        <Card
          v-if="selectedQuestion.type === 'fill-blank'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-fill-blank"
        >
          <CardHeader class="py-2">
            <CardTitle
              class="flex items-center justify-between font-mono text-xs uppercase tracking-wide"
            >
              <span>blanks</span>
              <Button
                variant="outline"
                size="sm"
                data-testid="quiz-builder-inspector-add-blank"
                @click="addBlank"
              >
                <Plus class="h-3.5 w-3.5" />
                Add blank
              </Button>
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div
              v-for="(blank, bIdx) in selectedQuestion.blanks ?? []"
              :key="blank.id"
              class="flex flex-col gap-1 rounded border bg-background p-2"
              :data-testid="`quiz-builder-inspector-blank-${bIdx}`"
            >
              <div class="flex items-center justify-between">
                <span class="font-mono text-[10px] text-muted-foreground">
                  Blank {{ bIdx + 1 }}
                </span>
                <Button
                  variant="ghost"
                  size="icon-sm"
                  class="text-destructive"
                  aria-label="Remove blank"
                  @click="removeBlank(bIdx)"
                >
                  <Trash2 class="h-3.5 w-3.5" />
                </Button>
              </div>
              <div
                v-for="(ans, aIdx) in blank.answers"
                :key="aIdx"
                class="flex items-center gap-1"
              >
                <Input
                  :model-value="ans"
                  placeholder="Accepted answer"
                  @input="
                    (e: Event) =>
                      setBlankAnswerAt(
                        bIdx,
                        aIdx,
                        (e.target as HTMLInputElement).value,
                      )
                  "
                />
                <Button
                  variant="ghost"
                  size="icon-sm"
                  class="text-destructive"
                  aria-label="Remove answer"
                  @click="removeBlankAnswer(bIdx, aIdx)"
                >
                  <X class="h-3.5 w-3.5" />
                </Button>
              </div>
              <Button
                variant="ghost"
                size="sm"
                class="self-start text-xs text-muted-foreground"
                @click="addBlankAnswer(bIdx)"
              >
                <Plus class="h-3 w-3" />
                Add answer
              </Button>
            </div>
            <div class="grid grid-cols-2 gap-1">
              <div
                class="flex items-center justify-between rounded border bg-background px-2 py-1"
              >
                <Label for="qb-blank-case">Case sensitive</Label>
                <Switch
                  id="qb-blank-case"
                  :model-value="Boolean(selectedQuestion.caseSensitive)"
                  @update:model-value="
                    (v: boolean) =>
                      patchSelectedQuestion({ caseSensitive: v })
                  "
                />
              </div>
              <div
                class="flex items-center justify-between rounded border bg-background px-2 py-1"
              >
                <Label for="qb-blank-partial">Per-blank credit</Label>
                <Switch
                  id="qb-blank-partial"
                  :model-value="
                    Boolean(selectedQuestion.partialCreditEnabled)
                  "
                  @update:model-value="
                    (v: boolean) =>
                      patchSelectedQuestion({ partialCreditEnabled: v })
                  "
                />
              </div>
            </div>
          </CardContent>
        </Card>

        <!-- matching-categories -->
        <Card
          v-if="selectedQuestion.type === 'matching-categories'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-matching-categories"
        >
          <CardHeader class="py-2">
            <CardTitle
              class="flex items-center justify-between font-mono text-xs uppercase tracking-wide"
            >
              <span>categories</span>
              <Button
                variant="outline"
                size="sm"
                data-testid="quiz-builder-inspector-add-category"
                @click="addCategory"
              >
                <Plus class="h-3.5 w-3.5" />
                Add category
              </Button>
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div class="flex flex-col gap-1">
              <Label for="qb-items-per-cat">Items per category</Label>
              <Input
                id="qb-items-per-cat"
                type="number"
                :model-value="selectedQuestion.itemsPerCategory ?? 4"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      itemsPerCategory: parseNumber(
                        (e.target as HTMLInputElement).value,
                      ),
                    })
                "
              />
            </div>
            <div
              v-for="(cat, cIdx) in selectedQuestion.categories ?? []"
              :key="cat.slug"
              class="flex flex-col gap-1 rounded border bg-background p-2"
              :data-testid="`quiz-builder-inspector-category-${cIdx}`"
            >
              <div class="flex items-center gap-1">
                <Input
                  :model-value="cat.label"
                  placeholder="Category label"
                  @input="
                    (e: Event) =>
                      setCategoryField(
                        cIdx,
                        'label',
                        (e.target as HTMLInputElement).value,
                      )
                  "
                />
                <Button
                  variant="ghost"
                  size="icon-sm"
                  class="text-destructive"
                  aria-label="Remove category"
                  @click="removeCategory(cIdx)"
                >
                  <Trash2 class="h-3.5 w-3.5" />
                </Button>
              </div>
              <Input
                v-for="(item, iIdx) in cat.items"
                :key="iIdx"
                :model-value="item"
                placeholder="Item"
                @input="
                  (e: Event) =>
                    setCategoryItemAt(
                      cIdx,
                      iIdx,
                      (e.target as HTMLInputElement).value,
                    )
                "
              />
            </div>
          </CardContent>
        </Card>

        <!-- ai-analysis -->
        <Card
          v-if="selectedQuestion.type === 'ai-analysis'"
          class="border-dashed"
          data-testid="quiz-builder-inspector-ai-analysis"
        >
          <CardHeader class="py-2">
            <CardTitle class="font-mono text-xs uppercase tracking-wide">
              ai analysis
            </CardTitle>
          </CardHeader>
          <CardContent class="flex flex-col gap-2 pb-3">
            <div class="flex flex-col gap-1">
              <Label for="qb-ai-method">Scoring method</Label>
              <Select
                :model-value="selectedQuestion.scoringMethod ?? 'standard'"
                @update:model-value="
                  (v: unknown) =>
                    patchSelectedQuestion({
                      scoringMethod: v as AiScoringMethod,
                    })
                "
              >
                <SelectTrigger
                  id="qb-ai-method"
                  data-testid="quiz-builder-inspector-ai-method"
                >
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="standard">Standard</SelectItem>
                  <SelectItem value="item_count">Item count</SelectItem>
                  <SelectItem value="ai_judgment">AI judgment</SelectItem>
                </SelectContent>
              </Select>
            </div>
            <div class="flex flex-col gap-1">
              <Label for="qb-ai-prompt">AI prompt</Label>
              <Textarea
                id="qb-ai-prompt"
                :model-value="selectedQuestion.aiPrompt ?? ''"
                data-testid="quiz-builder-inspector-ai-prompt"
                @input="
                  (e: Event) =>
                    patchSelectedQuestion({
                      aiPrompt: (e.target as HTMLTextAreaElement).value,
                    })
                "
              />
            </div>
            <div class="grid grid-cols-2 gap-1">
              <div class="flex flex-col gap-1">
                <Label for="qb-ai-max">Max points</Label>
                <Input
                  id="qb-ai-max"
                  type="number"
                  :model-value="selectedQuestion.maxPoints ?? 100"
                  @input="
                    (e: Event) =>
                      patchSelectedQuestion({
                        maxPoints: parseNumber(
                          (e.target as HTMLInputElement).value,
                        ),
                      })
                  "
                />
              </div>
              <div
                v-if="selectedQuestion.scoringMethod === 'item_count'"
                class="flex flex-col gap-1"
              >
                <Label for="qb-ai-per">Points per item</Label>
                <Input
                  id="qb-ai-per"
                  type="number"
                  :model-value="selectedQuestion.pointsPerItem ?? 50"
                  @input="
                    (e: Event) =>
                      patchSelectedQuestion({
                        pointsPerItem: parseNumber(
                          (e.target as HTMLInputElement).value,
                        ),
                      })
                  "
                />
              </div>
            </div>
            <div
              v-if="selectedQuestion.scoringMethod !== 'ai_judgment'"
              class="flex flex-col gap-2"
            >
              <div class="flex items-center justify-between">
                <span
                  class="font-mono text-[10px] uppercase tracking-wide text-muted-foreground"
                >
                  Identifying items
                </span>
                <Button
                  variant="outline"
                  size="sm"
                  data-testid="quiz-builder-inspector-add-criterion"
                  @click="addCriterion"
                >
                  <Plus class="h-3.5 w-3.5" />
                  Add
                </Button>
              </div>
              <div
                v-for="(crit, cIdx) in selectedQuestion.aiCriteria ?? []"
                :key="crit.key"
                class="flex flex-col gap-1 rounded border bg-background p-2"
                :data-testid="`quiz-builder-inspector-criterion-${cIdx}`"
              >
                <div class="grid grid-cols-[1fr_5rem_auto] gap-1">
                  <Input
                    :model-value="crit.name"
                    placeholder="Name"
                    @input="
                      (e: Event) =>
                        setCriterionField(
                          cIdx,
                          'name',
                          (e.target as HTMLInputElement).value,
                        )
                    "
                  />
                  <Input
                    type="number"
                    :model-value="crit.points"
                    placeholder="Points"
                    @input="
                      (e: Event) =>
                        setCriterionField(
                          cIdx,
                          'points',
                          parseNumber(
                            (e.target as HTMLInputElement).value,
                          ),
                        )
                    "
                  />
                  <Button
                    variant="ghost"
                    size="icon-sm"
                    class="text-destructive"
                    aria-label="Remove criterion"
                    @click="removeCriterion(cIdx)"
                  >
                    <Trash2 class="h-3.5 w-3.5" />
                  </Button>
                </div>
                <Textarea
                  :model-value="crit.description"
                  placeholder="Description"
                  @input="
                    (e: Event) =>
                      setCriterionField(
                        cIdx,
                        'description',
                        (e.target as HTMLTextAreaElement).value,
                      )
                  "
                />
              </div>
            </div>
          </CardContent>
        </Card>
        </div>

        <!-- ─── GRADING TAB ─────────────────────────────────────── -->
        <div
          v-show="inspectorTab === 'grading'"
          class="flex flex-col gap-2"
          data-testid="quiz-builder-inspector-panel-grading"
        >
          <div class="flex flex-col gap-1">
            <Label for="qb-feedback-correct">Feedback — correct</Label>
            <Textarea
              id="qb-feedback-correct"
              :model-value="selectedQuestion.feedback?.correct ?? ''"
              data-testid="quiz-builder-inspector-feedback-correct"
              placeholder="Shown when the answer is correct"
              @input="
                (e: Event) =>
                  patchFeedback({
                    correct: (e.target as HTMLTextAreaElement).value,
                  })
              "
            />
          </div>
          <div class="flex flex-col gap-1">
            <Label for="qb-feedback-incorrect">Feedback — incorrect</Label>
            <Textarea
              id="qb-feedback-incorrect"
              :model-value="selectedQuestion.feedback?.incorrect ?? ''"
              data-testid="quiz-builder-inspector-feedback-incorrect"
              placeholder="Shown when the answer is incorrect"
              @input="
                (e: Event) =>
                  patchFeedback({
                    incorrect: (e.target as HTMLTextAreaElement).value,
                  })
              "
            />
          </div>
        </div>

        <Button
          variant="destructive"
          size="sm"
          class="self-end"
          data-testid="quiz-builder-remove"
          @click="onRemoveQuestion"
        >
          <Trash2 class="h-3.5 w-3.5" />
          Remove question
        </Button>
      </div>
    </aside>
  </div>
</template>
