<!--
  组件名称：CaseStepEditor.vue
  组件描述：UI 自动化测试步骤编辑器，支持增删/上下移/动态参数表单 + 语义化目标 + 状态验证 + 执行时机

  功能：
  1. 添加 / 删除步骤
  2. 上移 / 下移（自动重排 order）
  3. 按动作类型动态显示参数输入框（与执行器 13 种动作 + 10 种断言一致）
  4. 语义目标编辑（semantic：type/text/contains/scope/nth/container/save_as）
  5. 状态验证编辑（verify：type/selector/text/timeout/must）
  6. 执行时机（run_on：always/finally/on_failure/on_success）
  7. api_call 动作（数据自建/自清理，配合 run_on: finally）
  8. v-model 双向绑定 steps 数组

  说明：
  - 参数统一存储在 step.params 中（snake_case 键），与后端 playwright_executor 读取方式一致
  - 语义目标 / 状态验证 / 执行时机为顶层字段（阶段 1 起 schema 已声明，recorder model_dump 透传）
  - executor 读取：params.selector / params.url / params.value / params.type / params.expected / params.timeout / params.key / params.direction / params.distance / params.force / params.full_page

  @author czj
  @date 2026-07-14  (阶段 6：2026-08-18)
-->

<template>
  <div class="case-step-editor">
    <!-- 步骤为空时的占位 -->
    <el-empty
      v-if="!steps.length"
      description="暂无步骤，点击下方按钮添加"
      :image-size="60"
    />

    <!-- 步骤列表 -->
    <div
      v-for="(step, index) in steps"
      :key="index"
      class="step-item"
    >
      <!-- 步骤头部：序号 + 名称 + 动作 + 执行时机 + 操作按钮 -->
      <div class="step-header">
        <span class="step-order">{{ step.order }}</span>
        <el-input
          v-model="step.name"
          placeholder="步骤名称"
          size="small"
          style="width: 180px"
        />
        <el-select
          v-model="step.action"
          size="small"
          style="width: 170px"
          @change="onActionChange(step)"
        >
          <el-option
            v-for="opt in ACTION_OPTIONS"
            :key="opt.value"
            :label="opt.label"
            :value="opt.value"
          />
        </el-select>
        <el-select
          v-model="step.run_on"
          size="small"
          style="width: 120px"
          clearable
          placeholder="时机"
          title="执行时机"
        >
          <el-option label="正常 (always)" value="always" />
          <el-option label="清理 (finally)" value="finally" />
          <el-option label="失败后 (on_failure)" value="on_failure" />
          <el-option label="成功后 (on_success)" value="on_success" />
        </el-select>

        <div class="step-ops">
          <el-button
            size="small"
            link
            :disabled="index === 0"
            @click="moveStep(index, -1)"
            title="上移"
          >
            <el-icon><Top /></el-icon>
          </el-button>
          <el-button
            size="small"
            link
            :disabled="index === steps.length - 1"
            @click="moveStep(index, 1)"
            title="下移"
          >
            <el-icon><Bottom /></el-icon>
          </el-button>
          <el-button
            size="small"
            link
            type="danger"
            @click="removeStep(index)"
            title="删除"
          >
            <el-icon><Delete /></el-icon>
          </el-button>
        </div>
      </div>

      <!-- 动态参数表单 -->
      <div class="step-params">
        <!-- 导航 -->
        <template v-if="step.action === 'navigate'">
          <el-input
            v-model="getParams(step).url"
            placeholder="导航 URL，如 https://192.168.5.44 或 /login"
            size="small"
          >
            <template #prepend>URL</template>
          </el-input>
        </template>

        <!-- 滚动 -->
        <template v-else-if="step.action === 'scroll'">
          <el-select v-model="getParams(step).direction" size="small" style="width: 120px">
            <el-option label="向下" value="down" />
            <el-option label="向上" value="up" />
          </el-select>
          <el-input-number
            v-model="getParams(step).distance"
            :min="0"
            :step="100"
            size="small"
            controls-position="right"
            style="width: 140px"
          />
          <span class="param-hint">滚动距离(px)</span>
        </template>

        <!-- 按键 -->
        <template v-else-if="step.action === 'press'">
          <el-input
            v-model="getParams(step).key"
            placeholder="按键，如 Enter / Tab / Escape / ArrowDown"
            size="small"
            style="width: 260px"
          >
            <template #prepend>按键</template>
          </el-input>
          <el-input
            v-model="getParams(step).selector"
            placeholder="选择器（可选，不填则在当前焦点按键）"
            size="small"
            style="width: 300px"
          />
        </template>

        <!-- api_call（数据自建/自清理） -->
        <template v-else-if="step.action === 'api_call'">
          <el-select
            v-model="getApiCase(step).method"
            size="small"
            style="width: 110px"
          >
            <el-option label="GET" value="GET" />
            <el-option label="POST" value="POST" />
            <el-option label="PUT" value="PUT" />
            <el-option label="DELETE" value="DELETE" />
          </el-select>
          <el-input
            v-model="getApiCase(step).url"
            placeholder="接口路径，如 /api/meeting/query（base_url 取被测系统 URL）"
            size="small"
            style="width: 320px"
          />
          <el-input
            v-model="getApiCase(step).bodyText"
            type="textarea"
            :rows="2"
            placeholder='请求体 JSON，如 {"name": "{__CTX:meetingName__}"}'
            size="small"
            style="width: 100%"
            class="api-body"
          />
          <span class="param-hint">支持 {__CTX:key__} 上下文变量；配合 run_on: finally 做数据清理</span>
        </template>

        <!-- 断言 -->
        <template v-else-if="step.action === 'assert'">
          <el-select
            v-model="getParams(step).type"
            size="small"
            style="width: 160px"
            placeholder="断言类型"
          >
            <el-option
              v-for="t in ASSERT_TYPE_OPTIONS"
              :key="t.value"
              :label="t.label"
              :value="t.value"
            />
          </el-select>
          <el-input
            v-if="needsSelector(getParams(step).type)"
            v-model="getParams(step).selector"
            placeholder="元素选择器"
            size="small"
            style="width: 280px"
          />
          <el-input
            v-if="needsExpected(getParams(step).type)"
            v-model="getParams(step).expected"
            placeholder="期望值（文本/数值/URL片段）"
            size="small"
            style="width: 280px"
          />
          <el-input-number
            v-if="getParams(step).type === 'element_count_min'"
            v-model="getParams(step).min_count"
            :min="0"
            size="small"
            controls-position="right"
            style="width: 120px"
          />
          <span v-if="getParams(step).type === 'element_count_min'" class="param-hint">最小数量</span>
        </template>

        <!-- 截图 -->
        <template v-else-if="step.action === 'screenshot'">
          <el-switch
            v-model="getParams(step).full_page"
            active-text="整页截图"
            inactive-text="可视区域"
          />
        </template>

        <!-- 需要选择器的动作：click / fill / select / hover / wait / check / uncheck -->
        <template v-else>
          <el-input
            v-model="getParams(step).selector"
            placeholder="元素选择器（支持 CSS / XPath / 文本）"
            size="small"
            style="width: 340px"
          >
            <template #prepend>选择器</template>
          </el-input>
          <!-- fill / select 需要值 -->
          <el-input
            v-if="step.action === 'fill' || step.action === 'select'"
            v-model="getParams(step).value"
            placeholder="输入值"
            size="small"
            style="width: 220px"
          >
            <template #prepend>值</template>
          </el-input>
          <!-- wait 需要超时 -->
          <el-input-number
            v-if="step.action === 'wait'"
            v-model="getParams(step).timeout"
            :min="0"
            :step="1000"
            size="small"
            controls-position="right"
            style="width: 160px"
          />
          <span v-if="step.action === 'wait'" class="param-hint">超时(ms)</span>
          <!-- click / fill / check / uncheck 支持强制 -->
          <el-checkbox
            v-if="['click', 'fill', 'check', 'uncheck'].includes(step.action)"
            v-model="getParams(step).force"
          >
            强制操作
          </el-checkbox>
        </template>
      </div>

      <!-- 语义目标 + 状态验证（高级区，可折叠） -->
      <div class="step-advanced">
        <el-button
          size="small"
          link
          type="primary"
          @click="toggleAdvanced(index)"
        >
          <el-icon><ArrowDown v-if="!isAdvancedOpen(index)" /><ArrowUp v-else /></el-icon>
          {{ hasSemantic(step) || hasVerify(step) ? '语义目标/状态验证' : '记录业务意图（可选）' }}
        </el-button>
        <div v-if="isAdvancedOpen(index)" class="advanced-panel">
          <!-- 语义目标 -->
          <div class="adv-section">
            <span class="adv-title">语义目标（semantic）</span>
            <div class="adv-row">
              <el-select v-model="getSemantic(step).type" size="small" style="width: 140px" placeholder="类型">
                <el-option
                  v-for="t in SEMANTIC_TYPE_OPTIONS"
                  :key="t.value"
                  :label="t.label"
                  :value="t.value"
                />
              </el-select>
              <el-input
                v-model="getSemantic(step).text"
                placeholder="业务文本，如 预定会议 / {__CTX:meetingName__}"
                size="small"
                style="width: 300px"
              />
              <el-input
                v-model="getSemantic(step).contains"
                placeholder="次级过滤 contains（可选）"
                size="small"
                style="width: 200px"
              />
              <el-input
                v-model="getSemantic(step).scope"
                placeholder="页面 scope（如 create_meeting）"
                size="small"
                style="width: 180px"
              />
            </div>
            <div class="adv-row">
              <el-input
                v-model="getSemantic(step).container.selector"
                placeholder="父容器选择器（可选）"
                size="small"
                style="width: 260px"
              />
              <el-input-number
                v-model="getSemantic(step).nth"
                :min="1"
                size="small"
                controls-position="right"
                style="width: 100px"
              />
              <span class="param-hint">第 N 个匹配（1 起）</span>
              <el-input
                v-model="getSemantic(step).save_as"
                placeholder="save_as 变量名（提取值写入上下文）"
                size="small"
                style="width: 260px"
              />
            </div>
          </div>

          <!-- 状态验证 -->
          <div class="adv-section">
            <span class="adv-title">状态验证（verify，防假通过）</span>
            <div class="adv-row">
              <el-select v-model="getVerify(step).type" size="small" style="width: 160px" placeholder="验证类型" clearable>
                <el-option
                  v-for="t in ASSERT_TYPE_OPTIONS"
                  :key="t.value"
                  :label="t.label"
                  :value="t.value"
                />
              </el-select>
              <el-input
                v-if="verifyNeedsSelector(getVerify(step).type)"
                v-model="getVerify(step).selector"
                placeholder="验证选择器"
                size="small"
                style="width: 260px"
              />
              <el-input
                v-if="verifyNeedsText(getVerify(step).type)"
                v-model="getVerify(step).text"
                placeholder="期望文本/URL片段"
                size="small"
                style="width: 220px"
              />
              <el-input-number
                v-model="getVerify(step).timeout"
                :min="0"
                :step="1000"
                size="small"
                controls-position="right"
                style="width: 120px"
              />
              <span class="param-hint">超时ms</span>
              <el-checkbox v-model="getVerify(step).must">
                失败即失败(must)
              </el-checkbox>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 添加步骤按钮 -->
    <el-button
      type="primary"
      plain
      size="small"
      class="add-step-btn"
      @click="addStep"
    >
      <el-icon><Plus /></el-icon>
      添加步骤
    </el-button>
  </div>
