// Mock collab regression for the AMBIGUOUS-id refusal in patch_node / delete_node
// (#159, PR #185 review pt 1). When a page has TWO blocks sharing one attrs.id
// (Docmost duplicates block ids on copy/paste), the transform's
// `if (replaced !== 1) return null` / `if (deleted !== 1) return null` guard must
// SKIP the collab write, and the call must then reject with an "ambiguous" error.
//
// The replaceNodeById/deleteNodeById counts and assertUnambiguousMatch are unit-
// tested in isolation (test/unit/node-ops.test.mjs); this exercises the END-TO-END
// wiring through the real client method + a live Hocuspocus collab doc, so a
// regression that loosened the guard (e.g. back to `=== 0`) would be caught here
// where the isolated unit tests would not.
//
// Unlike the other mock tests (which deliberately avoid the collab WebSocket), this
// one DOES stand up a real Hocuspocus server seeded with a duplicate-id document,
// so the transform actually runs against a live two-match doc.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import { WebSocketServer } from "ws";
import { Hocuspocus } from "@hocuspocus/server";
import { DocmostClient } from "../../build/client.js";
import { buildYDoc } from "../../build/lib/collaboration.js";

// A document with TWO paragraphs sharing the SAME attrs.id — the duplicate-id
// shape replaceNodeById/deleteNodeById report as `count === 2` (ambiguous).
const DUP_ID = "dup-block-id";
function seedDoc() {
  return {
    type: "doc",
    content: [
      {
        type: "paragraph",
        attrs: { id: DUP_ID },
        content: [{ type: "text", text: "first copy" }],
      },
      {
        type: "paragraph",
        attrs: { id: DUP_ID },
        content: [{ type: "text", text: "second copy" }],
      },
    ],
  };
}

// Stand up an HTTP server that authenticates + hands out a collab token AND
// upgrades /collab to a Hocuspocus instance seeded with the duplicate-id doc.
// `state.changed` flips true the instant Hocuspocus applies ANY client document
// update — it must stay false, proving the ambiguous write was never sent. (We
// track onChange, which fires synchronously per update, NOT onStoreDocument,
// which is debounced and would not fire before the test tears the server down —
// making a real clobbering write look clean.)
async function spawnCollabStack() {
  const state = { changed: false };

  const hocuspocus = new Hocuspocus({
    quiet: true,
    // Seed every requested document with a fresh duplicate-id Y.Doc, encoded with
    // the SAME docmost extensions the client reads with (so attrs.id round-trips).
    async onLoadDocument() {
      return buildYDoc(seedDoc());
    },
    // Fires immediately on any client-driven document update. A real (clobbering)
    // write would trip this; the ambiguous guard must keep it from firing.
    async onChange() {
      state.changed = true;
    },
  });

  const wss = new WebSocketServer({ noServer: true });

  const server = http.createServer((req, res) => {
    let raw = "";
    req.on("data", (c) => (raw += c));
    req.on("end", () => {
      if (req.url === "/api/auth/login") {
        res.writeHead(200, {
          "Content-Type": "application/json",
          "Set-Cookie": "authToken=t; Path=/; HttpOnly",
        });
        res.end(JSON.stringify({ success: true }));
        return;
      }
      if (req.url === "/api/auth/collab-token") {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ data: { token: "collab-jwt" } }));
        return;
      }
      res.writeHead(404, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ message: "not found" }));
    });
  });

  // buildCollabWsUrl maps http://host:port/api -> ws://host:port/collab.
  server.on("upgrade", (request, socket, head) => {
    if (!request.url || !request.url.startsWith("/collab")) {
      socket.destroy();
      return;
    }
    wss.handleUpgrade(request, socket, head, (ws) => {
      hocuspocus.handleConnection(ws, request);
    });
  });

  const baseURL = await new Promise((resolve) => {
    server.listen(0, "127.0.0.1", () => {
      const { port } = server.address();
      resolve(`http://127.0.0.1:${port}/api`);
    });
  });

  openStacks.push({ server, hocuspocus });
  return { state, baseURL };
}

const openStacks = [];
after(async () => {
  await Promise.all(
    openStacks.map(
      ({ server, hocuspocus }) =>
        new Promise((resolve) => {
          server.close(() => {
            Promise.resolve(hocuspocus.destroy?.()).finally(resolve);
          });
        }),
    ),
  );
});

test("patch_node REFUSES an ambiguous (duplicate) id without writing to collab", async () => {
  const { state, baseURL } = await spawnCollabStack();
  const client = new DocmostClient(baseURL, "user@example.com", "pw");

  await assert.rejects(
    () =>
      client.patchNode("page-1", DUP_ID, {
        type: "paragraph",
        content: [{ type: "text", text: "replacement" }],
      }),
    /ambiguous/i,
    "patch_node must reject a duplicate-id target with an 'ambiguous' error",
  );

  assert.equal(
    state.changed,
    false,
    "the collab document must NEVER be written when the id is ambiguous",
  );
});

test("delete_node REFUSES an ambiguous (duplicate) id without writing to collab", async () => {
  const { state, baseURL } = await spawnCollabStack();
  const client = new DocmostClient(baseURL, "user@example.com", "pw");

  await assert.rejects(
    () => client.deleteNode("page-2", DUP_ID),
    /ambiguous/i,
    "delete_node must reject a duplicate-id target with an 'ambiguous' error",
  );

  assert.equal(
    state.changed,
    false,
    "the collab document must NEVER be written when the id is ambiguous",
  );
});
