import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { expect, test } from "@playwright/test";

const STREAM_CHUNK_SIZE = 18;
const STREAM_CHUNK_DELAY_MS = 20;
const PROGRESS_GEOMETRY_TOLERANCE_PX = 0.1;

let streamServer: Server;
let streamServerUrl: string;
let appendProgress: (() => void) | null = null;
let failProgress: (() => void) | null = null;
let releaseStream: (() => void) | null = null;

test.beforeAll(async () => {
  streamServer = createServer((_request, response) => {
    response.writeHead(200, {
      "Access-Control-Allow-Origin": "*",
      "Cache-Control": "no-cache",
      "Content-Type": "text/event-stream",
      "X-Vercel-AI-UI-Message-Stream": "v1"
    });

    const content = createLongMarkdownResponse();
    const chunks = splitIntoCodePointChunks(content, STREAM_CHUNK_SIZE);
    const send = (chunk: Record<string, unknown>) => {
      response.write(`data: ${JSON.stringify(chunk)}\n\n`);
    };

    send({ type: "start", messageId: "assistant-stream-stability" });
    send({
      type: "data-progress",
      id: "inspect-context",
      data: {
        id: "inspect-context",
        phase: "planning",
        status: "running",
        title: "正在分析运行上下文",
        detail: "核对泵站、液位与降雨过程。",
        started_at: Date.now()
      }
    });
    send({
      type: "data-progress",
      id: "inspect-rainfall",
      data: {
        id: "inspect-rainfall",
        phase: "evidence",
        status: "completed",
        title: "正在核对降雨过程与汇水区响应关系",
        detail: "读取最近十二小时分钟级降雨序列，并检查每个汇水区的响应延迟是否处于合理范围。",
        started_at: Date.now()
      }
    });
    send({
      type: "data-progress",
      id: "inspect-level",
      data: {
        id: "inspect-level",
        phase: "evidence",
        status: "completed",
        title: "正在核对液位连续性",
        detail: "排除缺测与异常峰值。",
        started_at: Date.now()
      }
    });
    send({
      type: "data-todo_update",
      id: "stream-stability-plan",
      data: {
        session_id: "stream-stability-session",
        message_id: "assistant-stream-stability",
        created_at: Date.now(),
        todos: [
          { id: "inspect-pump", content: "检查泵站运行状态", status: "in_progress" },
          { id: "inspect-level", content: "核对液位计连续性", status: "pending" },
          { id: "assess-overflow", content: "判断溢流风险", status: "pending" }
        ]
      }
    });
    send({ type: "text-start", id: "answer" });

    let index = 0;
    let timer: ReturnType<typeof setInterval> | null = null;
    const sendNextTextChunk = () => {
      const delta = chunks[index];
      if (delta !== undefined) {
        index += 1;
        send({ type: "text-delta", id: "answer", delta });
        send({
          type: "data-stream_token",
          data: {
            session_id: "stream-stability-session",
            message_id: "assistant-stream-stability",
            content: delta
          },
          transient: true
        });
        return;
      }

      if (timer) {
        clearInterval(timer);
        timer = null;
      }
      send({
        type: "data-progress",
        id: "assess-risk",
        data: {
          id: "assess-risk",
          phase: "analysis",
          status: "completed",
          title: "溢流风险判断完成",
          detail: "已完成风险点位排序。",
          ended_at: Date.now()
        }
      });
      send({ type: "text-end", id: "answer" });
      send({ type: "finish", finishReason: "stop" });
      response.end();
    };

    while (index < 3 && index < chunks.length) {
      sendNextTextChunk();
    }
    appendProgress = () => {
      send({
        type: "data-progress",
        id: "assess-risk",
        data: {
          id: "assess-risk",
          phase: "analysis",
          status: "running",
          title: "正在判断溢流风险",
          detail: "结合流量突变与液位趋势形成判断。",
          started_at: Date.now()
        }
      });
    };

    failProgress = () => {
      send({
        type: "data-progress",
        id: "inspect-level",
        data: {
          id: "inspect-level",
          phase: "evidence",
          status: "error",
          title: "液位连续性核对失败",
          detail: "监测数据读取超时。",
          ended_at: Date.now()
        }
      });
    };

    releaseStream = () => {
      if (!timer) {
        timer = setInterval(sendNextTextChunk, STREAM_CHUNK_DELAY_MS);
      }
    };

    response.on("close", () => {
      if (timer) {
        clearInterval(timer);
      }
    });
  });

  await new Promise<void>((resolve) => streamServer.listen(0, "127.0.0.1", resolve));
  const address = streamServer.address() as AddressInfo;
  streamServerUrl = `http://127.0.0.1:${address.port}/stream`;
});

