import assert from "node:assert/strict";
import { after, before, test } from "node:test";

import { createServer } from "vite";

let server;
let createLatestAsyncCommitter;

before(async () => {
  server = await createServer({
    appType: "custom",
    logLevel: "silent",
    server: { middlewareMode: true },
  });
  ({ createLatestAsyncCommitter } = await server.ssrLoadModule(
    "/src/core/runtime/latestAsyncCommitter.ts",
  ));
});

after(async () => {
  await server?.close();
});

async function eventually(predicate, timeoutMs = 1_000) {
  const deadline = Date.now() + timeoutMs;
  while (!predicate()) {
    if (Date.now() >= deadline) throw new Error("Timed out waiting for async commit queue");
    await new Promise((resolve) => setTimeout(resolve, 0));
  }
}

test("viewer settings commits are serialized and intermediate slider values collapse", async () => {
  const calls = [];
  const resolvers = [];
  const settled = [];
  const committer = createLatestAsyncCommitter({
    commit(value) {
      calls.push(value);
      return new Promise((resolve) => resolvers.push(resolve));
    },
    onSettled(result) {
      settled.push(result);
    },
  });

  committer.enqueue({ accumulationSeconds: 1 });
  committer.enqueue({ accumulationSeconds: 2 });
  committer.enqueue({ accumulationSeconds: 12 });

  assert.deepEqual(calls, [{ accumulationSeconds: 1 }]);
  assert.equal(committer.isBusy(), true);
  resolvers.shift()(true);
  await eventually(() => calls.length === 2);

  assert.deepEqual(calls[1], { accumulationSeconds: 12 });
  assert.equal(settled[0].superseded, true);
  resolvers.shift()(true);
  await eventually(() => !committer.isBusy());

  assert.deepEqual(settled.map(({ value, applied, superseded }) => ({
    value,
    applied,
    superseded,
  })), [
    { value: { accumulationSeconds: 1 }, applied: true, superseded: true },
    { value: { accumulationSeconds: 12 }, applied: true, superseded: false },
  ]);
});

test("viewer settings commit failures settle as rejected without wedging the queue", async () => {
  const settled = [];
  const committer = createLatestAsyncCommitter({
    async commit(value) {
      if (value === "broken") throw new Error("offline");
      return true;
    },
    onSettled(result) {
      settled.push(result);
    },
  });

  committer.enqueue("broken");
  await eventually(() => settled.length === 1);
  committer.enqueue("healthy");
  await eventually(() => settled.length === 2);

  assert.deepEqual(settled.map(({ value, applied }) => ({ value, applied })), [
    { value: "broken", applied: false },
    { value: "healthy", applied: true },
  ]);
  assert.equal(committer.isBusy(), false);
});

test("waitForIdle resolves only after the running commit and latest queued value settle", async () => {
  const calls = [];
  const resolvers = [];
  const committer = createLatestAsyncCommitter({
    commit(value) {
      calls.push(value);
      return new Promise((resolve) => resolvers.push(resolve));
    },
  });

  committer.enqueue("initial");
  committer.enqueue("latest");
  let idle = false;
  const idlePromise = committer.waitForIdle().then(() => {
    idle = true;
  });

  await Promise.resolve();
  assert.equal(idle, false);
  resolvers.shift()(true);
  await eventually(() => calls.length === 2);
  assert.deepEqual(calls, ["initial", "latest"]);
  assert.equal(idle, false);

  resolvers.shift()(true);
  await idlePromise;
  assert.equal(idle, true);
  assert.equal(committer.isBusy(), false);
});

test("waitForIdle observes work enqueued by onSettled and resolves immediately when idle", async () => {
  const calls = [];
  let committer;
  committer = createLatestAsyncCommitter({
    async commit(value) {
      calls.push(value);
      return true;
    },
    onSettled({ value }) {
      if (value === "first") committer.enqueue("follow-up");
    },
  });

  await committer.waitForIdle();
  committer.enqueue("first");
  await committer.waitForIdle();

  assert.deepEqual(calls, ["first", "follow-up"]);
  assert.equal(committer.isBusy(), false);
});

test("layout serialization can flush a staged setting and read the confirmed latest value", async () => {
  let staged = { pointSize: 2 };
  let confirmed = { pointSize: 1 };
  let resolveCommit;
  const committer = createLatestAsyncCommitter({
    commit() {
      return new Promise((resolve) => {
        resolveCommit = resolve;
      });
    },
    onSettled({ value, applied }) {
      if (applied) confirmed = value;
    },
  });

  const saveLayout = async () => {
    committer.enqueue(staged);
    await committer.waitForIdle();
    return { sceneSettings: confirmed };
  };

  const savePromise = saveLayout();
  staged = { pointSize: 9 };
  let saved = false;
  void savePromise.then(() => {
    saved = true;
  });
  await Promise.resolve();
  assert.equal(saved, false);

  resolveCommit(true);
  const layout = await savePromise;
  assert.deepEqual(layout, { sceneSettings: { pointSize: 2 } });
});