</template>

<script setup lang="ts">
/**
 * CaseStepEditor 组件
 *
 * UI 自动化测试步骤编辑器（阶段 6 增强）。
 * 通过 v-model 双向绑定 steps 数组，参数存储在 step.params 中。
 * 语义目标 / 状态验证 / 执行时机为顶层字段，保存时随步骤一并提交。
 */
import { computed, ref } from 'vue'
import { Top, Bottom, Delete, Plus, ArrowDown, ArrowUp } from '@element-plus/icons-vue'

/** 步骤对象结构 */
interface Step {
  order: number
  name: string
  action: string
  params: Record<string, any>
  semantic?: Record<string, any>
  verify?: Record<string, any>
  run_on?: string
  [key: string]: any
}

const props = defineProps<{
  /** v-model 绑定的步骤数组 */
  modelValue: Step[]
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', value: Step[]): void
}>()

/** steps 计算属性：直接操作同一数组引用，变更后 emit 通知父组件 */
const steps = computed(() => props.modelValue)

/** 高级区展开状态（按步骤索引） */
const advancedOpen = ref<Record<number, boolean>>({})

const isAdvancedOpen = (index: number): boolean => !!advancedOpen.value[index]

const toggleAdvanced = (index: number) => {
  advancedOpen.value[index] = !advancedOpen.value[index]
}