test.afterAll(async () => {
  await new Promise<void>((resolve, reject) => {
    streamServer.close((error) => (error ? reject(error) : resolve()));
  });
});

test("stream completion and the plan card remain positionally stable", async ({ page }) => {
  await page.route("**/api/v1/agent/chat/**", async (route) => {
    const path = new URL(route.request().url()).pathname;

    if (path.endsWith("/stream")) {
      await route.continue({ url: streamServerUrl });
      return;
    }

    const body = path.endsWith("/models")
      ? {
          default_model: "test/model",
          models: [
            {
              id: "test/model",
              label: "测试模型",
              description: "流式稳定性测试",
              icon: "bolt"
            }
          ]
        }
      : path.endsWith("/sessions")
        ? { sessions: [] }
        : {};

    await route.fulfill({ json: body });
  });

  await page.goto("/", { waitUntil: "domcontentloaded" });

  await expect(
    page.getByRole("button", { name: "选择 Agent 模型" }).filter({ visible: true }).first()
  ).toContainText("测试模型");
  const prompt = page
    .getByPlaceholder(/输入调度问题/)
    .filter({ visible: true })
    .first();
  await expect(prompt).toBeVisible();
  await prompt.fill("检查流式响应稳定性");
  const sendButton = page
    .getByRole("button", { name: "发送 Agent 指令" })
    .filter({ visible: true })
    .first();
  await expect(sendButton).toBeEnabled();

  await page.evaluate(() => {
    type StreamSample = {
      clientHeight: number;
      planItemTransform: string | null;
      planSectionTransform: string | null;
      planTransform: string | null;
      scrollHeight: number;
      scrollTop: number;
      time: number;
      transform: string;
    };

    const samples: StreamSample[] = [];
    const startedAt = performance.now();
    const state = window as typeof window & {
      __agentStreamSamples?: StreamSample[];
      __agentStreamSamplingDone?: boolean;
    };
    state.__agentStreamSamples = samples;
    state.__agentStreamSamplingDone = false;

    const sample = () => {
      const message = document.querySelector<HTMLElement>(".agent-panel-message");
      const wrapper = message?.parentElement?.parentElement;
      const scroll = document.querySelector<HTMLElement>(".agent-conversation-scroll");
      const plan = Array.from(document.querySelectorAll<HTMLElement>(".agent-panel-nested")).find(
        (element) => element.textContent?.includes("计划")
      );
      const planWrapper = plan?.parentElement;
      const planSection = planWrapper?.parentElement;
      const planItem = plan?.querySelector("li");

      if (message && wrapper && scroll) {
        samples.push({
          clientHeight: scroll.clientHeight,
          planItemTransform: planItem ? getComputedStyle(planItem).transform : null,
          planSectionTransform: planSection ? getComputedStyle(planSection).transform : null,
          planTransform: planWrapper ? getComputedStyle(planWrapper).transform : null,
          scrollHeight: scroll.scrollHeight,
          scrollTop: scroll.scrollTop,
          time: performance.now() - startedAt,
          transform: getComputedStyle(wrapper).transform
        });
      }

      if (!state.__agentStreamSamplingDone) {
        requestAnimationFrame(sample);
      }
    };

    requestAnimationFrame(sample);
  });

  await sendButton.click();
  const userMessage = page.locator(".agent-panel-user-message");
  const assistantMessage = page.locator(".agent-panel-message").first();
  await expect(userMessage).toHaveCSS("background-color", "rgba(224, 229, 249, 0.92)");
  expect(
    await userMessage.evaluate((element) => getComputedStyle(element).backgroundColor)
  ).not.toBe(
    await assistantMessage.evaluate((element) => getComputedStyle(element).backgroundColor)
  );
  await expect
    .poll(() => userMessage.evaluate((element) => getComputedStyle(element).boxShadow))
    .not.toBe("none");
  await expect
    .poll(() => assistantMessage.evaluate((element) => getComputedStyle(element).boxShadow))
    .toBe(await userMessage.evaluate((element) => getComputedStyle(element).boxShadow));
  const operationalBrief = page.locator(".agent-panel-operational-float");
  await expect(operationalBrief).toBeVisible();
  await expect(operationalBrief).toHaveCSS("position", "absolute");
  await expect(operationalBrief).not.toHaveClass(/agent-panel-floating-acrylic/);
  await expect(operationalBrief).toHaveCSS("background-color", "rgba(255, 255, 255, 0.86)");
  await expect(operationalBrief).toHaveCSS("border-radius", "16px");
  await expect(operationalBrief).toHaveCSS("background-image", "none");
  await expect(operationalBrief).toHaveCSS("border-top-width", "0px");
  await expect(operationalBrief).toHaveCSS(
    "box-shadow",
    "rgba(15, 33, 55, 0.14) 0px 12px 32px 0px, rgba(15, 33, 55, 0.08) 0px 3px 10px 0px"
  );
  await expect
    .poll(() => operationalBrief.evaluate((element) => getComputedStyle(element).boxShadow))
    .not.toContain("inset");
  await expect
    .poll(() => operationalBrief.evaluate((element) => getComputedStyle(element).backdropFilter))
    .toContain("blur(");
  const operationalBriefBox = await operationalBrief.boundingBox();
  const conversationBox = await page.locator(".agent-panel-conversation-canvas").boundingBox();
  expect(operationalBriefBox).not.toBeNull();
  expect(conversationBox).not.toBeNull();
  expect(operationalBriefBox!.y).toBeLessThan(conversationBox!.y + conversationBox!.height);
  await expect(page.locator(".agent-panel-conversation-content-has-brief")).toHaveCSS(
    "padding-top",
    "208px"
  );

  const liveProgress = page.getByRole("list", { name: "最近执行进度" });
  await expect(liveProgress.locator("li")).toHaveCount(3);
  await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
  await expect(liveProgress).toHaveCSS("height", "156px");
  await expect.poll(() => liveProgress.evaluate((list) => list.style.height)).toBe("auto");

  await page.evaluate(() => {
    const state = window as typeof window & {
      __agentMinVisibleProgressRows?: number;
      __agentProgressGeometry?: {
        maxContainerHeight: number;
        maxListHeight: number;
        minContainerHeight: number;
        minListHeight: number;
      };
      __agentProgressWindowSampleDone?: boolean;
    };
    const startedAt = performance.now();
    state.__agentMinVisibleProgressRows = Number.POSITIVE_INFINITY;
    state.__agentProgressGeometry = {
      maxContainerHeight: Number.NEGATIVE_INFINITY,
      maxListHeight: Number.NEGATIVE_INFINITY,
      minContainerHeight: Number.POSITIVE_INFINITY,
      minListHeight: Number.POSITIVE_INFINITY
    };
    state.__agentProgressWindowSampleDone = false;

    const sample = () => {
      const list = document.querySelector<HTMLOListElement>('ol[aria-label="最近执行进度"]');
      const rows = list?.children.length ?? 0;
      state.__agentMinVisibleProgressRows = Math.min(
        state.__agentMinVisibleProgressRows ?? rows,
        rows
      );
      if (list && state.__agentProgressGeometry) {
        const listHeight = list.getBoundingClientRect().height;
        const containerHeight = list.parentElement?.getBoundingClientRect().height ?? 0;
        state.__agentProgressGeometry.minListHeight = Math.min(
          state.__agentProgressGeometry.minListHeight,
          listHeight
        );
        state.__agentProgressGeometry.maxListHeight = Math.max(
          state.__agentProgressGeometry.maxListHeight,
          listHeight
        );
        state.__agentProgressGeometry.minContainerHeight = Math.min(
          state.__agentProgressGeometry.minContainerHeight,
          containerHeight
        );
        state.__agentProgressGeometry.maxContainerHeight = Math.max(
          state.__agentProgressGeometry.maxContainerHeight,
          containerHeight
        );
      }
      if (performance.now() - startedAt < 400) {
        requestAnimationFrame(sample);
      } else {
        state.__agentProgressWindowSampleDone = true;
      }
    };

    requestAnimationFrame(sample);
  });
  triggerProgressAppend();
  await expect(liveProgress.getByText("正在判断溢流风险", { exact: true })).toBeVisible();
  await expect(liveProgress.locator("li")).toHaveCount(3);
  await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toHaveCount(0);
  await expect
    .poll(() =>
      page.evaluate(
        () =>
          (window as typeof window & { __agentProgressWindowSampleDone?: boolean })
            .__agentProgressWindowSampleDone ?? false
      )
    )
    .toBe(true);
  expect(
    await page.evaluate(
      () =>
        (window as typeof window & { __agentMinVisibleProgressRows?: number })
          .__agentMinVisibleProgressRows
    )
  ).toBeGreaterThanOrEqual(3);
  const progressGeometry = await page.evaluate(
    () =>
      (
        window as typeof window & {
          __agentProgressGeometry?: {
            maxContainerHeight: number;
            maxListHeight: number;
            minContainerHeight: number;
            minListHeight: number;
          };
        }
      ).__agentProgressGeometry
  );
  expect(progressGeometry).toBeDefined();
  expect(progressGeometry!.maxListHeight - progressGeometry!.minListHeight).toBeLessThanOrEqual(
    PROGRESS_GEOMETRY_TOLERANCE_PX
  );
  expect(
    progressGeometry!.maxContainerHeight - progressGeometry!.minContainerHeight
  ).toBeLessThanOrEqual(PROGRESS_GEOMETRY_TOLERANCE_PX);

  const liveProgressRowHeights = await liveProgress
    .locator("li")
    .evaluateAll((rows) => rows.map((row) => row.getBoundingClientRect().height));
  expect(liveProgressRowHeights.every((height) => Math.abs(height - 48) <= 0.01)).toBe(true);
  await expect(liveProgress.locator("li").first().locator("span.min-w-0 > span").first()).toHaveCSS(
    "white-space",
    "nowrap"
  );
  const progressToggle = page
    .getByRole("button", { name: /执行进度/ })
    .filter({ visible: true })
    .first();
  await expect(progressToggle).toBeEnabled();
  await expect(progressToggle).toHaveAttribute("data-state", "closed");
  await expect
    .poll(async () => (await progressToggle.boundingBox())?.height ?? 0)
    .toBeGreaterThanOrEqual(40);
  await progressToggle.click();
  await expect(progressToggle).toHaveAttribute("data-state", "open");
  const expandedLiveProgress = page.getByRole("list", { name: "全部执行进度" });
  await expect(expandedLiveProgress.locator("li")).toHaveCount(4);
  await expect(expandedLiveProgress.locator(".status-badge")).toHaveCount(0);
  await expect(expandedLiveProgress.locator('[data-progress-status="running"]').first()).toHaveCSS(
    "color",
    "oklch(0.546 0.245 262.881)"
  );
  await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
  await expect(
    expandedLiveProgress.getByText("正在核对降雨过程与汇水区响应关系", { exact: true })
  ).toHaveCSS("white-space", "normal");
  await progressToggle.click();
  await expect(page.getByRole("list", { name: "最近执行进度" }).locator("li")).toHaveCount(3);

  triggerProgressFailure();
  await expect(liveProgress.getByText("液位连续性核对失败", { exact: true })).toBeVisible();
  await expect(page.locator('[data-progress-status="error"]')).toHaveCSS("transform", "none");
  resumeStream();

  await expect(
    page.getByText("分析完成", { exact: true }).filter({ visible: true }).first()
  ).toBeVisible({
    timeout: 10_000
  });
  await expect(progressToggle.locator("../..")).toContainText("步骤4/4");
  await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
  await expect(
    page.getByText("溢流风险判断完成", { exact: true }).filter({ visible: true })
  ).toHaveCount(0);
  await expect
    .poll(async () =>
      page.evaluate(() => {
        const scroll = document.querySelector<HTMLElement>(".agent-conversation-scroll");
        return scroll
          ? Math.abs(scroll.scrollHeight - scroll.clientHeight - scroll.scrollTop)
          : Number.POSITIVE_INFINITY;
      })
    )
    .toBeLessThanOrEqual(2);
  await expect
    .poll(() =>
      page.evaluate(() => {
        const samples = (
          window as typeof window & {
            __agentStreamSamples?: Array<{
              clientHeight: number;
              scrollHeight: number;
              scrollTop: number;
            }>;
          }
        ).__agentStreamSamples;
        const sample = samples?.at(-1);
        return sample
          ? Math.abs(sample.scrollHeight - sample.clientHeight - sample.scrollTop)
          : Number.POSITIVE_INFINITY;
      })
    )
    .toBeLessThanOrEqual(2);
  await page.evaluate(() => {
    (window as typeof window & { __agentStreamSamplingDone?: boolean }).__agentStreamSamplingDone =
      true;
  });

  const samples = await page.evaluate(
    () =>
      (
        window as typeof window & {
          __agentStreamSamples?: Array<{
            clientHeight: number;
            planItemTransform: string | null;
            planSectionTransform: string | null;
            planTransform: string | null;
            scrollHeight: number;
            scrollTop: number;
            time: number;
            transform: string;
          }>;
        }
      ).__agentStreamSamples ?? []
  );

  expect(samples.length).toBeGreaterThan(5);
  const finalSample = samples.at(-1);
  expect(finalSample).toBeDefined();
  expect(finalSample).toMatchObject({
    planItemTransform: "none",
    planSectionTransform: "none",
    planTransform: "none",
    transform: "none"
  });
  expect(
    Math.abs(finalSample!.scrollHeight - finalSample!.clientHeight - finalSample!.scrollTop)
  ).toBeLessThanOrEqual(2);

  await progressToggle.click();
  const expandedProgress = page.getByRole("list", { name: "全部执行进度" });
  await expect(expandedProgress.locator("li")).toHaveCount(4);
  await expect(
    page.getByText("正在分析运行上下文", { exact: true }).filter({ visible: true }).first()
  ).toBeVisible();
  const expandedLongTitle = expandedProgress.getByText("正在核对降雨过程与汇水区响应关系", {
    exact: true
  });
  await expect(expandedLongTitle).toHaveCSS("white-space", "normal");
  await expect(expandedProgress.locator("li").nth(1)).not.toHaveCSS("height", "48px");

  await progressToggle.click();
  await prompt.fill("验证展开状态在流结束后保持");
  await sendButton.click();

  const latestProgressToggle = page
    .getByRole("button", { name: /执行进度/ })
    .filter({ visible: true })
    .last();
  const latestProgressRegion = latestProgressToggle.locator("../..");
  const latestExpandedProgress = latestProgressRegion.getByRole("list", {
    name: "全部执行进度"
  });
  await expect(latestProgressRegion).toContainText("最近3/3");
  triggerProgressAppend();
  await expect(
    page.getByRole("list", { name: "最近执行进度" }).getByText("正在判断溢流风险", { exact: true })
  ).toBeVisible();
  await latestProgressToggle.click();
  await expect(latestProgressRegion).toContainText("全部4/4");
  resumeStream();

  await expect(latestProgressRegion).toContainText("全部4/4", { timeout: 10_000 });
  await expect(latestExpandedProgress).toBeVisible();
  await expect(latestExpandedProgress.getByText("溢流风险判断完成", { exact: true })).toBeVisible();

  const conversationScroll = page.locator(".agent-conversation-scroll");
  await conversationScroll.hover();
  await page.mouse.wheel(0, -2400);
  const scrollToLatestButton = page.getByRole("button", { name: "滚动到最新消息" });
  await expect(scrollToLatestButton).toBeVisible();
  await expect(scrollToLatestButton).toHaveCSS("background-color", "rgba(255, 255, 255, 0.86)");
  await expect
    .poll(() =>
      scrollToLatestButton.evaluate((element) => getComputedStyle(element).backdropFilter)
    )
    .toContain("blur(");
  await expect
    .poll(() => scrollToLatestButton.evaluate((element) => getComputedStyle(element).boxShadow))
    .not.toBe("none");
  const readAcrylicMaterial = (element: HTMLElement) => {
    const style = getComputedStyle(element);

    return {
      backdropFilter: style.backdropFilter,
      backgroundColor: style.backgroundColor,
      backgroundImage: style.backgroundImage,
      boxShadow: style.boxShadow
    };
  };
  expect(await scrollToLatestButton.evaluate(readAcrylicMaterial)).toEqual(
    await page.locator(".agent-panel-composer").evaluate(readAcrylicMaterial)
  );

  const selectableAssistantMessage = page.getByRole("heading", { name: "诊断结果" }).first();
  await selectableAssistantMessage.evaluate((element) => {
    const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
    let textNode = walker.nextNode();
    while (textNode && !textNode.textContent?.trim()) {
      textNode = walker.nextNode();
    }
    if (!textNode?.textContent) {
      throw new Error("Assistant message does not contain selectable text");
    }

    const range = document.createRange();
    range.setStart(textNode, 0);
    range.setEnd(textNode, Math.min(8, textNode.textContent.length));
    const selection = window.getSelection();
    selection?.removeAllRanges();
    selection?.addRange(range);
    element.dispatchEvent(new PointerEvent("pointerup", { bubbles: true }));
  });

  const speechSelectionIconMotion = await page.evaluate(async () => {
    const waitForAnimationFrame = () =>
      new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
    const deadline = performance.now() + 1_000;
    let button: HTMLButtonElement | undefined;
    while (!button && performance.now() < deadline) {
      button = Array.from(document.querySelectorAll<HTMLButtonElement>("button")).find((element) =>
        element.textContent?.includes("从这里开始朗读")
      );
      if (!button) await waitForAnimationFrame();
    }
    const icon = button?.querySelector("svg");
    if (!button || !icon) {
      throw new Error("Speech selection action did not render with an icon");
    }

    const samples: Array<{ x: number; y: number }> = [];
    const startedAt = performance.now();
    while (performance.now() - startedAt < 220) {
      const rect = icon.getBoundingClientRect();
      samples.push({
        x: rect.x + rect.width / 2,
        y: rect.y + rect.height / 2
      });
      await waitForAnimationFrame();
    }

    const xValues = samples.map((sample) => sample.x);
    const yValues = samples.map((sample) => sample.y);
    return {
      xSpread: Math.max(...xValues) - Math.min(...xValues),
      ySpread: Math.max(...yValues) - Math.min(...yValues)
    };
  });

  expect(speechSelectionIconMotion.xSpread).toBeLessThanOrEqual(0.1);
  expect(speechSelectionIconMotion.ySpread).toBeLessThanOrEqual(0.1);
  const speechSelectionButton = page.getByRole("button", { name: "从这里开始朗读" });
  await expect(speechSelectionButton).toBeVisible();
  await expect(speechSelectionButton).toHaveCSS("border-top-width", "0px");
  expect(await speechSelectionButton.evaluate(readAcrylicMaterial)).toEqual(
    await scrollToLatestButton.evaluate(readAcrylicMaterial)
  );
});

