/* ============================================================
   test.jsx — the test-taker experience
   Phases: start → listening → interstitial_L → reading
           → (interstitial_S → speaking)? → results
   Exports window.TestExperience
   ============================================================ */
(function () {
const { useState, useEffect, useRef, useMemo } = React;
const S = window.TestStore;

/* ---- TOEIC-style scaled scoring ---- */
function scaleSection(correct, total) {
  if (!total) return 5;
  return Math.min(495, Math.max(5, Math.round((correct / total * 495) / 5) * 5));
}
function proficiencyBand(total) {
  if (total >= 860) return { label: "International Proficiency",   color: "var(--correct)" };
  if (total >= 730) return { label: "Working Proficiency Plus",    color: "#2A7AB0" };
  if (total >= 470) return { label: "Limited Working Proficiency", color: "var(--primary)" };
  if (total >= 220) return { label: "Elementary Proficiency",      color: "var(--warn)" };
  return                   { label: "Basic Proficiency",           color: "var(--ink-soft)" };
}

/* ============================================================
   Start screen
   ============================================================ */
function StartScreen({ settings, onBegin, onExit }) {
  const [name, setName] = useState("");
  const hasSpeaking  = S.allSpeakingItems().length > 0 && settings.speakingEnabled !== false;
  const speakItems   = S.allSpeakingItems();

  const sections = ["listening", "reading"]
    .filter((key) => key === "listening" ? settings.listeningEnabled !== false : settings.readingEnabled !== false)
    .map((key) => {
      const sec = S.SECTIONS[key];
      return {
        key, name: sec.name,
        icon: key === "listening" ? "headphones" : "book",
        duration: key === "listening" ? settings.durationListening : settings.durationReading,
        parts: sec.parts.map((p) => ({ ...S.PARTS[p], count: S.questionCount(p) })),
        total: sec.parts.reduce((s, p) => s + S.questionCount(p), 0),
      };
    });
  const grandTotal = sections.reduce((s, x) => s + x.total, 0);
  const canBegin   = (grandTotal > 0 || hasSpeaking) && name.trim().length > 0;

  const sectionLabels = [
    ...(settings.listeningEnabled !== false ? ["listening"] : []),
    ...(settings.readingEnabled !== false ? ["reading"] : []),
    ...(hasSpeaking ? ["speaking"] : []),
  ];
  const sectionTitle = sectionLabels.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(", ").replace(/, ([^,]*)$/, " & $1");

  return (
    <div className="view-in" style={{ maxWidth: 880, margin: "0 auto", padding: "44px 24px 80px" }}>
      <div style={{ textAlign: "center", marginBottom: 36 }}>
        <span className="eyebrow">{sectionTitle} Assessment</span>
        <h1 className="display" style={{ fontSize: 44, margin: "12px 0 10px" }}>{settings.testTitle}</h1>
        <p className="muted" style={{ fontSize: 17, maxWidth: 560, margin: "0 auto" }}>
          A timed proficiency test.
          Work through each section before its timer ends.
        </p>
      </div>

      {/* Notice — shown first so test-takers with mic see it before filling name */}
      {hasSpeaking && (
        <div className="panel" style={{ padding: "14px 18px", background: "var(--incorrect-soft)", border: "1px solid var(--incorrect-line)", marginBottom: 24 }}>
          <div className="row gap-3" style={{ alignItems: "flex-start" }}>
            <Icon name="mic" size={17} style={{ color: "var(--incorrect)", marginTop: 2, flexShrink: 0 }} />
            <div className="col gap-1" style={{ fontSize: 13.5 }}>
              <strong style={{ fontWeight: 600 }}>Microphone required</strong>
              <span className="muted">This test includes a speaking section. Please ensure your microphone is connected and accessible before you begin.</span>
            </div>
          </div>
        </div>
      )}

      {/* section cards */}
      <div className="col gap-4" style={{ marginBottom: 24 }}>
        {sections.map((sec) => (
          <div key={sec.key} className="card" style={{ padding: 22 }}>
            <div className="row spread" style={{ alignItems: "flex-start", marginBottom: 16 }}>
              <div className="row gap-3" style={{ alignItems: "center" }}>
                <div style={{ width: 44, height: 44, borderRadius: 11, background: "var(--primary-soft)", color: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                  <Icon name={sec.icon} size={22} />
                </div>
                <div className="col">
                  <h3 style={{ fontSize: 20, fontFamily: "var(--font-serif)" }}>{sec.name}</h3>
                  <span className="faint" style={{ fontSize: 13.5 }}>{sec.total} questions</span>
                </div>
              </div>
              <span className="badge"><Icon name="clock" size={13} /> {sec.duration} min</span>
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(150px,1fr))", gap: 8 }}>
              {sec.parts.map((p) => (
                <div key={p.n} className="row gap-2" style={{ alignItems: "center", padding: "9px 12px", background: "var(--surface-2)", borderRadius: 8, border: "1px solid var(--line-soft)" }}>
                  <span className="mono" style={{ fontSize: 12, fontWeight: 600, color: "var(--primary)" }}>P{p.n}</span>
                  <div className="col" style={{ minWidth: 0, lineHeight: 1.25 }}>
                    <span style={{ fontSize: 13, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{p.short}</span>
                    <span className="faint" style={{ fontSize: 11.5 }}>{p.count} Q</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
        ))}

        {/* Speaking card */}
        {hasSpeaking && (
          <div className="card" style={{ padding: 22, borderColor: "var(--incorrect-line)" }}>
            <div className="row spread" style={{ alignItems: "flex-start", marginBottom: 16 }}>
              <div className="row gap-3" style={{ alignItems: "center" }}>
                <div style={{ width: 44, height: 44, borderRadius: 11, background: "var(--incorrect-soft)", color: "var(--incorrect)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                  <Icon name="mic" size={22} />
                </div>
                <div className="col">
                  <h3 style={{ fontSize: 20, fontFamily: "var(--font-serif)" }}>Speaking</h3>
                  <span className="faint" style={{ fontSize: 13.5 }}>{speakItems.length} prompt{speakItems.length !== 1 ? "s" : ""}</span>
                </div>
              </div>
              <span className="badge" style={{ background: "var(--incorrect-soft)", color: "var(--incorrect)", border: "1px solid var(--incorrect-line)" }}>
                <Icon name="mic" size={12} /> Recorded
              </span>
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(180px,1fr))", gap: 8 }}>
              {speakItems.map((it, i) => (
                <div key={it.id} className="row gap-2" style={{ alignItems: "center", padding: "9px 12px", background: "var(--incorrect-soft)", borderRadius: 8, border: "1px solid var(--incorrect-line)" }}>
                  <span className="mono" style={{ fontSize: 12, fontWeight: 600, color: "var(--incorrect)" }}>Q{i + 1}</span>
                  <div className="col" style={{ minWidth: 0, lineHeight: 1.25 }}>
                    <span style={{ fontSize: 13, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.topic || "Speaking"}</span>
                    <span className="faint" style={{ fontSize: 11.5 }}>{it.prepTime}s prep · {it.responseTime}s speak</span>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>

      {/* Participant name */}
      <div className="card" style={{ padding: 20, marginBottom: 22 }}>
        <Field label="Your name" hint="Saved with your test result so the admin can identify your submission.">
          <input
            className="input"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="Enter your full name"
            style={{ maxWidth: 420 }}
            onKeyDown={(e) => { if (e.key === "Enter" && canBegin) onBegin(name.trim()); }}
          />
        </Field>
      </div>

      {/* General notice */}
      <div className="panel" style={{ padding: "14px 18px", background: "var(--surface-2)", marginBottom: 28 }}>
        <div className="row gap-3" style={{ alignItems: "flex-start" }}>
          <Icon name="alert" size={17} style={{ color: "var(--warn)", marginTop: 2, flexShrink: 0 }} />
          <div className="col gap-1" style={{ fontSize: 13.5 }}>
            <strong style={{ fontWeight: 600 }}>Before you begin</strong>
            <span className="muted">Each section is timed independently — the test advances automatically when a timer runs out. You may revisit answers within a section using the navigator.</span>
          </div>
        </div>
      </div>

      <div className="row gap-3 center">
        <button className="btn btn-ghost btn-lg" onClick={onExit}><Icon name="arrowLeft" size={17} /> Home</button>
        <button className="btn btn-primary btn-lg" onClick={() => onBegin(name.trim())} disabled={!canBegin}>
          Begin Test <Icon name="arrowRight" size={17} />
        </button>
      </div>
      {!grandTotal && !hasSpeaking && <p className="hint" style={{ textAlign: "center", marginTop: 12 }}>No questions yet — add some in the admin panel first.</p>}
      {(grandTotal > 0 || hasSpeaking) && !name.trim() && <p className="hint" style={{ textAlign: "center", marginTop: 12 }}>Enter your name above to begin.</p>}
    </div>
  );
}

/* ============================================================
   QuestionBlock
   ============================================================ */
function QuestionBlock({ entry, qNumber, answers, onAnswer, optionCount }) {
  const q   = entry.q;
  const sel = answers[q.id];
  return (
    <div className="col gap-3" style={{ paddingTop: 4 }}>
      {q.text ? (
        <div className="row gap-3" style={{ alignItems: "baseline" }}>
          <span className="mono" style={{ fontSize: 13, fontWeight: 600, color: "var(--primary)", flexShrink: 0 }}>{qNumber}.</span>
          <p style={{ fontSize: 16, fontWeight: 500, lineHeight: 1.5 }}>{q.text}</p>
        </div>
      ) : (
        <div className="row gap-2" style={{ alignItems: "center" }}>
          <span className="mono" style={{ fontSize: 13, fontWeight: 600, color: "var(--primary)" }}>Question {qNumber}</span>
          <span className="faint" style={{ fontSize: 13 }}>— mark the letter of the best response.</span>
        </div>
      )}
      <div className="col gap-2">
        {q.options.slice(0, optionCount).map((opt, i) => {
          const active = sel === i;
          return (
            <button key={i} onClick={() => onAnswer(q.id, i)} style={{
              display: "flex", alignItems: "center", gap: 12, textAlign: "left",
              padding: "11px 14px", borderRadius: 9, cursor: "pointer", width: "100%",
              background: active ? "var(--primary-tint)" : "var(--surface)",
              border: `1.5px solid ${active ? "var(--primary)" : "var(--line)"}`,
              transition: "all .12s ease", fontFamily: "var(--font-ui)",
            }}>
              <OptionLetter index={i} state={active ? "selected" : "idle"} />
              {opt
                ? <span style={{ fontSize: 15, color: "var(--ink)", lineHeight: 1.45 }}>{opt}</span>
                : <span className="faint" style={{ fontSize: 14 }}>(spoken option {["A","B","C","D"][i]})</span>}
            </button>
          );
        })}
      </div>
    </div>
  );
}

/* ============================================================
   ItemView
   ============================================================ */
function ItemView({ item, entries, numberOf, answers, onAnswer, stimKey }) {
  const meta          = S.PARTS[item.part];
  const isReadingPass = meta.hasPassage;

  const stimulus = (
    <div className="col gap-4">
      {meta.hasImage && (
        <div style={{ borderRadius: 12, overflow: "hidden", border: "1px solid var(--line)", background: "var(--surface-3)" }}>
          <img src={item.imageUrl} alt="Photograph" style={{ width: "100%", display: "block" }} />
        </div>
      )}
      {meta.hasAudio && <AudioPlayer item={item} autoKey={stimKey} />}
      {isReadingPass && (
        <div className="panel" style={{ padding: "20px 22px", background: "var(--surface-2)" }}>
          {item.passageLabel && <span className="badge badge-read" style={{ marginBottom: 12 }}>{item.passageLabel}</span>}
          <div style={{ fontSize: 15, lineHeight: 1.72, whiteSpace: "pre-wrap", fontFamily: "var(--font-ui)", color: "var(--ink)" }}>{item.passage}</div>
          {item.passage2 && (
            <div style={{ marginTop: 16, paddingTop: 16, borderTop: "1px dashed var(--line-strong)", fontSize: 15, lineHeight: 1.72, whiteSpace: "pre-wrap" }}>{item.passage2}</div>
          )}
        </div>
      )}
    </div>
  );

  const questions = (
    <div className="col gap-6">
      {entries.map((e) => (
        <QuestionBlock key={e.q.id} entry={e} qNumber={numberOf(e.q.id)}
          answers={answers} onAnswer={onAnswer} optionCount={meta.options} />
      ))}
    </div>
  );

  const twoCol = isReadingPass || meta.hasImage;
  return (
    <div className="card view-in" style={{ padding: 0, overflow: "hidden" }}>
      <div className="row gap-3" style={{ alignItems: "center", padding: "13px 20px", borderBottom: "1px solid var(--line)", background: "var(--surface-2)" }}>
        <span className="badge badge-primary">Part {item.part}</span>
        <span style={{ fontWeight: 600, fontSize: 14.5 }}>{meta.name}</span>
        <span className="faint grow" style={{ fontSize: 13, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>— {meta.desc}</span>
      </div>
      <div className={twoCol ? "two-col" : ""} style={{ padding: 22, display: twoCol ? "grid" : "block", gridTemplateColumns: twoCol ? "minmax(0,1fr) minmax(0,1fr)" : "1fr", gap: 26 }}>
        {twoCol
          ? <>{stimulus}<div>{questions}</div></>
          : <div className="col gap-4">{meta.hasAudio && stimulus}{questions}</div>}
      </div>
    </div>
  );
}

/* ============================================================
   SectionRunner
   ============================================================ */
function SectionRunner({ sectionKey, settings, answers, onAnswer, onComplete }) {
  const sec  = S.SECTIONS[sectionKey];
  const flat  = useMemo(() => S.buildExam(sectionKey), [sectionKey]);
  const items = useMemo(() => {
    const seen = new Set();
    const result = [];
    flat.forEach((e) => { if (!seen.has(e.item.id)) { seen.add(e.item.id); result.push(e.item); } });
    return result;
  }, [flat]);
  const qMap  = useMemo(() => { const m = {}; flat.forEach((e, i) => { m[e.q.id] = i + 1; }); return m; }, [flat]);

  const [idx, setIdx]                 = useState(0);
  const [showNav, setShowNav]         = useState(false);
  const [confirmSubmit, setConfirmSubmit] = useState(false);
  const duration  = sectionKey === "listening" ? settings.durationListening : settings.durationReading;
  const scrollRef = useRef(null);

  useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = 0; }, [idx]);

  if (!items.length) {
    useEffect(() => onComplete(), []);
    return null;
  }

  const item          = items[idx];
  const itemEntries   = flat.filter((e) => e.item.id === item.id);
  const answeredCount = flat.filter((e) => answers[e.q.id] != null).length;
  const isLast        = idx === items.length - 1;
  const numberOf      = (qid) => qMap[qid];

  function jumpToQuestion(qid) {
    const target = flat.find((e) => e.q.id === qid); if (!target) return;
    const i = items.findIndex((it) => it.id === target.item.id);
    if (i >= 0) setIdx(i);
    setShowNav(false);
  }

  return (
    <div className="col" style={{ height: "100vh" }}>
      {/* top bar */}
      <header className="row spread" style={{ padding: "12px 22px", borderBottom: "1px solid var(--line)", background: "var(--surface)", alignItems: "center", flexShrink: 0 }}>
        <div className="row gap-3" style={{ alignItems: "center" }}>
          <div style={{ width: 36, height: 36, borderRadius: 9, background: "var(--primary-soft)", color: "var(--primary)", display: "flex", alignItems: "center", justifyContent: "center" }}>
            <Icon name={sectionKey === "listening" ? "headphones" : "book"} size={19} />
          </div>
          <div className="col" style={{ lineHeight: 1.2 }}>
            <span style={{ fontWeight: 600, fontSize: 15, fontFamily: "var(--font-serif)" }}>{sec.name} Section</span>
            <span className="faint" style={{ fontSize: 12.5 }}>Stimulus {idx + 1} of {items.length} · {answeredCount}/{flat.length} answered</span>
          </div>
        </div>
        <div className="row gap-3" style={{ alignItems: "center" }}>
          <Timer minutes={duration} runningKey={sectionKey} onExpire={onComplete} />
          <button className="btn btn-ghost btn-sm" onClick={() => setShowNav(true)}><Icon name="grid" size={15} /> Navigator</button>
        </div>
      </header>

      {/* progress */}
      <div style={{ height: 4, background: "var(--surface-3)", flexShrink: 0 }}>
        <div style={{ height: "100%", width: `${((idx + 1) / items.length) * 100}%`, background: "var(--primary)", transition: "width .3s ease" }} />
      </div>

      {/* body */}
      <div ref={scrollRef} className="grow" style={{ overflow: "auto", padding: "26px 22px" }}>
        <div style={{ maxWidth: 1080, margin: "0 auto" }}>
          <ItemView item={item} entries={itemEntries} numberOf={numberOf} answers={answers} onAnswer={onAnswer} stimKey={item.id} />
        </div>
      </div>

      {/* footer */}
      <footer className="row spread" style={{ padding: "14px 22px", borderTop: "1px solid var(--line)", background: "var(--surface)", alignItems: "center", flexShrink: 0 }}>
        <button className="btn btn-ghost" onClick={() => setIdx((i) => Math.max(0, i - 1))} disabled={idx === 0}>
          <Icon name="arrowLeft" size={16} /> Previous
        </button>
        <div className="row gap-2" style={{ alignItems: "center" }}>
          {itemEntries.map((e) => (
            <span key={e.q.id} title={`Q${numberOf(e.q.id)}`} style={{ width: 9, height: 9, borderRadius: "50%", background: answers[e.q.id] != null ? "var(--primary)" : "var(--line-strong)" }} />
          ))}
        </div>
        {isLast
          ? <button className="btn btn-primary" onClick={() => setConfirmSubmit(true)}>
              {sectionKey === "listening" ? "Finish Listening" : "Proceed"} <Icon name="arrowRight" size={16} />
            </button>
          : <button className="btn btn-primary" onClick={() => setIdx((i) => Math.min(items.length - 1, i + 1))}>
              Next <Icon name="arrowRight" size={16} />
            </button>}
      </footer>

      {/* Navigator modal */}
      <Modal open={showNav} onClose={() => setShowNav(false)} title={`${sec.name} — Question Navigator`} width={560}>
        <div className="col gap-4">
          {sec.parts.map((p) => {
            const pe = flat.filter((e) => e.part === p);
            if (!pe.length) return null;
            return (
              <div key={p} className="col gap-2">
                <div className="row gap-2" style={{ alignItems: "center" }}>
                  <span className="badge badge-primary">Part {p}</span>
                  <span style={{ fontSize: 13.5, fontWeight: 500 }}>{S.PARTS[p].name}</span>
                </div>
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(42px,1fr))", gap: 7 }}>
                  {pe.map((e) => {
                    const done = answers[e.q.id] != null;
                    const onSc = e.item.id === item.id;
                    return (
                      <button key={e.q.id} onClick={() => jumpToQuestion(e.q.id)} style={{
                        height: 40, borderRadius: 8, fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 600, cursor: "pointer",
                        background: done ? "var(--primary)" : "var(--surface)",
                        color: done ? "#fff" : "var(--ink-soft)",
                        border: `1.5px solid ${onSc ? "var(--accent)" : done ? "var(--primary)" : "var(--line-strong)"}`,
                      }}>{numberOf(e.q.id)}</button>
                    );
                  })}
                </div>
              </div>
            );
          })}
          <div className="row gap-4 wrap" style={{ fontSize: 12.5, color: "var(--ink-soft)", paddingTop: 6 }}>
            <span className="row gap-2"><span style={{ width: 13, height: 13, borderRadius: 4, background: "var(--primary)", display: "inline-block" }} /> Answered</span>
            <span className="row gap-2"><span style={{ width: 13, height: 13, borderRadius: 4, background: "var(--surface)", border: "1.5px solid var(--line-strong)", display: "inline-block" }} /> Unanswered</span>
            <span className="row gap-2"><span style={{ width: 13, height: 13, borderRadius: 4, background: "var(--surface)", border: "1.5px solid var(--accent)", display: "inline-block" }} /> On screen</span>
          </div>
        </div>
      </Modal>

      {/* Submit confirm */}
      <Modal open={confirmSubmit} onClose={() => setConfirmSubmit(false)} title="Finish this section?" width={420}>
        <p className="muted" style={{ fontSize: 14.5, marginBottom: 8 }}>
          You've answered <strong style={{ color: "var(--ink)" }}>{answeredCount} of {flat.length}</strong> questions in this section.
        </p>
        {answeredCount < flat.length && <p className="hint" style={{ marginBottom: 16 }}>Unanswered questions will be marked incorrect.</p>}
        <div className="row gap-3" style={{ justifyContent: "flex-end", marginTop: 8 }}>
          <button className="btn btn-ghost" onClick={() => setConfirmSubmit(false)}>Keep working</button>
          <button className="btn btn-primary" onClick={onComplete}>
            {sectionKey === "listening" ? "Continue to Reading" : "Proceed"}
          </button>
        </div>
      </Modal>
    </div>
  );
}

/* ============================================================
   Interstitials
   ============================================================ */
function ListeningDone({ settings, onContinue }) {
  return (
    <div className="view-in center" style={{ display: "flex", height: "100vh", flexDirection: "column", padding: 24, textAlign: "center" }}>
      <div style={{ width: 60, height: 60, borderRadius: 15, background: "var(--correct-soft)", color: "var(--correct)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 22 }}>
        <Icon name="checkCircle" size={30} />
      </div>
      <span className="eyebrow">Section complete</span>
      <h2 className="display" style={{ fontSize: 32, margin: "10px 0 10px" }}>Listening finished</h2>
      <p className="muted" style={{ fontSize: 16, maxWidth: 440, marginBottom: 28 }}>
        Next is the Reading section — {settings.durationReading} minutes. The timer starts when you continue.
      </p>
      <button className="btn btn-primary btn-lg" onClick={onContinue}>Start Reading <Icon name="arrowRight" size={17} /></button>
    </div>
  );
}

/* ============================================================
   Main orchestrator
   ============================================================ */
window.TestExperience = function TestExperience({ onExit }) {
  const [settings]         = useState(() => S.getSettings());
  const [phase, setPhase]  = useState("start");
  const [answers, setAnswers] = useState({});
  const [participantName, setParticipantName] = useState("");
  const [speakingResults, setSpeakingResults] = useState([]);
  const savedRef   = useRef(false);
  const answersRef = useRef({});

  // Keep answersRef in sync so closures always see latest answers
  useEffect(() => { answersRef.current = answers; }, [answers]);

  const hasListening = settings.listeningEnabled !== false;
  const hasReading   = settings.readingEnabled   !== false;
  const hasSpeaking  = S.getTestSpeakingItems().length > 0 && settings.speakingEnabled !== false;
  const onAnswer     = (qid, opt) => setAnswers((a) => ({ ...a, [qid]: opt }));

  function beginTest(name) {
    S.startExam();
    setParticipantName(name);
    setAnswers({});
    answersRef.current = {};
    savedRef.current = false;
    setSpeakingResults([]);
    if (hasListening) {
      setPhase("listening");
    } else if (hasReading) {
      setPhase("reading");
    } else if (hasSpeaking) {
      setPhase("interstitial_speaking");
    } else {
      goToResults([]);
    }
  }

  function completeListening() {
    if (hasReading) {
      setPhase("interstitial_listening");
    } else if (hasSpeaking) {
      setPhase("interstitial_speaking");
    } else {
      goToResults([]);
    }
  }

  function completeReading() {
    if (hasSpeaking) {
      setPhase("interstitial_speaking");
    } else {
      goToResults([]);
    }
  }

  function goToResults(spkResults) {
    setSpeakingResults(spkResults);
    if (!savedRef.current) {
      savedRef.current = true;
      const ans = answersRef.current;
      const compute = (key) => {
        const flat    = S.buildExam(key);
        const correct = flat.filter((e) => ans[e.q.id] === e.q.correct).length;
        const total   = flat.length;
        return { correct, total, scaled: scaleSection(correct, total) };
      };
      const L = hasListening ? compute("listening") : { correct: 0, total: 0, scaled: 0 };
      const R = hasReading   ? compute("reading")   : { correct: 0, total: 0, scaled: 0 };
      const totalScaled = L.scaled + R.scaled;
      const b = proficiencyBand(totalScaled);
      const spkItems = S.getTestSpeakingItems();
      S.saveResult({
        participantName,
        listening: L,
        reading: R,
        speaking: {
          attempted: spkResults.filter((r) => r.attempted).length,
          total: spkItems.length,
          items: spkResults,
        },
        hasSpeaking,
        totalScaled,
        band: b.label,
        answers: { ...ans },
      });
    }
    setPhase("results");
  }

  if (phase === "start")
    return <div style={{ minHeight: "100vh" }}><StartScreen settings={settings} onBegin={beginTest} onExit={onExit} /></div>;

  if (phase === "listening")
    return <SectionRunner sectionKey="listening" settings={settings} answers={answers} onAnswer={onAnswer} onComplete={completeListening} />;

  if (phase === "interstitial_listening")
    return <ListeningDone settings={settings} onContinue={() => setPhase("reading")} />;

  if (phase === "reading")
    return <SectionRunner sectionKey="reading" settings={settings} answers={answers} onAnswer={onAnswer} onComplete={completeReading} />;

  if (phase === "interstitial_speaking")
    return <window.SpeakingInterstitial onContinue={() => setPhase("speaking")} />;

  if (phase === "speaking")
    return <window.SpeakingSection onComplete={goToResults} />;

  return <window.ResultsScreen
    settings={settings}
    answers={answers}
    speakingResults={speakingResults}
    hasSpeaking={hasSpeaking}
    onExit={onExit}
    onRetake={() => { setAnswers({}); setSpeakingResults([]); savedRef.current = false; setPhase("start"); }}
  />;
};
})();