// ==================== 动作与断言配置 ====================

/** 执行器支持的 13 种动作（与 playwright_executor 一致） */
const ACTION_OPTIONS = [
  { label: '导航 (navigate)', value: 'navigate' },
  { label: '点击 (click)', value: 'click' },
  { label: '填充 (fill)', value: 'fill' },
  { label: '下拉选择 (select)', value: 'select' },
  { label: '悬停 (hover)', value: 'hover' },
  { label: '等待元素 (wait)', value: 'wait' },
  { label: '滚动 (scroll)', value: 'scroll' },
  { label: '按键 (press)', value: 'press' },
  { label: '勾选 (check)', value: 'check' },
  { label: '取消勾选 (uncheck)', value: 'uncheck' },
  { label: '断言 (assert)', value: 'assert' },
  { label: '截图 (screenshot)', value: 'screenshot' },
  { label: '接口调用 (api_call)', value: 'api_call' },
]

/** 语义目标类型选项（与后端 semantic_resolver TYPE_ACTION_MAP 一致） */
const SEMANTIC_TYPE_OPTIONS = [
  { label: '按钮 (button)', value: 'button' },
  { label: '输入框 (input)', value: 'input' },
  { label: '文本域 (textarea)', value: 'textarea' },
  { label: '复选框 (checkbox)', value: 'checkbox' },
  { label: '行复选框 (row_checkbox)', value: 'row_checkbox' },
  { label: '表格行 (row)', value: 'row' },
  { label: '标签页 (tab)', value: 'tab' },
  { label: '链接 (link)', value: 'link' },
  { label: '菜单 (menu)', value: 'menu' },
  { label: '图标 (icon)', value: 'icon' },
  { label: '文本 (text)', value: 'text' },
  { label: '弹窗 (dialog)', value: 'dialog' },
]

