
// ─── A9 — SimulatePlanBlock ────────────────────────────────────────────────
// Appended to page-gaokao.jsx at build time by the append script below.
// Renders the "生成完整志愿方案" block beneath the result table.

const BUCKET_COLOR_PLAN = { reach: 'var(--pb-danger)', match: 'var(--pb-navy-700)', safety: 'var(--pb-success)' };

// A3 — provinces where the 普通批 one-click smart slate is fully live.
// HI (海南): verified, backtested. GD (广东): official eea.gd.gov.cn 院校专业组-level
// 2024 data; calibration pending 2025 backtest (added 2026-06-13, PR #unlocked).
// ZJ (浙江) / SD (山东): official 逐专业平行投档数据; 80/96 major_parallel slots;
// 选科待核; calibration pending cross-year backtest (added 2026-06-14, Phase 2a-B).
// All others show a guard banner instead of generating a potentially misleading slate.
// IMPORTANT: keep in sync with FULL_SET in lib/gaokao-province-capabilities.js.
const ONE_CLICK_SLATE_PROVINCES = new Set(['HI', 'GD', 'ZJ', 'SD']);

// #338 — 5-tier 搏/冲/稳/保/垫 relabel from a slot's probability (DISPLAY only; the
// 30-slot allocation still runs on the 3-tier engine bucket). Self-contained so it
// works on both the in-browser-Babel and the precompiled dist load paths. Keys
// match window.PBAdmissionModel.fineBucket.
const FINE_TIER_COLOR_PLAN = { bo: '#b91c1c', chong: 'var(--pb-danger)', wen: 'var(--pb-navy-700)', bao: 'var(--pb-success)', dian: '#047857' };
const FINE_TIER_ZH_PLAN = { bo: '搏', chong: '冲', wen: '稳', bao: '保', dian: '垫' };
function fineKeyForSlotProb(prob) {
  const M = (typeof window !== 'undefined' && window.PBAdmissionModel) || null;
  const p = (typeof prob === 'number' ? prob : 50) / 100;
  return (M && M.fineBucket) ? M.fineBucket(p) : 'wen';
}

function displayableMajorChoices(slot) {
  const gate = (typeof window !== 'undefined' && window.PBSourceCapabilities) || null;
  return gate && typeof gate.verifiedSlotMajors === 'function'
    ? gate.verifiedSlotMajors(slot)
    : [];
}

// ── Batch definitions (H1.2) ──────────────────────────────────────────────
// HI keeps rule tabs visible, while unverified datasets return a placeholder.
// nameKey / placeholderKey map to i18n.simulate keys for lang-aware rendering.
const PROVINCE_BATCHES = {
  HI: [
    { code: 'normal',     nameKey: 'batchNormal',     slots: 30, hasRealData: true, placeholderKey: null },
    { code: 'early',      nameKey: 'batchEarly',      slots: 6,  hasRealData: true,  placeholderKey: null },
    { code: 'special',    nameKey: 'batchSpecial',    slots: 10, hasRealData: true,  placeholderKey: null },
    // 高职(专科)批 10 组（同年官方投档线 + 一分一段位次换算）
    { code: 'vocational', nameKey: 'batchVocational', slots: 10, hasRealData: true, placeholderKey: null },
  ],
  // Wave 5-A: GD now has real admissions (PR #75), so normal batch is real-data.
  // Slot counts per lib/gaokao-batch-config.js GD block (45/20/15/null).
  GD: [
    { code: 'normal',  nameKey: 'batchNormal',   slots: 45,   hasRealData: true,  placeholderKey: null },
    { code: 'early',   nameKey: 'batchEarly',    slots: 20,   hasRealData: false, placeholderKey: 'placeholderEarly' },
    { code: 'special', nameKey: 'batchSpecial',  slots: 15,   hasRealData: false, placeholderKey: 'placeholderSpecial' },
    { code: 'qjj',     nameKey: 'batchQjj',      slots: null, hasRealData: false, placeholderKey: 'placeholderQjj' },
  ],
  // Phase 2a-B: ZJ/SD major_parallel real admissions (2026-06-14).
  // 专业平行志愿: each slot = one specific major at one school.
  ZJ: [
    { code: 'normal',  nameKey: 'batchNormal',   slots: 80,   hasRealData: true,  placeholderKey: null },
    { code: 'early',   nameKey: 'batchEarly',    slots: null, hasRealData: false, placeholderKey: 'placeholderEarly' },
    { code: 'special', nameKey: 'batchSpecial',  slots: null, hasRealData: false, placeholderKey: 'placeholderSpecial' },
  ],
  SD: [
    { code: 'normal',  nameKey: 'batchNormal',   slots: 96,   hasRealData: true,  placeholderKey: null },
    { code: 'early',   nameKey: 'batchEarly',    slots: null, hasRealData: false, placeholderKey: 'placeholderEarly' },
    { code: 'special', nameKey: 'batchSpecial',  slots: null, hasRealData: false, placeholderKey: 'placeholderSpecial' },
  ],
  DEFAULT: [
    { code: 'normal', nameKey: 'batchNormal', slots: null, hasRealData: false, placeholderKey: null },
  ],
};

function getBatchesForProvince(province) {
  // HI/GD ship curated multi-batch definitions — keep them.
  if (PROVINCE_BATCHES[province]) return PROVINCE_BATCHES[province];
  // National: derive the normal-batch slot count from window.PBPlanRules
  // (lib/gaokao-plan-rules.js) and always offer 提前批/专项 (we ship national
  // admissions-batches data for all 30 provinces; the panels fetch on demand).
  var pr = (window.PBPlanRules && window.PBPlanRules.getPlanRules) ? window.PBPlanRules.getPlanRules(province) : null;
  if (!pr || !pr.batches || !pr.batches[0]) return PROVINCE_BATCHES.DEFAULT;
  return [
    { code: 'normal',  nameKey: 'batchNormal',  slots: pr.batches[0].slots, hasRealData: true, placeholderKey: null },
    { code: 'early',   nameKey: 'batchEarly',   slots: null, hasRealData: true, placeholderKey: null },
    { code: 'special', nameKey: 'batchSpecial', slots: null, hasRealData: true, placeholderKey: null },
  ];
}

// Per-province 报考方式 (选科模式 / 投档单位 / 志愿数 / 调剂) from window.PBPlanRules.
function getAdmissionRules(province) {
  return (window.PBPlanRules && window.PBPlanRules.getPlanRules) ? window.PBPlanRules.getPlanRules(province) : null;
}

// Wave 9-D — QJJ schools data (browser-side, lazy fetch + cache)
const _QJJ_CACHE = {};
async function _loadQjjData(province, year) {
  const key = `${province}-${year}`;
  if (_QJJ_CACHE[key] !== undefined) return _QJJ_CACHE[key];
  // 强基计划 = 教育部 fixed ~39-校 program, year-stable. The selected year's file may
  // not be published yet (e.g. HI 2025 missing → only 2024 on disk), which wrongly
  // showed 「暂无强基计划数据」. Fall back to the most recent available year (up to 3 back).
  const y = Number(year) || new Date().getFullYear();
  for (let yy = y; yy >= y - 3; yy--) {
    try {
      const res = await fetch(`/data/gaokao-qjj/${province}/${yy}.json`);
      if (res.ok) {
        const data = await res.json();
        const arr = Array.isArray(data) ? data : null;
        if (arr && arr.length) { _QJJ_CACHE[key] = arr; return arr; }
      }
    } catch (_e) { /* try previous year */ }
  }
  _QJJ_CACHE[key] = null;
  return null;
}