function createLongMarkdownResponse() {
  let content = "## 诊断结果\n\n";
  for (let index = 1; index <= 6; index += 1) {
    content += `### 区域 ${index}\n\n`;
    content +=
      "当前管网运行状态总体稳定，但部分区域存在水位持续升高、流量突变和监测数据缺失。需要结合实时工况核对监测点连续性。\n\n";
    content += "- 检查泵站运行状态与实时流量。\n";
    content += "- 核对液位计连续性和异常峰值。\n";
    content += "- 结合降雨过程判断溢流风险。\n\n";
  }
  return `${content}建议优先处置高风险点位，并持续跟踪后续变化。`;
}

function splitIntoCodePointChunks(content: string, chunkSize: number) {
  const codePoints = Array.from(content);
  const chunks: string[] = [];
  for (let index = 0; index < codePoints.length; index += chunkSize) {
    chunks.push(codePoints.slice(index, index + chunkSize).join(""));
  }
  return chunks;
}

function resumeStream() {
  if (!releaseStream) {
    throw new Error("Stream checkpoint was not reached");
  }

  const resume = releaseStream;
  releaseStream = null;
  resume();
}

function triggerProgressAppend() {
  if (!appendProgress) {
    throw new Error("Progress append checkpoint was not reached");
  }

  const append = appendProgress;
  appendProgress = null;
  append();
}

function triggerProgressFailure() {
  if (!failProgress) {
    throw new Error("Progress failure checkpoint was not reached");
  }

  const fail = failProgress;
  failProgress = null;
  fail();
}