/** 断言类型选项（10 种，与后端 _do_assert 对齐；url 已统一为 url_contains） */
const ASSERT_TYPE_OPTIONS = [
  { label: '元素可见 (visible)', value: 'visible' },
  { label: '元素隐藏 (hidden)', value: 'hidden' },
  { label: '文本相等 (text)', value: 'text' },
  { label: '文本包含 (contains)', value: 'contains' },
  { label: '输入值相等 (value)', value: 'value' },
  { label: '元素数量 (count)', value: 'count' },
  { label: 'URL 包含 (url_contains)', value: 'url_contains' },
  { label: '标题包含 (title)', value: 'title' },
  { label: '元素存在 (element_exists)', value: 'element_exists' },
  { label: '数量>=最小 (element_count_min)', value: 'element_count_min' },
]

// ==================== 辅助方法 ====================

/**
 * 获取步骤的 params 对象（不存在则初始化为空对象）
 * 确保嵌套对象可响应式绑定
 */
const getParams = (step: Step): Record<string, any> => {
  if (!step.params || typeof step.params !== 'object') {
    step.params = {}
  }
  return step.params
}

/** 获取 semantic 对象（不存在则初始化） */
const getSemantic = (step: Step): Record<string, any> => {
  if (!step.semantic || typeof step.semantic !== 'object') {
    step.semantic = { type: 'button' }
  }
  if (!step.semantic.container || typeof step.semantic.container !== 'object') {
    step.semantic.container = {}
  }
  return step.semantic
}

/** 获取 verify 对象（不存在则初始化） */
const getVerify = (step: Step): Record<string, any> => {
  if (!step.verify || typeof step.verify !== 'object') {
    step.verify = {}
  }
  return step.verify
}

/** 获取 api_call 的 api_case 对象（bodyText 为编辑态文本，提交时转 JSON） */
const getApiCase = (step: Step): Record<string, any> => {
  const p = getParams(step)
  if (!p.api_case || typeof p.api_case !== 'object') {
    p.api_case = { method: 'POST', url: '' }
  }
  const c = p.api_case
  if (c.bodyText == null) {
    c.bodyText = c.body ? JSON.stringify(c.body, null, 2) : ''
  }
  return c
}

/** 语义 / 验证是否存在（用于折叠按钮文案） */
const hasSemantic = (step: Step): boolean =>
  !!step.semantic && !!step.semantic.text

