import React, { act } from "react";
(globalThis as { React?: typeof React }).React = React;
import assert from "node:assert/strict";
import { test } from "node:test";
import { createRoot } from "react-dom/client";
import { parseHTML } from "linkedom";
import { QuestionDock } from "../components/question-dock.tsx";
import { AnsweredCard } from "../components/answered-card.tsx";
import { newAnswerResult, type QuestionAnswerData } from "../src/core/index.ts";
const dom = parseHTML("
");
Object.assign(globalThis, { window: dom.window, document: dom.document, Element: dom.window.Element,
HTMLElement: dom.window.HTMLElement, IS_REACT_ACT_ENVIRONMENT: true });
test("actual select → submit → saved result → historical Why uses source data and presentation-only recovery", async () => {
const question = String.raw`Find $\\nabla f$ for $\begin{pmatrix}a&b\\c&d\end{pmatrix}$.`;
const label = String.raw`$\\nabla f$`;
const explanation = String.raw`First paragraph.\n\nThe matrix is $\begin{pmatrix}p&q\\r&s\end{pmatrix}$ and $\\nabla f$ is its gradient.`;
const input = { question, options: [{ label }, { label: "no" }], correctAnswer: label, explanation };
const before = structuredClone(input);
let answer: QuestionAnswerData | undefined;
const host = document.createElement("div"); document.body.append(host);
const root = createRoot(host);
try {
await act(async () => root.render( {}} onSubmit={a => { answer = a; }} />));
assert.equal(host.querySelectorAll("mtr").length, 2, "the live matrix retains two rows");
const choice = [...host.querySelectorAll('[role="radio"]')].find(button => button.querySelector(".katex"))!;
assert.ok(choice);
await act(async () => choice.click());
assert.equal(choice.getAttribute("aria-checked"), "true");
const submit = [...host.querySelectorAll("button")].find(button => button.textContent?.includes("Submit answer"))!;
assert.equal(submit.disabled, false);
// linkedom does not implement the browser's default button→form
// action; dispatch its native submit event through React instead.
await act(async () => submit.closest("form")!.dispatchEvent(new window.Event("submit", { bubbles: true, cancelable: true })));
assert.ok(answer);
assert.equal(answer.questionText, question);
assert.equal(answer.explanation, explanation);
assert.equal(answer.answers[0]!.value, label, "display recovery must not alter the submitted identity");
assert.equal(answer.answers[0]!.label, label);
assert.equal(answer.correct, true);
const persisted = JSON.parse(JSON.stringify(newAnswerResult("interactive_source", "probe", answer)));
assert.deepEqual(persisted.details, JSON.parse(JSON.stringify(answer)));
assert.equal(persisted.details.explanation, explanation);
const saved = structuredClone(persisted.details);
await act(async () => root.render());
const why = [...host.querySelectorAll("button")].find(button => button.textContent === "Why")!;
await act(async () => why.click());
assert.equal(host.querySelectorAll("mtr").length, 4, "question and revealed explanation retain their rows");
const paragraphs = host.querySelectorAll('[data-testid="tucked-explanation"] p');
assert.equal(paragraphs.length, 2, "recovered paragraphs remain distinct in Why");
assert.ok(paragraphs[0]!.parentElement!.classList.contains("[&>p:first-child]:inline"));
assert.ok(!paragraphs[0]!.parentElement!.classList.contains("[&_p]:inline"));
const annotations = [...host.querySelectorAll('annotation[encoding="application/x-tex"]')].map(n => n.textContent);
assert.equal(annotations.filter(t => t === String.raw`\nabla f`).length, 3);
assert.deepEqual(persisted.details, saved);
assert.deepEqual(input, before);
} finally { await act(async () => root.unmount()); host.remove(); }
});
test("committed-answer reveals keep the first paragraph inline without flattening later paragraphs", async () => {
const committed = String.raw`First paragraph.\n\nSecond paragraph with $\\nabla f$ and $\nu$.`;
const answer: QuestionAnswerData = { kind: "explain", questionText: "Explain the gradient.", answers: [], text: "Synthetic answer", committed, dontKnow: false, answeredAt: "2026-10-26T00:00:00.000Z" };
const input = { question: answer.questionText, correctAnswer: committed };
const before = structuredClone(answer);
const host = document.createElement("div"); document.body.append(host);
const root = createRoot(host);
try {
await act(async () => root.render());
const paragraphs = host.querySelectorAll("p");
assert.equal(paragraphs.length, 3, "question plus two committed-answer paragraphs");
assert.ok(paragraphs[1]!.parentElement!.classList.contains("[&>p:first-child]:inline"));
assert.ok(!paragraphs[1]!.parentElement!.classList.contains("[&_p]:inline"));
assert.equal(host.querySelectorAll(".katex-error").length, 0);
assert.deepEqual(answer, before);
} finally { await act(async () => root.unmount()); host.remove(); }
});