const SimulatePlanBlock = ({ result, province, year, score, rank, subjects, majorCategory, preferences, lang, isAnonymous, draftHydration, draftHydrationLoading, onDraftHydrationClear, planFilters = {} }) => {
  // 会员门槛总开关 (lib/access-gates.js) — when off (functional testing) the
  // anonymous "免费模拟方案 5 槽预览 / 登录解锁" framing is suppressed: anon users
  // get the same full-plan copy + table as members. 登录保存 CTA stays (factual).
  const gatesOn = !!(window.PBAccessGates && window.PBAccessGates.gatesEnabled());
  const gatedAnon = !!isAnonymous && gatesOn;
  const [planState, setPlanState] = React.useState(null);
  const [isEstimatePlan, setIsEstimatePlan] = React.useState(false);
  const [isVerifiedPlan, setIsVerifiedPlan] = React.useState(false);
  const [collapsed, setCollapsed] = React.useState(false);
  const [activeBatch, setActiveBatch] = React.useState('normal');
  const [qjjData, setQjjData] = React.useState(null);

  // M43.1 — When draftHydration arrives, initialise planState from it so the plan renders immediately
  React.useEffect(() => {
    if (!draftHydration || !Array.isArray(draftHydration.plan) || draftHydration.plan.length === 0) return;
    setPlanState({ data: { plan: draftHydration.plan, summary: draftHydration.summary, rules: null, warnings: [] }, anonymous: false });
    setCollapsed(false);
  }, [draftHydration]);
  const [qjjLoading, setQjjLoading] = React.useState(false);
  // Wave 22-A — early/special batch schools panel
  const [batchPanelData, setBatchPanelData] = React.useState(null);
  const [batchPanelMeta, setBatchPanelMeta] = React.useState(null);
  const [batchPanelLoading, setBatchPanelLoading] = React.useState(false);
  const [originalPlan, setOriginalPlan] = React.useState(null);
  const [optSummary, setOptSummary] = React.useState(null);
  // M41.3 — save draft state
  const [saveBanner, setSaveBanner] = React.useState(null); // null | 'saving' | 'ok' | 'error' | 'limit'
  // M41.4 — export state
  const [copyConfirm, setCopyConfirm] = React.useState(false);

  // M41.3 — auth check (window.useAuth mirrors dashboard pattern)
  const _auth = (typeof window.useAuth === 'function') ? window.useAuth() : null;
  const _isLoggedIn = !!(_auth && _auth.user);

  const PROVINCE_SLOTS = { BJ: 180, SH: 96, GD: 270, JS: 240, ZJ: 80, SD: 96, HI: 180 };
  // i18n-aware helpers — H1.8 keys live at I18N.{lang}.gk.simulate (nested under
  // the 'gk' gaokao namespace, not at the top level). The pre-fix path
  // I18N.{lang}.simulate was always undefined, which fell through to raw keys
  // ('batchNormal', 'batchEarly', ...) in the segmented control labels.
  const sim = (window.I18N && window.I18N[lang] && window.I18N[lang].gk && window.I18N[lang].gk.simulate)
    || (window.I18N && window.I18N.zh && window.I18N.zh.gk && window.I18N.zh.gk.simulate)
    || null;
  // Wave 49 M49.3 — peer-cohort dict
  const peerD = (window.I18N && window.I18N[lang] && window.I18N[lang].gk && window.I18N[lang].gk.peerCohort)
    || (window.I18N && window.I18N.zh && window.I18N.zh.gk && window.I18N.zh.gk.peerCohort)
    || null;
  // Wave 49 M49.4 — compareProfile dict (PK button + cart)
  const cpD = (window.I18N && window.I18N[lang] && window.I18N[lang].gk && window.I18N[lang].gk.compareProfile)
    || (window.I18N && window.I18N.zh && window.I18N.zh.gk && window.I18N.zh.gk.compareProfile)
    || null;
  // Wave 49 M49.4 — picked codes for PK (max 4, dedupe)
  const [pickedCodes, setPickedCodes] = React.useState([]);
  const PK_MAX = 4;
  const getPkCode = React.useCallback((slot) => {
    if (!slot) return '';
    const raw = slot.enrollCode || slot.schoolCode || slot.baseSchoolId || slot.schoolId || '';
    return String(raw).split(':')[0].trim();
  }, []);
  const togglePickedCode = React.useCallback((code) => {
    if (!code) return;
    setPickedCodes((prev) => {
      if (prev.includes(code)) return prev.filter((c) => c !== code);
      if (prev.length >= PK_MAX) return prev;
      return [...prev, code];
    });
  }, []);
  const clearPickedCodes = React.useCallback(() => setPickedCodes([]), []);
  const launchCompare = React.useCallback(() => {
    if (pickedCodes.length < 2) return;
    const q = new URLSearchParams();
    q.set('codes', pickedCodes.join(','));
    if (province) q.set('province', province);
    if (year) q.set('year', String(year));
    if (score) q.set('score', String(score));
    if (rank) q.set('rank', String(rank));
    window.location.hash = '#/gaokao/compare?' + q.toString();
  }, [pickedCodes, province, year, score, rank]);
  const provinceName = (sim && sim.provinces && sim.provinces[province]) || province;
  const getBatchName = (b) => (sim && sim[b.nameKey]) || b.nameKey;
  const getBatchPlaceholder = (b) => b.placeholderKey ? ((sim && sim[b.placeholderKey]) || b.placeholderKey) : null;
  const BUCKET_LABEL = (sim && sim.buckets) || { reach: 'reach', match: 'match', safety: 'safety' };
  const FINE_LABEL_PLAN = (sim && sim.fineTiers) || FINE_TIER_ZH_PLAN;

  const batches = getBatchesForProvince(province);
  const currentBatch = batches.find((b) => b.code === activeBatch) || batches[0];
  // Per-province 报考方式 (选科模式 / 投档单位 / 本科批志愿数 / 服从调剂) for the explainer.
  const admissionRules = getAdmissionRules(province);
  const normalBatchRule = admissionRules && admissionRules.batches ? admissionRules.batches[0] : null;
  // totalSlots: use batch-specific slots when currentBatch is defined.
  // If currentBatch.slots is null (e.g. qjj = per-school), keep null so the
  // heading renders "按校" instead of a number. Only fall back to PROVINCE_SLOTS
  // when there is no currentBatch at all (should not occur in practice).
  const totalSlots = currentBatch
    ? (currentBatch.slots != null ? currentBatch.slots : null)
    : (PROVINCE_SLOTS[province] || '?');

  // QA 2026-06-10 — 线下横幅"一键切换专科批"切换后还要再点一次生成，
  // 打了折扣：带 autoGenerate 的切换在状态落地后自动触发 onGenerate。
  const [autoGenBatch, setAutoGenBatch] = React.useState(null);
  const onBatchSwitch = (code, opts) => {
    if (code === activeBatch) return;
    setActiveBatch(code);
    setPlanState(null); // clear old plan on batch switch
    setCollapsed(false);
    setBatchPanelData(null); // clear early/special panel on switch
    if (opts && opts.autoGenerate) setAutoGenBatch(code);
  };
  React.useEffect(() => {
    if (!autoGenBatch || autoGenBatch !== activeBatch) return;
    setAutoGenBatch(null);
    onGenerate();
  }, [autoGenBatch, activeBatch]);

  const onGenerate = async () => {
    if (!province) return;
    // Non-real-data batches: show placeholder immediately without API call
    const batchPlaceholder = getBatchPlaceholder(currentBatch);
    if (currentBatch && !currentBatch.hasRealData && batchPlaceholder) {
      setPlanState({ placeholder: true, placeholderMessage: batchPlaceholder });
      return;
    }
    // A3 — Non-HI guard (宁缺勿错): the one-click smart slate is only live for
    // provinces in ONE_CLICK_SLATE_PROVINCES.  For all other provinces' normal
    // batch, show a guard banner instead of generating a potentially misleading slate.
    // Use window.PBProvinceCapabilities as single source of truth when available;
    // fall back to ONE_CLICK_SLATE_PROVINCES for resilience (e.g. script load order).
    const _cap = window.PBProvinceCapabilities
      ? window.PBProvinceCapabilities.provinceCapabilities(province)
      : (ONE_CLICK_SLATE_PROVINCES.has(province) ? 'full' : 'rank-only');
    const _notFull = _cap !== 'full';
    const _isEstimate = _cap === 'estimate';
    const _isVerified = _cap === 'verified';
    // verified provinces have a real, cross-verified plan (like estimate) — they must
    // NOT fall into the placeholder guard meant for rank-only/none provinces.
    if (activeBatch === 'normal' && _notFull && !_isEstimate && !_isVerified) {
      // Part C — honest 报考方式 placeholder: build a bilingual message from PBPlanRules
      // so the user sees the real slot/model facts even before this province is unlocked.
      const _honestMsg = (window.PBPlanRules && window.PBPlanRules.buildPlaceholderMessage)
        ? window.PBPlanRules.buildPlaceholderMessage(province, lang)
        : null;
      setPlanState({ placeholder: true, placeholderMessage: _honestMsg || (sim && sim.oneClickNotReady) || '该省数据补齐后开放' });
      setIsEstimatePlan(false);
      setIsVerifiedPlan(false);
      return;
    }
    // Hybrid estimate tier — track so the banner renders after the API responds.
    // verified ≠ estimate: verified drops the 仅供参考 banner and shows the
    // cross-verified provenance banner instead.
    setIsEstimatePlan(_isEstimate);
    setIsVerifiedPlan(_isVerified);
    // Wave 22-A — early/special batch: load schools via PBBatchLoader and show panel
    // A1b: also fetch metadata (source, controlLine) to drive real-vs-sample labelling
    // #444 — HI early goes through the simulate API instead: the engine applies
    // the requireRealSource gate (2025-early.json still carries 11 unsourced
    // legacy seed rows the raw loader panel would display verbatim).
    if ((activeBatch === 'early' || activeBatch === 'special') && !(province === 'HI' && (activeBatch === 'early' || activeBatch === 'special'))) {
      setBatchPanelLoading(true);
      setBatchPanelData(null);
      setBatchPanelMeta(null);
      try {
        const loader = window.PBBatchLoader;
        let schools = [];
        let meta = null;
        if (loader && typeof loader.fetchBatchSchools === 'function') {
          schools = await loader.fetchBatchSchools(province, year || 2025, activeBatch);
        }
        if (loader && typeof loader.fetchBatchMeta === 'function') {
          meta = await loader.fetchBatchMeta(province, year || 2025, activeBatch);
        }
        setBatchPanelData(Array.isArray(schools) ? schools : []);
        setBatchPanelMeta(meta || null);
      } catch (_e) {
        setBatchPanelData([]);
        setBatchPanelMeta(null);
      }
      setBatchPanelLoading(false);
      setPlanState({ batchPanel: true });
      return;
    }
    // Wave 9-D — QJJ batch: load and show QJJ schools panel instead of simulate API
    if (activeBatch === 'qjj') {
      setQjjLoading(true);
      const data = await _loadQjjData(province, year || 2025);
      setQjjData(data);
      setQjjLoading(false);
      setPlanState({ qjjPanel: true });
      return;
    }
    setPlanState({ loading: true });
    try {
      const r = await fetch('/api/gaokao/calculate', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ simulatePlan: true, province, year, score, rank, subjects, majorCategory, preferences, batch: activeBatch, lang }),
      });
      let data = {};
      try { data = await r.json(); } catch (_) {}
      if ((r.status === 200 || r.status === 201) && data.plan) {
        setPlanState({ data: data.plan, anonymous: data.anonymous });
        setCollapsed(false);
        return;
      }
      if (r.status === 429) {
        setPlanState({ error: lang === 'zh' ? '请求过于频繁，请稍后再试' : 'Rate limited, try later' });
        return;
      }
      setPlanState({ error: lang === 'zh' ? '生成失败，请稍后再试' : 'Generation failed' });
    } catch (_) {
      setPlanState({ error: lang === 'zh' ? '网络错误' : 'Network error' });
    }
  };

  const opt = (window.I18N && window.I18N[lang] && window.I18N[lang].gk && window.I18N[lang].gk.optimizer) || {};

  const handleSimOptimize = () => {
    const optimizer = window.PBSlotOptimizer;
    if (!optimizer || !plan) return;

    // Enrich plan slots with prob (0–1) and plan fields for optimizer
    const enriched = plan.map((slot) => ({
      ...slot,
      prob: slot.probability != null ? slot.probability / 100 : 0.5,
      plan: slot.planCount != null ? slot.planCount : 10,
    }));

    setOriginalPlan([...plan]);

    const optimized = optimizer.optimizeSlotOrder(enriched, { preserveIndex: true });
    const comparison = optimizer.formatComparison(enriched, optimized);

    // Patch planState.data.plan with reordered slots
    setPlanState((prev) => ({
      ...prev,
      data: { ...prev.data, plan: optimized },
    }));
    setOptSummary(comparison);
  };

  const handleSimRevert = () => {
    if (originalPlan) {
      setPlanState((prev) => ({
        ...prev,
        data: { ...prev.data, plan: originalPlan },
      }));
      setOriginalPlan(null);
      setOptSummary(null);
    }
  };

  // M41.4 — blob download helper
  const _downloadBlob = (content, filename, mime) => {
    const blob = new Blob([content], { type: mime });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() => URL.revokeObjectURL(url), 100);
  };

  // M41.4 — export handlers
  const _exp = (typeof window !== 'undefined' && window.PathBridgePlanExport) || null;

  const handleCopyTSV = async () => {
    if (!plan || !_exp) return;
    try {
      const tsv = _exp.formatPlanAsTSV(planData);
      await navigator.clipboard.writeText(tsv);
      setCopyConfirm(true);
      setTimeout(() => setCopyConfirm(false), 2500);
    } catch (e) {
      if (typeof console !== 'undefined') console.warn('[PathBridge] clipboard copy failed', e);
    }
  };

  const handleDownloadCSV = () => {
    if (!plan || !_exp) return;
    const csv = _exp.formatPlanAsCSV(planData);
    const filename = _exp.defaultExportFilename(planData, { province, score }, 'csv');
    _downloadBlob(csv, filename, 'text/csv;charset=utf-8;');
  };

  const handleDownloadJSON = () => {
    if (!plan || !_exp) return;
    const profile = { province, year, score, rank, subjects };
    const summary = planData && planData.summary ? planData.summary : {};
    const json = _exp.formatPlanAsJSON(planData, { profile, summary });
    const filename = _exp.defaultExportFilename(planData, { province, score }, 'json');
    _downloadBlob(json, filename, 'application/json');
  };

  // M43.2 — print / save PDF handler
  const handlePrintPDF = () => {
    if (!plan || !_exp || typeof _exp.formatPlanAsPrintHTML !== 'function') return;
    const profile = { province, year, score, rank, subjects, majorCategory };
    const summary = planData && planData.summary ? planData.summary : {};
    const html = _exp.formatPlanAsPrintHTML(planData, profile, summary);
    const w = window.open('', '_blank');
    if (!w) { alert('请允许弹窗以打印 PDF'); return; }
    w.document.open();
    w.document.write(html);
    w.document.close();
    setTimeout(() => { try { w.print(); } catch (_) {} }, 300);
  };

  // M41.3 — save draft handler
  const handleSaveDraft = async () => {
    if (!plan || plan.length === 0) return;
    const subjectStr = Array.isArray(subjects) ? subjects.join('') : (subjects || '');
    const dateStr = new Date().toISOString().slice(0, 10);
    const provinceName2 = (window.I18N && window.I18N.zh && window.I18N.zh.gk && window.I18N.zh.gk.simulate && window.I18N.zh.gk.simulate.provinces && window.I18N.zh.gk.simulate.provinces[province]) || province || '海南';
    const defaultName = `${provinceName2} / ${score || ''} 分 / ${subjectStr} / ${dateStr}`;
    const name = window.prompt ? window.prompt(lang === 'zh' ? '请输入方案名称' : 'Enter plan name', defaultName) : defaultName;
    if (name === null) return; // user cancelled prompt

    setSaveBanner('saving');
    try {
      const summary = planData && planData.summary ? planData.summary : {};
      const res = await fetch('/api/user/reports?op=plan-drafts-create', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: (name && name.trim()) || defaultName,
          profile: { province, year, score, rank, subjects },
          summary,
          plan,
        }),
      });
      if (res.status === 409) {
        setSaveBanner('limit');
        setTimeout(() => setSaveBanner(null), 4000);
        return;
      }
      if (res.ok) {
        setSaveBanner('ok');
        setTimeout(() => setSaveBanner(null), 3000);
        return;
      }
      setSaveBanner('error');
      setTimeout(() => setSaveBanner(null), 4000);
    } catch (_e) {
      setSaveBanner('error');
      setTimeout(() => setSaveBanner(null), 4000);
    }
  };

  // National rollout — render for any province PBPlanRules knows (all 30 in-scope).
  // Falls back to PROVINCE_SLOTS only if PBPlanRules failed to load.
  if (!province || (!getAdmissionRules(province) && !PROVINCE_SLOTS[province])) return null;

  const planData = planState && planState.data;
  const plan = planData ? planData.plan : null;
  const rules = planData ? planData.rules : null;

  return (
    <div className="pb-card" style={{ padding: 0, overflow: 'hidden' }} data-testid="gaokao-simulate-section">
      <div style={{ padding: '24px 28px 16px', borderBottom: '1px solid var(--pb-border)' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap', marginBottom: 16 }}>
          <div style={{ flex: 1, minWidth: 240 }}>
            <div className="pb-eyebrow" style={{ marginBottom: 8, color: 'var(--pb-navy-700)' }}>
              {sim && sim.eyebrow}
            </div>
            <h4 style={{ fontFamily: 'var(--pb-serif)', fontSize: 20, fontWeight: 600, margin: '0 0 6px' }}>
              {lang === 'zh'
                ? `生成完整志愿方案（${provinceName}${currentBatch ? ' · ' + getBatchName(currentBatch) : ''} 共 ${totalSlots !== null ? totalSlots + (sim && sim.slotsLabel || '槽') : (sim && sim.perSchool || '按校')}）`
                : `Generate full ${provinceName} plan (${totalSlots !== null ? totalSlots + ' slots' : 'per-school'})`}
            </h4>
            <p style={{ fontSize: 12, color: 'var(--pb-fg-muted)', margin: 0, lineHeight: 1.55 }}>
              {sim && (typeof sim.desc === 'function' ? sim.desc(gatedAnon) : sim.desc)}
            </p>
            {/* 报考方式 explainer — adapts to each province's 选科模式/投档单位/志愿数/调剂 */}
            {/* QA 2026-06-10 — 志愿数/调剂 chips 随 activeBatch 切换：专项 tab 上
                显示"本科普通批 30 个志愿"会误导；qjj（slots=null）保持普通批兜底 */}
            {normalBatchRule && (() => {
              const factsRule = (activeBatch !== 'normal' && currentBatch && currentBatch.slots != null)
                ? {
                    nameZh: getBatchName(currentBatch),
                    slots: currentBatch.slots,
                    majorsPerSlot: currentBatch.majorsPerSlot || normalBatchRule.majorsPerSlot,
                    adjustOption: currentBatch.adjustOption != null ? currentBatch.adjustOption : normalBatchRule.adjustOption,
                  }
                : normalBatchRule;
              return (
              <div data-testid="province-admission-explainer" style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 10 }}>
                {[
                  (lang === 'zh' ? '选科模式：' : 'Mode: ') + admissionRules.reformMode,
                  (lang === 'zh' ? '投档单位：' : 'Unit: ') + (lang === 'en' ? (admissionRules.voluntaryModelEn || (window.PBPlanRules && window.PBPlanRules.voluntaryModelEnOf && window.PBPlanRules.voluntaryModelEnOf(admissionRules.voluntaryModel)) || admissionRules.voluntaryModel) : admissionRules.voluntaryModel),
                  (lang === 'zh'
                    ? `${factsRule.nameZh} ${factsRule.slots} 个志愿` + (factsRule.majorsPerSlot > 1 ? `（每志愿 ${factsRule.majorsPerSlot} 个专业）` : '')
                    : `${factsRule.slots} slots` + (factsRule.majorsPerSlot > 1 ? ` x ${factsRule.majorsPerSlot} majors` : '')),
                  (lang === 'zh'
                    ? '专业服从调剂：' + (factsRule.adjustOption ? '有' : '无（直投专业）')
                    : 'Major reassignment: ' + (factsRule.adjustOption ? 'yes' : 'no')),
                ].map((t, i) => (
                  <span key={i} style={{ padding: '3px 10px', fontSize: 11, borderRadius: 20, background: 'var(--pb-surface-alt)', border: '1px solid var(--pb-border)', color: 'var(--pb-fg-muted)', whiteSpace: 'nowrap' }}>
                    {t}
                  </span>
                ))}
              </div>
              );
            })()}
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            {plan && (
              <button className="pb-btn pb-btn-link pb-btn-sm" onClick={() => setCollapsed((c) => !c)}>
                {collapsed ? (sim && sim.expand) : (sim && sim.collapse)}
              </button>
            )}
            <button
              className="pb-btn pb-btn-primary"
              style={{ padding: '10px 20px', fontSize: 13, whiteSpace: 'nowrap' }}
              onClick={onGenerate}
              disabled={planState && planState.loading}
              data-testid="gaokao-simulate-button"
            >
              {planState && planState.loading
                ? (sim && sim.generating)
                : (() => {
                    // A3 — HI 普通批 uses the prominent 一键智能生成志愿表 label
                    const isOneClick = activeBatch === 'normal' && ONE_CLICK_SLATE_PROVINCES.has(province);
                    if (isOneClick) {
                      return plan
                        ? ((sim && sim.oneClickSmartRegen) || '重新智能生成志愿表')
                        : ((sim && sim.oneClickSmart) || '一键智能生成志愿表');
                    }
                    // Hybrid estimate tier — distinct safe label (must NOT use 模拟方案/测算报告)
                    const _capBtn = window.PBProvinceCapabilities
                      ? window.PBProvinceCapabilities.provinceCapabilities(province)
                      : 'rank-only';
                    if (activeBatch === 'normal' && _capBtn === 'estimate') {
                      return plan
                        ? ((sim && sim.estimateRegen) || '重新生成参考方案')
                        : ((sim && sim.estimateGenerate) || '生成参考方案');
                    }
                    return plan
                      ? (sim && sim.regenerate)
                      : (gatedAnon
                        ? ((sim && sim.generateFree) || (sim && sim.generate))
                        : ((sim && sim.generateFull) || (sim && sim.generate)));
                  })()}
            </button>
          </div>
        </div>

        {/* ── Batch segmented control (H1.2) ── */}
        {batches.length > 1 && (
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }} data-testid="batch-segmented-control">
            {batches.map((b) => (
              <button
                key={b.code}
                data-testid={`batch-tab-${b.code}`}
                onClick={() => onBatchSwitch(b.code, { autoGenerate: true })}
                style={{
                  padding: '5px 14px',
                  fontSize: 12,
                  fontWeight: activeBatch === b.code ? 700 : 400,
                  borderRadius: 20,
                  border: '1px solid ' + (activeBatch === b.code ? 'var(--pb-navy-700)' : 'var(--pb-border)'),
                  background: activeBatch === b.code ? 'var(--pb-navy-700)' : 'transparent',
                  color: activeBatch === b.code ? '#fff' : 'var(--pb-fg-muted)',
                  cursor: 'pointer',
                  whiteSpace: 'nowrap',
                  transition: 'all 0.15s',
                }}
              >
                {getBatchName(b)}
                {b.slots != null
                  ? <span style={{ marginLeft: 4, opacity: 0.7, fontWeight: 400 }}>{b.slots}{sim && sim.slotsLabel}</span>
                  : <span style={{ marginLeft: 4, opacity: 0.7, fontWeight: 400 }}>{sim && sim.perSchool}</span>}
              </button>
            ))}
          </div>
        )}
      </div>

      {planState && planState.error && (
        <div style={{ padding: '12px 28px', background: '#fdecea', color: '#9b1c1c', fontSize: 13 }}>
          {planState.error}
        </div>
      )}

      {/* M43.1 — Draft hydration loading indicator */}
      {draftHydrationLoading && (
        <div style={{ padding: '10px 28px', background: '#eff6ff', color: '#1e40af', fontSize: 13 }}>
          {lang === 'zh' ? '正在加载草稿…' : 'Loading draft…'}
        </div>
      )}

      {/* M43.1 — Draft hydration banner: "📂 已加载草稿: <name>" + dismiss button */}
      {draftHydration && !draftHydrationLoading && (
        <div
          data-testid="draft-hydration-banner"
          style={{
            padding: '10px 28px',
            background: '#f0f9ff',
            borderBottom: '1px solid #bae6fd',
            display: 'flex',
            alignItems: 'center',
            gap: 12,
            fontSize: 13,
            color: '#0369a1',
          }}
        >
          <span style={{ flex: 1 }}>
            📂 {lang === 'zh' ? '已加载草稿：' : 'Loaded draft: '}
            <strong>{draftHydration.name}</strong>
          </span>
          <button
            type="button"
            data-testid="draft-hydration-clear-btn"
            onClick={() => {
              setPlanState(null);
              if (typeof onDraftHydrationClear === 'function') onDraftHydrationClear();
            }}
            style={{ background: 'none', border: '1px solid #bae6fd', borderRadius: 4, cursor: 'pointer', fontSize: 12, padding: '2px 10px', color: '#0369a1', whiteSpace: 'nowrap' }}
          >
            {lang === 'zh' ? '取消加载' : 'Dismiss'}
          </button>
        </div>
      )}

      {/* Placeholder banner for non-real-data batches */}
      {planState && planState.placeholder && (
        <div style={{ padding: '28px', textAlign: 'center', color: 'var(--pb-fg-muted)' }} data-testid="batch-placeholder-banner">
          <div style={{ fontSize: 32, marginBottom: 12 }}>🏗</div>
          <div style={{ fontFamily: 'var(--pb-serif)', fontSize: 16, fontWeight: 600, marginBottom: 8, color: 'var(--pb-navy-700)' }}>
            {planState.placeholderMessage}
          </div>
          <div style={{ fontSize: 12, lineHeight: 1.6 }}>
            {sim && sim.placeholderReady}
          </div>
        </div>
      )}

      {/* Wave 22-A — early/special batch schools panel */}
      {(batchPanelLoading || (planState && planState.batchPanel)) && (
        <div data-testid="batch-schools-panel">
          {batchPanelLoading && (
            <div style={{ padding: '28px', textAlign: 'center', color: 'var(--pb-fg-muted)', fontSize: 13 }}>
              {lang === 'zh' ? '加载院校数据…' : 'Loading batch school data…'}
            </div>
          )}
          {!batchPanelLoading && batchPanelData && batchPanelData.length > 0 && (
            <div>
              <div style={{ padding: '10px 28px', background: 'var(--pb-surface-alt)', fontSize: 11, color: 'var(--pb-fg-muted)', borderBottom: '1px solid var(--pb-border)' }}>
                {(() => {
                  const isReal = batchPanelMeta && typeof batchPanelMeta.source === 'string' && batchPanelMeta.source.indexOf('real-public') === 0;
                  const ctrlLine = batchPanelMeta && batchPanelMeta.controlLine != null ? batchPanelMeta.controlLine : null;
                  const batchLabel = activeBatch === 'early' ? (lang === 'zh' ? '本科提前批' : 'Early Admission') : (lang === 'zh' ? '国家专项计划' : 'National Special Program');
                  if (lang === 'zh') {
                    const ctrlSuffix = (isReal && ctrlLine != null) ? `，控制线 ${ctrlLine} 分` : '';
                    const qualifier = isReal ? '已核实真实数据' : '样本数据';
                    return `${batchLabel}候选院校（${batchPanelData.length} 所，${qualifier}${ctrlSuffix}）— 分数线仅供参考`;
                  } else {
                    const ctrlSuffix = (isReal && ctrlLine != null) ? `, control line ${ctrlLine}` : '';
                    const qualifier = isReal ? 'verified real data' : 'sample data';
                    return `${batchLabel} candidate schools (${batchPanelData.length}, ${qualifier}${ctrlSuffix}) — score lines are reference only`;
                  }
                })()}
              </div>
              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
                <thead>
                  <tr style={{ background: 'var(--pb-surface-alt)' }}>
                    <th style={{ textAlign: 'left', padding: '8px 28px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '院校' : 'School'}
                    </th>
                    <th style={{ textAlign: 'left', padding: '8px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '专业' : 'Major'}
                    </th>
                    <th style={{ textAlign: 'right', padding: '8px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '投档线' : 'Cutoff'}
                    </th>
                    <th style={{ textAlign: 'right', padding: '8px 28px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '计划数' : 'Plan'}
                    </th>
                  </tr>
                </thead>
                <tbody>
                  {batchPanelData
                    .filter((s) => !score || (s.majors && s.majors.some((m) => m.cutoff <= score + 40)))
                    .sort((a, b) => {
                      const aCut = a.majors && a.majors[0] ? a.majors[0].cutoff : 0;
                      const bCut = b.majors && b.majors[0] ? b.majors[0].cutoff : 0;
                      return bCut - aCut;
                    })
                    .map((s, i) => {
                      const firstMajor = s.majors && s.majors[0];
                      const eligible = score && firstMajor && score >= firstMajor.cutoff;
                      return (
                        <tr key={s.schoolCode || i} data-testid={`batch-school-row-${s.schoolCode || i}`}
                          style={{ borderTop: i > 0 ? '1px solid var(--pb-border)' : 'none', background: eligible ? 'rgba(59,130,246,0.03)' : 'transparent' }}>
                          <td style={{ padding: '12px 28px' }}>
                            <div style={{ fontWeight: 600, fontFamily: 'var(--pb-serif)', fontSize: 14 }}>
                              {s.schoolName}
                            </div>
                            {eligible && (
                              <span style={{ fontSize: 10, color: '#2563eb', fontWeight: 600 }}>
                                {lang === 'zh' ? '✓ 分数符合' : '✓ Score meets threshold'}
                              </span>
                            )}
                          </td>
                          <td style={{ padding: '12px 8px', maxWidth: 260 }}>
                            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                              {(s.majors || []).map((m, j) => (
                                <span key={j} style={{ padding: '2px 6px', fontSize: 10, background: 'var(--pb-surface-alt)', borderRadius: 4, border: '1px solid var(--pb-border)', whiteSpace: 'nowrap' }}>
                                  {m.name}
                                </span>
                              ))}
                            </div>
                          </td>
                          <td style={{ padding: '12px 8px', textAlign: 'right', fontFamily: 'var(--pb-mono)', fontSize: 13, fontWeight: 600, color: eligible ? 'var(--pb-success)' : 'var(--pb-fg-muted)' }}>
                            {firstMajor ? firstMajor.cutoff + '+' : '—'}
                          </td>
                          <td style={{ padding: '12px 28px', textAlign: 'right', fontFamily: 'var(--pb-mono)', fontSize: 11, color: 'var(--pb-fg-muted)' }}>
                            {(s.majors || []).reduce((sum, m) => sum + (m.plan || 0), 0)}
                          </td>
                        </tr>
                      );
                    })}
                </tbody>
              </table>
              <div style={{ padding: '10px 28px', background: 'var(--pb-surface-alt)', fontSize: 11, color: 'var(--pb-fg-muted)', borderTop: '1px solid var(--pb-border)' }}
                data-testid="batch-panel-source-footer">
                {(() => {
                  const src = batchPanelMeta && batchPanelMeta.source;
                  const isReal = src && src.indexOf('real-public') === 0;
                  const yr = (batchPanelMeta && batchPanelMeta.year) || (year || 2025);
                  if (lang === 'zh') {
                    return isReal
                      ? `数据来源：海南省考试局 ${yr} 年国家专项计划真实公开数据（已核验）。分数线为当年实录，以招简为准。`
                      : '数据来源：海南考试局公开数据（样本，完整 ETL 待补）。分数线为历史参考，以当年招简为准。';
                  } else {
                    return isReal
                      ? `Source: Hainan Provincial Exam Bureau ${yr} National Special Program verified real public data. Score lines are official records.`
                      : 'Source: Hainan Exam Bureau public data (sample, full ETL pending). Score lines are historical reference only.';
                  }
                })()}
              </div>
            </div>
          )}
          {!batchPanelLoading && (!batchPanelData || batchPanelData.length === 0) && (
            <div style={{ padding: '28px', textAlign: 'center', color: 'var(--pb-fg-muted)', fontSize: 13 }}>
              {lang === 'zh' ? '暂无该批次数据' : 'No data available for this batch'}
            </div>
          )}
        </div>
      )}

      {/* Wave 9-D — QJJ panel: 强基计划资格校列表 */}
      {(qjjLoading || (planState && planState.qjjPanel)) && (
        <div data-testid="qjj-panel">
          {qjjLoading && (
            <div style={{ padding: '28px', textAlign: 'center', color: 'var(--pb-fg-muted)', fontSize: 13 }}>
              {lang === 'zh' ? '加载强基计划数据…' : 'Loading Top-Talent program data…'}
            </div>
          )}
          {!qjjLoading && qjjData && qjjData.length > 0 && (
            <div>
              <div style={{ padding: '10px 28px', background: 'var(--pb-surface-alt)', fontSize: 11, color: 'var(--pb-fg-muted)', borderBottom: '1px solid var(--pb-border)' }}>
                {lang === 'zh'
                  ? `强基计划资格校（${qjjData.length} 所）— 须参加各校单独考核，分数线仅供参考`
                  : `Top-Talent Program eligible schools (${qjjData.length}) — separate school exam required`}
              </div>
              <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
                <thead>
                  <tr style={{ background: 'var(--pb-surface-alt)' }}>
                    <th style={{ textAlign: 'left', padding: '8px 28px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '院校' : 'School'}
                    </th>
                    <th style={{ textAlign: 'left', padding: '8px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '强基专业' : 'QJJ Majors'}
                    </th>
                    <th style={{ textAlign: 'right', padding: '8px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '参考分（估算）' : 'Ref Score (est.)'}
                    </th>
                    <th style={{ textAlign: 'right', padding: '8px 28px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.07em' }}>
                      {lang === 'zh' ? '考核窗口' : 'Exam Window'}
                    </th>
                  </tr>
                </thead>
                <tbody>
                  {qjjData
                    .filter((s) => !score || s.requiredScore <= score + 30)
                    .sort((a, b) => b.requiredScore - a.requiredScore)
                    .map((s, i) => {
                      const eligible = score && score >= s.requiredScore;
                      const borderTop = i > 0 ? '1px solid var(--pb-border)' : 'none';
                      return (
                        <tr key={s.schoolCode} data-testid={`qjj-row-${s.schoolCode}`}
                          style={{ borderTop, background: eligible ? 'rgba(59,130,246,0.03)' : 'transparent' }}>
                          <td style={{ padding: '12px 28px' }}>
                            <div style={{ fontWeight: 600, fontFamily: 'var(--pb-serif)', fontSize: 14 }}>
                              {s.schoolName}
                            </div>
                            {eligible && (
                              <span style={{ fontSize: 10, color: '#2563eb', fontWeight: 600 }}>
                                {lang === 'zh' ? '✓ 分数符合' : '✓ Score meets threshold'}
                              </span>
                            )}
                          </td>
                          <td style={{ padding: '12px 8px', maxWidth: 260 }}>
                            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                              {(s.qjjMajors || []).map((m, j) => (
                                <span key={j} style={{ padding: '2px 6px', fontSize: 10, background: 'var(--pb-surface-alt)', borderRadius: 4, border: '1px solid var(--pb-border)', whiteSpace: 'nowrap' }}>
                                  {m}
                                </span>
                              ))}
                            </div>
                          </td>
                          <td style={{ padding: '12px 8px', textAlign: 'right', fontFamily: 'var(--pb-mono)', fontSize: 13, fontWeight: 600, color: eligible ? 'var(--pb-success)' : 'var(--pb-fg-muted)' }}>
                            {s.requiredScore}+
                          </td>
                          <td style={{ padding: '12px 28px', textAlign: 'right', fontFamily: 'var(--pb-mono)', fontSize: 11, color: 'var(--pb-fg-muted)' }}>
                            {s.examWindow}
                          </td>
                        </tr>
                      );
                    })}
                </tbody>
              </table>
              <div style={{ padding: '10px 28px', background: 'var(--pb-surface-alt)', fontSize: 11, color: 'var(--pb-fg-muted)', borderTop: '1px solid var(--pb-border)' }}>
                {/* QA 2026-06-11 项6 — requiredScore 无行级官方来源，不得宣称简章出处 */}
                {lang === 'zh'
                  ? '开设方向来自教育部公开政策整理；表中参考分为内部估算（未经官方核验），不代表强基入围线，请以各校当年简章与入围公告为准。'
                  : "Program directions compiled from public MOE policy; score thresholds are internal estimates (not source-verified) — refer to each school's official brochure and shortlist announcements."}
              </div>
            </div>
          )}
          {!qjjLoading && (!qjjData || qjjData.length === 0) && (
            <div style={{ padding: '28px', textAlign: 'center', color: 'var(--pb-fg-muted)', fontSize: 13 }}>
              {lang === 'zh' ? '暂无强基计划数据' : 'No Top-Talent program data available'}
            </div>
          )}
        </div>
      )}

      {plan && !collapsed && (
        <div>
          {/* Hybrid estimate tier — prominent non-official banner (A9 hybrid-track-rollout) */}
          {isEstimatePlan && (
            <div
              data-testid="estimate-plan-notice"
              style={{
                padding: '10px 28px',
                background: 'rgba(251,191,36,0.12)',
                borderTop: '3px solid #f59e0b',
                borderBottom: '1px solid var(--pb-border)',
                fontSize: 12,
                color: '#92400e',
                lineHeight: 1.6,
                display: 'flex',
                alignItems: 'flex-start',
                gap: 8,
              }}
            >
              <span style={{ fontWeight: 700, whiteSpace: 'nowrap', fontSize: 13 }}>⚠ 非官方 · 仅供参考</span>
              <span>
                {lang === 'zh'
                  ? '本方案基于公开历史分数线估算，非官方正式投档方案；概率为分差粗估，不代表实际录取率。请以官方渠道（省考试院、学校招生章程）为准，谨慎参考。'
                  : 'This plan is estimated from public historical cutoff scores — not an official admissions slate. Probabilities are rough score-gap estimates and do not represent actual admission rates. Refer to official provincial exam authority channels for actual filing.'}
              </span>
            </div>
          )}
          {/* Cross-verified tier — positive provenance banner (M3). The province's 2025
              投档线 were corroborated by ≥2 independent sources; probability uses the
              official 一分一段 位次换算. NOT the 仅供参考 estimate framing. */}
          {isVerifiedPlan && !isEstimatePlan && (
            <div
              data-testid="verified-plan-notice"
              style={{
                padding: '10px 28px',
                background: 'rgba(16,185,129,0.10)',
                borderTop: '3px solid #10b981',
                borderBottom: '1px solid var(--pb-border)',
                fontSize: 12,
                color: '#065f46',
                lineHeight: 1.6,
                display: 'flex',
                alignItems: 'flex-start',
                gap: 8,
              }}
            >
              <span style={{ fontWeight: 700, whiteSpace: 'nowrap', fontSize: 13 }}>✓ 多源交叉核验</span>
              <span>
                {lang === 'zh'
                  ? '本方案基于 2025 年真实投档线（经 ≥2 个独立来源交叉核验，多为官方公示），概率由官方一分一段位次换算。正式填报仍以各省考试院当年系统与招生计划为准。'
                  : 'This plan is based on 2025 real admission scores (cross-verified against ≥2 independent sources, mostly official publications); probabilities are derived via the official score-to-rank (一分一段) table. Always confirm final filing through your provincial exam authority.'}
              </span>
            </div>
          )}
          {rules && (
            <div style={{ padding: '10px 28px', background: 'var(--pb-surface-alt)', fontSize: 11, color: 'var(--pb-fg-muted)', display: 'flex', gap: 24, flexWrap: 'wrap' }}>
              <span>{sim && sim.reformMode}: <strong>{rules.reformMode}</strong></span>
              <span>{sim && sim.batch}: <strong>{rules.batchNameZh}</strong></span>
              {/* M6 — 投档单位 pill: shown consistently for all provinces via rules.candidateUnit */}
              {rules.voluntaryModel && (
                <span>
                  {lang === 'zh' ? '投档单位：' : 'Unit: '}
                  <strong>{lang === 'en' ? (rules.voluntaryModelEn || rules.voluntaryModel) : rules.voluntaryModel}</strong>
                </span>
              )}
              <span>{sim && sim.groups}: <strong>{rules.slots}</strong></span>
              <span>{sim && sim.majorsPerGroup}: <strong>{rules.majorsPerSlot}</strong></span>
              <span>{sim && sim.rms}: <strong>{rules.slotAlloc ? `${rules.slotAlloc.reach}/${rules.slotAlloc.match}/${rules.slotAlloc.safety}` : '—'}</strong></span>
              {majorCategory && majorCategory !== 'default'
                && plan.some((slot) => Number(slot && slot._majorRelevance) > 0
                  && displayableMajorChoices(slot).length > 0) && (
                <span data-testid="plan-major-fit-legend" style={{ color: 'var(--pb-gold-700)', fontWeight: 600 }}>
                  {lang === 'zh' ? '★ 已按所选专业方向优先排序并标记' : '★ majors matching your preference are prioritized'}
                </span>
              )}
              {plan.some((slot) => slot && slot._prefHit) && (
                <span data-testid="plan-pref-hit-legend" style={{ color: 'var(--pb-gold-700)', fontWeight: 600 }}>
                  {lang === 'zh' ? '★ 已按就业/保研真实数据排序并标记' : '★ ranked by verified employment/grad-rate data'}
                </span>
              )}
              {gatesOn && planState.anonymous && (
                <span style={{ color: 'var(--pb-danger)', fontWeight: 600 }}>
                  {sim && sim.anonPreview}
                </span>
              )}
            </div>
          )}
          {/* #436/#443 — 边界诚实横幅：引擎已产出 boundaryNote（线下全冲 /
              顶部全保），此前从未在 UI 渲染。线下考生附一键切换专科批。 */}
          {planData && planData.summary && planData.summary.boundaryNote && (
            <div
              data-testid="plan-boundary-note"
              style={{
                padding: '12px 28px', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
                background: 'rgba(234,179,8,0.10)', borderTop: '1px solid var(--pb-border)',
                fontSize: 13, lineHeight: 1.6,
              }}
            >
              <span style={{ color: 'var(--pb-gold-700)', fontWeight: 600 }}>⚠</span>
              <span style={{ flex: 1, minWidth: 240 }}>{planData.summary.boundaryNote}</span>
              {planData.summary.boundaryMode === 'below-benke-line'
                && activeBatch !== 'vocational'
                && batches.some((b) => b.code === 'vocational') && (
                <button
                  type="button"
                  className="pb-btn pb-btn-primary pb-btn-sm"
                  data-testid="plan-switch-vocational"
                  onClick={() => onBatchSwitch('vocational', { autoGenerate: true })}
                  style={{ whiteSpace: 'nowrap' }}
                >
                  {lang === 'zh' ? '切换到 高职(专科)批 →' : 'Switch to Vocational Batch →'}
                </button>
              )}
            </div>
          )}
          {/* Province calibration-status notice — distinguishes data-verified-but-backtest-pending
               provinces from HI (data-verified + 2025 backtest complete). Required by
               data-authenticity contract: never claim GD/ZJ/SD are backtested.
               GD: 院校专业组级 (school_group). ZJ/SD: 专业平行 (major_parallel).
               Added 2026-06-13 (GD), 2026-06-14 (ZJ/SD, Phase 2a-B). */}
          {province === 'GD' && plan && (
            <div
              data-testid="gd-calibration-notice"
              style={{
                padding: '10px 28px',
                background: 'rgba(59,130,246,0.06)',
                borderTop: '1px solid var(--pb-border)',
                fontSize: 12,
                color: 'var(--pb-fg-muted)',
                lineHeight: 1.6,
              }}
            >
              {lang === 'zh'
                ? '广东方案基于 2024 年官方正式投档数据（院校专业组级，eea.gd.gov.cn）；逐专业明细（专业明细待核验）与再选选科要求（选科待核）以官方招生专业目录为准；概率校准待 2025 官方投档回测。'
                : 'GD plan is based on 2024 official school-group (院校专业组) admissions data (eea.gd.gov.cn). Per-major details (专业明细待核验) and elective subject requirements (选科待核) are subject to the official enrollment catalog. Probability calibration pending 2025 official admissions backtest.'}
            </div>
          )}
          {/* ZJ/SD major_parallel calibration notice (Phase 2a-B, 2026-06-14).
              Real major names shown; 选科待核 honest; probability calibration pending cross-year backtest. */}
          {(province === 'ZJ' || province === 'SD') && plan && (
            <div
              data-testid="mp-calibration-notice"
              style={{
                padding: '10px 28px',
                background: 'rgba(59,130,246,0.06)',
                borderTop: '1px solid var(--pb-border)',
                fontSize: 12,
                color: 'var(--pb-fg-muted)',
                lineHeight: 1.6,
              }}
            >
              {lang === 'zh'
                ? `${provinceName}方案基于 2025 年官方逐专业平行投档数据；再选选科要求以官方招生专业目录为准（待核）；概率校准待跨年官方回测。`
                : `${provinceName} plan is based on 2025 official per-major parallel admissions data. Elective subject requirements are subject to the official enrollment catalog (pending verification). Probability calibration pending cross-year official admissions backtest.`}
            </div>
          )}
          {(() => {
            // M2a/M2b: apply left-console filters (view-only; plan/engine/export unchanged)
            // M2b — enrich slots with schoolTags from slot.tags (already set by plan engine;
            // filterPlanSlots expects schoolTags; this is client-side display metadata only).
            const _planWithTags = plan.map((slot) =>
              slot.schoolTags ? slot : { ...slot, schoolTags: Array.isArray(slot.tags) ? slot.tags : [] }
            );
            const _pfActive = window.PBPlanFilter && planFilters && (planFilters.buckets?.length || planFilters.query || planFilters.tiers?.length);
            const _pf = _pfActive ? window.PBPlanFilter.filterPlanSlots(_planWithTags, planFilters) : null;
            const visibleSlots = _pf ? _pf.rows : _planWithTags;
            return (
              <React.Fragment>
                {_pf && (
                  <div
                    data-testid="gk-plan-filter-count"
                    style={{ fontSize: 12, color: 'var(--pb-fg-muted)', padding: '6px 28px', background: 'var(--pb-surface-alt)', borderBottom: '1px solid var(--pb-border)' }}
                  >
                    {lang === 'zh'
                      ? `已筛 ${_pf.counts.shown} / 共 ${_pf.counts.total}`
                      : `Showing ${_pf.counts.shown} / ${_pf.counts.total}`}
                  </div>
                )}
                <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
                  <thead>
                    <tr style={{ background: 'var(--pb-surface-alt)' }}>
                      <th style={{ textAlign: 'left', padding: '10px 28px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>#</th>
                      <th style={{ textAlign: 'left', padding: '10px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{sim && sim.tierCol}</th>
                      <th style={{ textAlign: 'left', padding: '10px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{sim && sim.schoolCol}</th>
                      <th style={{ textAlign: 'left', padding: '10px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{sim && sim.majorsCol}</th>
                      <th style={{ textAlign: 'right', padding: '10px 8px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{sim && sim.probCol}</th>
                      <th style={{ textAlign: 'right', padding: '10px 28px', fontSize: 11, fontWeight: 600, color: 'var(--pb-fg-muted)', textTransform: 'uppercase', letterSpacing: '0.08em' }}>{sim && sim.cutoffCol}</th>
                    </tr>
                  </thead>
                  <tbody>
                    {_pf && _pf.rows.length === 0 && (
                      <tr data-testid="gk-plan-filter-empty">
                        <td colSpan={6} style={{ padding: '24px 28px', textAlign: 'center', color: 'var(--pb-fg-muted)', fontSize: 13 }}>
                          {lang === 'zh' ? '当前筛选无匹配院校，点此清空' : 'No schools match — clear filters'}
                        </td>
                      </tr>
                    )}
                    {visibleSlots.map((slot, i) => {
                const verifiedMajorChoices = displayableMajorChoices(slot);
                const hasRawMajorChoices = Array.isArray(slot.majorChoices) && slot.majorChoices.length > 0;
                return (
                <tr key={i} style={{ borderTop: '1px solid var(--pb-border)' }} data-testid={`gaokao-plan-row-${i}`}>
                  <td style={{ padding: '14px 28px', fontFamily: 'var(--pb-mono)', fontSize: 12, color: 'var(--pb-fg-muted)' }}>
                    {String(slot.slotIdx).padStart(2, '0')}
                  </td>
                  <td style={{ padding: '14px 8px' }}>
                    {(() => {
                      const hasProb = slot.probability != null;
                      const fk = hasProb ? fineKeyForSlotProb(slot.probability) : null;
                      const c = hasProb ? (FINE_TIER_COLOR_PLAN[fk] || 'var(--pb-fg-muted)') : (BUCKET_COLOR_PLAN[slot.bucket] || 'var(--pb-fg-muted)');
                      const label = hasProb ? (FINE_LABEL_PLAN[fk] || slot.bucket) : (BUCKET_LABEL[slot.bucket] || slot.bucket || '—');
                      return (
                        <span style={{ display: 'inline-flex', padding: '2px 8px', fontSize: 11, fontWeight: 700, borderRadius: 4, background: c + '18', color: c }}>
                          {label}
                        </span>
                      );
                    })()}
                  </td>
                  <td style={{ padding: '14px 8px' }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                      <div style={{ fontWeight: 600, fontFamily: 'var(--pb-serif)', fontSize: 14, color: slot.isPlaceholder ? 'var(--pb-fg-muted)' : 'inherit' }}>
                        {slot._prefHit && (
                          <span
                            data-testid="plan-pref-hit-star"
                            title={lang === 'zh' ? '匹配所选就业/保研偏好（已核验数据）' : 'Matches your employment/grad-rate preference (verified data)'}
                            style={{ color: 'var(--pb-gold-700)', marginRight: 4, fontStyle: 'normal' }}
                          >★</span>
                        )}
                        {slot.schoolName}
                      </div>
                      {/* Wave 49 M49.4 — PK toggle */}
                      {!slot.isPlaceholder && cpD && (() => {
                        const pkCode = getPkCode(slot);
                        if (!pkCode) return null;
                        const picked = pickedCodes.includes(pkCode);
                        const atMax = pickedCodes.length >= PK_MAX && !picked;
                        return (
                          <button
                            type="button"
                            onClick={() => togglePickedCode(pkCode)}
                            disabled={atMax}
                            data-testid={`gaokao-pk-toggle-${i}`}
                            title={atMax ? cpD.cartHintMax : ''}
                            style={{
                              padding: '2px 8px', fontSize: 11, fontWeight: 600, borderRadius: 4,
                              border: '1px solid ' + (picked ? 'var(--pb-success)' : 'var(--pb-border)'),
                              background: picked ? 'var(--pb-success)' : 'transparent',
                              color: picked ? '#fff' : (atMax ? 'var(--pb-fg-muted)' : 'var(--pb-fg-muted)'),
                              cursor: atMax ? 'not-allowed' : 'pointer',
                              opacity: atMax ? 0.5 : 1,
                              whiteSpace: 'nowrap',
                            }}
                          >
                            {picked ? cpD.pkButtonRemove : cpD.pkButtonAdd}
                          </button>
                        );
                      })()}
                    </div>
                    {slot.tier && (
                      <div style={{ fontSize: 11, color: 'var(--pb-fg-muted)', marginTop: 2 }}>
                        {/* M6 — show majorGroup for school_group (e.g. 专业组 01), slotLabel for parallel/school/trad */}
                        {slot.tier}{(slot.majorGroup || slot.slotLabel) ? ' · ' + (slot.majorGroup || slot.slotLabel) : ''}
                        {/* QA 2026-06-11 项3 — 组级计划数：引擎仅对真实来源行下发 groupPlan */}
                        {slot.groupPlan != null && (
                          <span data-testid="slot-group-plan" style={{ marginLeft: 6 }}>
                            · {lang === 'zh' ? `招 ${slot.groupPlan} 人` : `${slot.groupPlan} seats`}
                          </span>
                        )}
                      </div>
                    )}
                    {/* #429 L3b — 选科待核：GD 投档线源仅含首选门槛，再选选科要求未知，
                        显式标记而非默认"可报"，不误导没选化学的考生。 */}
                    {slot.subjReqPending && !slot.isPlaceholder && (
                      <div style={{ marginTop: 3 }}>
                        <span
                          data-testid="slot-subjreq-pending"
                          title={slot.subjReqNote || (lang === 'zh'
                            ? '该专业组再选科目要求以官方招生专业目录为准'
                            : 'Elective-subject requirement per the official catalog')}
                          style={{
                            padding: '1px 6px', fontSize: 10, borderRadius: 4, whiteSpace: 'nowrap',
                            background: 'var(--pb-surface-alt)', border: '1px dashed var(--pb-border)',
                            color: 'var(--pb-fg-muted)', fontStyle: 'italic',
                          }}
                        >
                          {lang === 'zh' ? '选科待核' : 'Electives pending'}
                        </span>
                      </div>
                    )}
                  </td>
                  <td style={{ padding: '14px 8px', maxWidth: 320 }}>
                    {/* Phase 2a-B — major_parallel (ZJ/SD): slot IS one real major; show it
                        directly from slot.majorChoices without the verifiedSlotMajors gate
                        (that gate requires sourceRef/dataStatus metadata not present in
                        the 逐专业平行投档 dataset). Fall through to the gate for school_group. */}
                    {/* QA 2026-06-10 — 未核验明细只渲染一枚占位 chip，不重复 N 次 */}
                    {/* A1 2026-06-15 — HI 院校专业组无逐专业明细时，渲染组级诚实文案
                        （slot.groupMajorNote，引擎按双语对象下发）取代裸「待核验」；
                        绝不臆造专业名。无 note 时回落到原占位。 */}
                    {hasRawMajorChoices && verifiedMajorChoices.length === 0 && slot.candidateUnit !== 'major_parallel' ? (
                      slot.groupMajorNote ? (
                        <span
                          data-testid="plan-majors-pending"
                          style={{
                            display: 'inline-block', maxWidth: 300, padding: '4px 8px', fontSize: 11,
                            lineHeight: 1.45, borderRadius: 4, whiteSpace: 'normal',
                            background: 'var(--pb-surface-alt)', border: '1px dashed var(--pb-border)',
                            color: 'var(--pb-fg-muted)',
                          }}
                        >
                          {lang === 'zh' ? slot.groupMajorNote.zh : slot.groupMajorNote.en}
                        </span>
                      ) : (
                      <span
                        data-testid="plan-majors-pending"
                        title={lang === 'zh' ? '该专业组暂无已核验的专业明细，不展示模拟专业名称' : 'No verified major detail for this group yet'}
                        style={{
                          padding: '2px 7px', fontSize: 11, borderRadius: 4, whiteSpace: 'nowrap',
                          background: 'var(--pb-surface-alt)', border: '1px dashed var(--pb-border)',
                          color: 'var(--pb-fg-muted)', fontStyle: 'italic',
                        }}
                      >
                        {lang === 'zh' ? '专业明细待核验' : 'Major detail pending verification'}
                      </span>
                      )
                    ) : (verifiedMajorChoices.length > 0 || (slot.candidateUnit === 'major_parallel' && hasRawMajorChoices)) ? (
                      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
                        {(slot.candidateUnit === 'major_parallel' && verifiedMajorChoices.length === 0
                          ? slot.majorChoices
                          : verifiedMajorChoices
                        ).map((m, j) => {
                          const name = typeof m === 'object' && m !== null ? m.name : m;
                          // 专业方向命中标记 — 让"选了方向"在结果里肉眼可见
                          const fit = !!(majorCategory && majorCategory !== 'default'
                            && window.PBMajorMatch && window.PBMajorMatch.majorMatchesCategory(name, majorCategory));
                          return (
                            <span
                              key={j}
                              data-testid={fit ? 'plan-major-fit' : undefined}
                              title={fit ? (lang === 'zh' ? '匹配所选专业方向' : 'Matches your major preference') : undefined}
                              style={{
                                padding: '2px 7px', fontSize: 11, borderRadius: 4, whiteSpace: 'nowrap',
                                background: fit ? 'var(--pb-gold-500)22' : 'var(--pb-surface-alt)',
                                border: '1px solid ' + (fit ? 'var(--pb-gold-500)' : 'var(--pb-border)'),
                                color: fit ? 'var(--pb-gold-700)' : 'inherit',
                                fontWeight: fit ? 600 : 400,
                              }}
                            >
                              {fit ? '★ ' : ''}{name}
                            </span>
                          );
                        })}
                      </div>
                    ) : (
                      <span style={{ color: 'var(--pb-fg-muted)', fontSize: 12 }}>—</span>
                    )}
                  </td>
                  <td style={{ padding: '14px 8px', textAlign: 'right', fontFamily: 'var(--pb-mono)', fontSize: 13, fontWeight: 600, color: slot.probability != null ? (FINE_TIER_COLOR_PLAN[fineKeyForSlotProb(slot.probability)] || 'var(--pb-fg-muted)') : 'var(--pb-fg-muted)' }}>
                    {slot.probability != null ? Math.min(slot.probability, 98) + '%' : '—'}
                  </td>
                  <td style={{ padding: '14px 28px', textAlign: 'right', fontFamily: 'var(--pb-mono)', fontSize: 12, color: 'var(--pb-fg-muted)' }}>
                    {slot.cutoffScore != null ? slot.cutoffScore : '—'}
                    {slot.cutoffRank != null && (
                      <div style={{ fontSize: 10 }}>#{slot.cutoffRank.toLocaleString()}</div>
                    )}
                  </td>
                </tr>
                );
              })}
                  </tbody>
                </table>
              </React.Fragment>
            );
          })()}

          {/* Wave 49 M49.3 — Peer-cohort section: same rank-band landings (HI only currently) */}
          {peerD && planData.peerCohort && planData.peerCohort.supportedProvince && planData.peerCohort.supportedBatch !== false && (
            <div
              data-testid="peer-cohort-section"
              style={{
                borderTop: '1px solid var(--pb-border)',
                padding: '20px 28px',
                background: 'var(--pb-paper)',
              }}
            >
              <div style={{ fontFamily: 'var(--pb-mono)', fontSize: 11, color: 'var(--pb-gold-500)', fontWeight: 700, letterSpacing: '0.05em', marginBottom: 4 }}>
                {peerD.eyebrow}
              </div>
              <h3 style={{ fontFamily: 'var(--pb-serif)', fontSize: 16, fontWeight: 600, margin: '0 0 4px 0' }} data-testid="peer-cohort-title">
                {peerD.title}
              </h3>
              <div style={{ fontSize: 12, color: 'var(--pb-fg-muted)', marginBottom: 14 }} data-testid="peer-cohort-subtitle">
                {peerD.subtitleFmt(
                  planData.peerCohort.userRank,
                  planData.peerCohort.windowRankLow,
                  planData.peerCohort.windowRankHigh,
                  planData.peerCohort.schools.length,
                )}
              </div>
              {planData.peerCohort.schools.length === 0 ? (
                <div style={{ color: 'var(--pb-fg-muted)', fontSize: 13 }} data-testid="peer-cohort-empty">
                  {peerD.emptyWindow}
                </div>
              ) : (
                <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }} data-testid="peer-cohort-table">
                  <thead>
                    <tr style={{ background: 'var(--pb-surface-alt)' }}>
                      <th style={{ textAlign: 'left', padding: '8px 12px', fontSize: 11, color: 'var(--pb-fg-muted)', fontWeight: 600 }}>{peerD.colSchool}</th>
                      <th style={{ textAlign: 'right', padding: '8px 12px', fontSize: 11, color: 'var(--pb-fg-muted)', fontWeight: 600 }}>{peerD.colMinScore}</th>
                      <th style={{ textAlign: 'right', padding: '8px 12px', fontSize: 11, color: 'var(--pb-fg-muted)', fontWeight: 600 }}>{peerD.colMinRank}</th>
                      <th style={{ textAlign: 'right', padding: '8px 12px', fontSize: 11, color: 'var(--pb-fg-muted)', fontWeight: 600 }}>{peerD.colDelta}</th>
                    </tr>
                  </thead>
                  <tbody>
                    {planData.peerCohort.schools.slice(0, 10).map((s, i) => (
                      <tr
                        key={(s.schoolCode || '') + '-' + (s.groupCode || '') + '-' + i}
                        style={{ borderTop: '1px solid var(--pb-border)' }}
                        data-testid={`peer-cohort-row-${i}`}
                      >
                        <td style={{ padding: '8px 12px', fontFamily: 'var(--pb-serif)', fontWeight: 600 }}>{s.schoolName}</td>
                        <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: 'var(--pb-mono)' }}>{typeof s.minScore === 'number' ? s.minScore : '—'}</td>
                        <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: 'var(--pb-mono)', color: 'var(--pb-fg-muted)' }}>
                          {typeof s.minRank === 'number' ? '#' + s.minRank.toLocaleString() : '—'}
                        </td>
                        <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: 'var(--pb-mono)', fontSize: 11, color: s.rankDelta === 0 ? 'var(--pb-success)' : s.rankDelta < 0 ? 'var(--pb-danger)' : 'var(--pb-fg-muted)' }}>
                          {s.rankDelta === 0
                            ? peerD.rankDeltaSame
                            : s.rankDelta < 0
                              ? peerD.rankDeltaHigherFmt(-s.rankDelta)
                              : peerD.rankDeltaLowerFmt(s.rankDelta)}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              )}
              <div style={{ marginTop: 10, fontSize: 11, color: 'var(--pb-fg-muted)' }}>{peerD.footnote}</div>
            </div>
          )}

          {planData.warnings && planData.warnings.length > 0 && (
            <div style={{ padding: '12px 28px', background: 'var(--pb-surface-alt)', fontSize: 11, color: 'var(--pb-fg-muted)', lineHeight: 1.6, borderTop: '1px solid var(--pb-border)' }}>
              <strong>{sim && sim.notes}</strong>
              {planData.warnings.map((w, i2) => (
                <span key={i2}>{w}{i2 < planData.warnings.length - 1 ? ' · ' : ''}</span>
              ))}
            </div>
          )}

          {/* ── Save banner (M41.3) ────────────────────────────────────── */}
          {saveBanner && (
            <div
              data-testid="plan-save-banner"
              style={{
                padding: '10px 28px',
                fontSize: 13,
                fontWeight: 600,
                background: saveBanner === 'ok' ? '#f0fdf4' : saveBanner === 'saving' ? '#eff6ff' : '#fef2f2',
                color: saveBanner === 'ok' ? '#166534' : saveBanner === 'saving' ? '#1e40af' : '#991b1b',
                borderTop: '1px solid var(--pb-border)',
              }}
            >
              {saveBanner === 'ok' && (lang === 'zh' ? '已保存 ✓' : 'Saved ✓')}
              {saveBanner === 'saving' && (lang === 'zh' ? '保存中…' : 'Saving…')}
              {saveBanner === 'limit' && (lang === 'zh' ? '已达上限（最多 20 个草稿），请在仪表盘删除旧方案后再保存。' : 'Draft limit reached (max 20). Delete old drafts from the dashboard first.')}
              {saveBanner === 'error' && (lang === 'zh' ? '保存失败，请稍后重试。' : 'Save failed, please try again.')}
            </div>
          )}

          {/* ── Slot Optimizer bar ─────────────────────────────────────── */}
          {plan && plan.length > 0 && (
            <div style={{ padding: '12px 28px', borderTop: '1px solid var(--pb-border)', display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', background: 'var(--pb-paper)' }}>
              {optSummary && (
                <div
                  data-testid="sim-optimization-summary"
                  style={{
                    flex: 1, minWidth: 220,
                    padding: '10px 14px',
                    background: optSummary.deltaPct > 0 ? '#f0fdf4' : '#eff6ff',
                    border: `1px solid ${optSummary.deltaPct > 0 ? '#86efac' : '#93c5fd'}`,
                    borderRadius: 8,
                    fontSize: 12,
                  }}
                >
                  <span style={{ fontWeight: 600 }}>
                    {opt.summaryTitle || (lang === 'zh' ? '录取期望提升分析' : 'Expected Admission Analysis')}：
                  </span>
                  {optSummary.deltaPct > 0
                    ? (opt.summaryFmt
                        ? opt.summaryFmt(optSummary.beforeRate, optSummary.afterRate, optSummary.deltaPct)
                        : `原顺序录取期望 ${optSummary.beforeRate}% → 优化后 ${optSummary.afterRate}% (+${optSummary.deltaPct}%)`)
                    : (opt.summaryNoGain
                        ? opt.summaryNoGain(optSummary.beforeRate)
                        : `原顺序录取期望 ${optSummary.beforeRate}%，当前排列已是较优方案`)}
                </div>
              )}
              <button
                type="button"
                className="pb-btn pb-btn-link pb-btn-sm"
                onClick={handleSimOptimize}
                data-testid="sim-optimize-order-btn"
                style={{ whiteSpace: 'nowrap' }}
              >
                {opt.btnOptimize || (lang === 'zh' ? '🔀 优化顺序' : '🔀 Optimize Order')}
              </button>
              {optSummary && (
                <button
                  type="button"
                  className="pb-btn pb-btn-link pb-btn-sm"
                  onClick={handleSimRevert}
                  data-testid="sim-revert-order-btn"
                  style={{ whiteSpace: 'nowrap' }}
                >
                  {opt.btnRevert || (lang === 'zh' ? '↩️ 还原' : '↩️ Revert')}
                </button>
              )}
              {/* M41.4 — Export buttons */}
              {_exp ? (
                <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' }}>
                  <button
                    type="button"
                    className="pb-btn pb-btn-ghost pb-btn-sm"
                    onClick={handleCopyTSV}
                    data-testid="plan-export-copy-btn"
                    style={{ whiteSpace: 'nowrap' }}
                    title={lang === 'zh' ? '复制为制表符分隔格式，可直接粘贴到海南考试局填报系统' : 'Copy as TSV for Hainan exam bureau system'}
                  >
                    {lang === 'zh' ? '📋 复制志愿表' : '📋 Copy table'}
                  </button>
                  {copyConfirm && (
                    <span
                      data-testid="plan-export-copy-confirm"
                      style={{ fontSize: 12, color: 'var(--pb-success)', fontWeight: 600 }}
                    >
                      ✓
                    </span>
                  )}
                  <button
                    type="button"
                    className="pb-btn pb-btn-ghost pb-btn-sm"
                    onClick={handleDownloadCSV}
                    data-testid="plan-export-csv-btn"
                    style={{ whiteSpace: 'nowrap' }}
                    title={lang === 'zh' ? '下载 CSV（UTF-8 BOM，Excel 友好）' : 'Download CSV (UTF-8 BOM, Excel-friendly)'}
                  >
                    {lang === 'zh' ? '📥 下载 CSV' : '📥 Download CSV'}
                  </button>
                  <button
                    type="button"
                    className="pb-btn pb-btn-ghost pb-btn-sm"
                    onClick={handleDownloadJSON}
                    data-testid="plan-export-json-btn"
                    style={{ whiteSpace: 'nowrap' }}
                    title={lang === 'zh' ? '下载完整方案 JSON（含概率/置信度字段）' : 'Download full plan JSON (with probability/confidence)'}
                  >
                    {lang === 'zh' ? '📦 下载 JSON' : '📦 Download JSON'}
                  </button>
                  <button
                    type="button"
                    className="pb-btn pb-btn-ghost pb-btn-sm"
                    onClick={handlePrintPDF}
                    data-testid="plan-print-btn"
                    style={{ whiteSpace: 'nowrap' }}
                    title={lang === 'zh' ? '在新标签页打开打印预览，选择"另存为 PDF"即可导出' : 'Open print preview in new tab, choose "Save as PDF"'}
                  >
                    {lang === 'zh' ? '🖨 打印 / 保存 PDF' : '🖨 Print / Save PDF'}
                  </button>
                </div>
              ) : null}
              {/* M41.3 — Save draft button */}
              {_isLoggedIn ? (
                <button
                  type="button"
                  className="pb-btn pb-btn-ghost pb-btn-sm"
                  onClick={handleSaveDraft}
                  data-testid="plan-save-btn"
                  disabled={saveBanner === 'saving'}
                  style={{ whiteSpace: 'nowrap', marginLeft: 'auto' }}
                >
                  {lang === 'zh' ? '💾 保存方案' : '💾 Save plan'}
                </button>
              ) : (
                <button
                  type="button"
                  className="pb-btn pb-btn-ghost pb-btn-sm"
                  onClick={() => navigateTo('/login?next=' + encodeURIComponent('#/gaokao'))}
                  data-testid="plan-save-login-cta"
                  style={{ whiteSpace: 'nowrap', marginLeft: 'auto' }}
                >
                  {lang === 'zh' ? '🔒 登录保存方案' : '🔒 Log in to save'}
                </button>
              )}
            </div>
          )}

          {gatesOn && planState.anonymous && (
            <div style={{ padding: '16px 28px', background: 'linear-gradient(180deg, transparent, var(--pb-surface-alt) 60%)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, flexWrap: 'wrap', borderTop: '1px solid var(--pb-border)' }}>
              <div style={{ flex: 1, minWidth: 220 }}>
                <div style={{ fontFamily: 'var(--pb-serif)', fontSize: 15, fontWeight: 600, marginBottom: 4 }}>
                  {sim && (typeof sim.unlockTitle === 'function' ? sim.unlockTitle(totalSlots) : sim.unlockTitle)}
                </div>
                <div style={{ fontSize: 12, color: 'var(--pb-fg-muted)', lineHeight: 1.55 }}>
                  {sim && sim.unlockDesc}
                </div>
              </div>
              <button
                className="pb-btn pb-btn-primary"
                style={{ padding: '10px 20px', fontSize: 13 }}
                onClick={() => navigateTo('/login?next=' + encodeURIComponent('#/gaokao'))}
                data-testid="gaokao-simulate-anon-cta"
              >
                {sim && sim.loginCta}
              </button>
            </div>
          )}
        </div>
      )}

      {/* Wave 49 M49.4 — sticky PK cart (visible when ≥ 1 picked) */}
      {cpD && pickedCodes.length > 0 && (
        <div
          data-testid="gaokao-pk-cart"
          style={{
            position: 'fixed', bottom: 20, left: '50%', transform: 'translateX(-50%)', zIndex: 50,
            background: 'var(--pb-paper)', border: '1px solid var(--pb-border)',
            borderRadius: 12, padding: '12px 16px', boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
            display: 'flex', alignItems: 'center', gap: 12, fontSize: 13,
            maxWidth: 'calc(100vw - 32px)', flexWrap: 'wrap', justifyContent: 'center',
          }}
        >
          <span data-testid="gaokao-pk-cart-count" style={{ fontFamily: 'var(--pb-mono)', fontWeight: 700 }}>
            {cpD.cartCountFmt(pickedCodes.length, PK_MAX)}
          </span>
          {pickedCodes.length < 2 && (
            <span style={{ fontSize: 11, color: 'var(--pb-fg-muted)' }}>{cpD.cartHintMin}</span>
          )}
          <button
            type="button"
            className="pb-btn pb-btn-link pb-btn-sm"
            onClick={clearPickedCodes}
            data-testid="gaokao-pk-cart-clear"
            style={{ fontSize: 12 }}
          >
            {cpD.cartClearBtn}
          </button>
          <button
            type="button"
            className="pb-btn pb-btn-primary"
            onClick={launchCompare}
            disabled={pickedCodes.length < 2}
            data-testid="gaokao-pk-cart-compare"
            style={{ padding: '6px 14px', fontSize: 13, opacity: pickedCodes.length < 2 ? 0.5 : 1, cursor: pickedCodes.length < 2 ? 'not-allowed' : 'pointer' }}
          >
            {cpD.cartCompareBtn}
          </button>
        </div>
      )}
    </div>
  );
};

window.SimulatePlanBlock = SimulatePlanBlock;