const hasVerify = (step: Step): boolean =>
  !!step.verify && !!step.verify.type

/** 断言是否需要选择器 */
const needsSelector = (type: string): boolean =>
  ['visible', 'hidden', 'text', 'contains', 'value', 'count', 'element_exists', 'element_count_min'].includes(type || '')

/** 断言是否需要期望值 */
const needsExpected = (type: string): boolean =>
  ['text', 'contains', 'value', 'count', 'url_contains', 'title', 'element_count_min'].includes(type || '')

/** 验证是否需要选择器 */
const verifyNeedsSelector = (type: string): boolean =>
  needsSelector(type)

/** 验证是否需要期望文本 */
const verifyNeedsText = (type: string): boolean =>
  needsExpected(type)

/**
 * 动作变更时重置无关参数，避免残留干扰
 * 保留通用字段，清空动作特有字段
 */
const onActionChange = (step: Step) => {
  const p = getParams(step)
  // 断言默认类型为 visible
  if (step.action === 'assert' && !p.type) {
    p.type = 'visible'
  }
  // api_call 默认 POST
  if (step.action === 'api_call') {
    const c = getApiCase(step)
    if (!c.method) c.method = 'POST'
  }
  // 滚动默认向下 300px
  if (step.action === 'scroll') {
    if (!p.direction) p.direction = 'down'
    if (p.distance == null) p.distance = 300
  }
  emitChange()
}

// ==================== 步骤增删移动 ====================

/** 添加步骤（自动编号 + 默认动作 click） */
const addStep = () => {
  steps.value.push({
    order: steps.value.length + 1,
    name: `步骤${steps.value.length + 1}`,
    action: 'click',
    params: { selector: '' },
  })
  emitChange()
}

/** 删除步骤（重排 order） */
const removeStep = (index: number) => {
  steps.value.splice(index, 1)
  delete advancedOpen.value[index]
  renumber()
}

/**
 * 移动步骤
 * @param index 当前索引
 * @param dir -1 上移 / 1 下移
 */
const moveStep = (index: number, dir: number) => {
  const target = index + dir
  if (target < 0 || target >= steps.value.length) return
  const tmp = steps.value[index]
  steps.value[index] = steps.value[target]
  steps.value[target] = tmp
  renumber()
}

/** 重新编号 order（从 1 开始） */
const renumber = () => {
  steps.value.forEach((s, i) => {
    s.order = i + 1
  })
  emitChange()
}

/** 通知父组件变更（保持 v-model 同步） */
const emitChange = () => {
  emit('update:modelValue', steps.value)
}
</script>

<style lang="scss" scoped>
.case-step-editor {
  width: 100%;

  .step-item {
    padding: 12px;
    margin-bottom: 12px;
    background: #fafbfc;
    border: 1px solid #ebeef5;
    border-radius: 6px;

    .step-header {
      display: flex;
      align-items: center;
      gap: 8px;
      margin-bottom: 10px;

      .step-order {
        flex-shrink: 0;
        width: 24px;
        height: 24px;
        line-height: 24px;
        text-align: center;
        background: #409eff;
        color: #fff;
        border-radius: 50%;
        font-size: 12px;
        font-weight: 600;
      }

      .step-ops {
        margin-left: auto;
        display: flex;
        align-items: center;
      }
    }

    .step-params {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 8px;
      padding-left: 32px;

      .api-body {
        margin-top: 4px;
      }

      .param-hint {
        font-size: 12px;
        color: #909399;
      }
    }

    .step-advanced {
      margin-top: 8px;
      padding-left: 32px;

      .advanced-panel {
        margin-top: 6px;
        padding: 10px;
        background: #f4f7fa;
        border: 1px dashed #d0d7de;
        border-radius: 4px;

        .adv-section {
          margin-bottom: 10px;

          &:last-child {
            margin-bottom: 0;
          }

          .adv-title {
            display: block;
            font-size: 12px;
            font-weight: 600;
            color: #409eff;
            margin-bottom: 6px;
          }

          .adv-row {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 8px;
            margin-bottom: 6px;

            &:last-child {
              margin-bottom: 0;
            }

            .param-hint {
              font-size: 12px;
              color: #909399;
            }
          }
        }
      }
    }
  }

  .add-step-btn {
    width: 100%;
    border-style: dashed;
  }
}
</style>
