// API tests for the collaborative checklist toggle endpoint
// `POST /notes/:id/checklist/:itemId/toggle`. Owner + active recipients
// can flip an item; the change is mirrored to every recipient snapshot.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import pg from "pg";

const API_BASE = process.env.TEST_API_BASE ?? "http://localhost:8080";
const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error("DATABASE_URL must be set to run these tests");
}

const TEST_PASSWORD = "TestPass123!";
const TEST_PASSWORD_HASH =
  "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";

const pool = new pg.Pool({ connectionString: DATABASE_URL });
const createdUserIds = [];
const createdNoteIds = [];

function uniq() {
  return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}

async function createUser(prefix) {
  const username = `${prefix}_${uniq()}`;
  const { rows } = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, $4, 'en', true) RETURNING id`,
    [username, `${username}@example.com`, TEST_PASSWORD_HASH, prefix],
  );
  const id = rows[0].id;
  createdUserIds.push(id);
  await pool.query(
    `INSERT INTO user_roles (user_id, role_id)
     SELECT $1, id FROM roles WHERE name = 'user'`,
    [id],
  );
  return { id, username };
}

async function login(username) {
  const res = await fetch(`${API_BASE}/api/auth/login`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password: TEST_PASSWORD }),
  });
  assert.equal(res.status, 200);
  const sid = res.headers
    .get("set-cookie")
    .split(",")
    .map((c) => c.split(";")[0].trim())
    .find((c) => c.startsWith("connect.sid="));
  return sid;
}

async function api(path, cookie, init = {}) {
  return fetch(`${API_BASE}/api${path}`, {
    ...init,
    headers: {
      Cookie: cookie,
      "Content-Type": "application/json",
      ...(init.headers ?? {}),
    },
  });
}

async function createChecklistNote(cookie, items) {
  const res = await api("/notes", cookie, {
    method: "POST",
    body: JSON.stringify({
      title: "Shared list",
      kind: "checklist",
      items,
      color: "blue",
    }),
  });
  assert.ok(res.status === 200 || res.status === 201);
  const json = await res.json();
  createdNoteIds.push(json.id);
  return json;
}

after(async () => {
  if (createdNoteIds.length > 0) {
    await pool.query(`DELETE FROM note_recipients WHERE note_id = ANY($1::int[])`, [createdNoteIds]);
    await pool.query(`DELETE FROM notes WHERE id = ANY($1::int[])`, [createdNoteIds]);
  }
  if (createdUserIds.length > 0) {
    await pool.query(`DELETE FROM notifications WHERE user_id = ANY($1::int[])`, [createdUserIds]);
    await pool.query(`DELETE FROM user_roles WHERE user_id = ANY($1::int[])`, [createdUserIds]);
    await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [createdUserIds]);
  }
  await pool.end();
});

test("owner can toggle a checklist item; live note + recipient snapshot both update", async () => {
  const owner = await createUser("cl_owner");
  const recipient = await createUser("cl_recipient");
  const oCookie = await login(owner.username);

  const note = await createChecklistNote(oCookie, [
    { id: "a", text: "Buy milk", done: false },
    { id: "b", text: "Walk dog", done: false },
  ]);
  await api(`/notes/${note.id}/send`, oCookie, {
    method: "POST",
    body: JSON.stringify({ recipientUserIds: [recipient.id] }),
  });

  const res = await api(`/notes/${note.id}/checklist/a/toggle`, oCookie, {
    method: "POST",
    body: JSON.stringify({ done: true }),
  });
  assert.equal(res.status, 200);
  const body = await res.json();
  assert.equal(body.success, true);
  assert.equal(body.items.find((i) => i.id === "a").done, true);

  // Live note row updated.
  const { rows: liveRows } = await pool.query(
    `SELECT items FROM notes WHERE id = $1`,
    [note.id],
  );
  assert.equal(liveRows[0].items.find((i) => i.id === "a").done, true);
  // Recipient snapshot mirrored.
  const { rows: snapRows } = await pool.query(
    `SELECT items FROM note_recipients WHERE note_id = $1`,
    [note.id],
  );
  assert.equal(snapRows[0].items.find((i) => i.id === "a").done, true);
});

test("recipient can toggle a checklist item collaboratively", async () => {
  const owner = await createUser("cl_owner2");
  const recipient = await createUser("cl_recipient2");
  const oCookie = await login(owner.username);
  const rCookie = await login(recipient.username);

  const note = await createChecklistNote(oCookie, [
    { id: "x", text: "Task X", done: false },
  ]);
  await api(`/notes/${note.id}/send`, oCookie, {
    method: "POST",
    body: JSON.stringify({ recipientUserIds: [recipient.id] }),
  });

  const res = await api(`/notes/${note.id}/checklist/x/toggle`, rCookie, {
    method: "POST",
    body: JSON.stringify({ done: true }),
  });
  assert.equal(res.status, 200, "active recipient must be able to toggle");
  // Owner sees the change in their live note.
  const { rows } = await pool.query(`SELECT items FROM notes WHERE id = $1`, [note.id]);
  assert.equal(rows[0].items[0].done, true);
});

test("non-recipient gets 403 when toggling", async () => {
  const owner = await createUser("cl_owner3");
  const recipient = await createUser("cl_recipient3");
  const stranger = await createUser("cl_stranger3");
  const oCookie = await login(owner.username);
  const sCookie = await login(stranger.username);

  const note = await createChecklistNote(oCookie, [
    { id: "y", text: "Y", done: false },
  ]);
  await api(`/notes/${note.id}/send`, oCookie, {
    method: "POST",
    body: JSON.stringify({ recipientUserIds: [recipient.id] }),
  });
  const res = await api(`/notes/${note.id}/checklist/y/toggle`, sCookie, {
    method: "POST",
    body: JSON.stringify({ done: true }),
  });
  assert.equal(res.status, 403);
});

test("archived recipient gets 403 when toggling", async () => {
  const owner = await createUser("cl_owner4");
  const recipient = await createUser("cl_recipient4");
  const oCookie = await login(owner.username);
  const rCookie = await login(recipient.username);

  const note = await createChecklistNote(oCookie, [
    { id: "z", text: "Z", done: false },
  ]);
  await api(`/notes/${note.id}/send`, oCookie, {
    method: "POST",
    body: JSON.stringify({ recipientUserIds: [recipient.id] }),
  });
  await api(`/notes/${note.id}/archive`, rCookie, {
    method: "POST",
    body: JSON.stringify({ archived: true }),
  });

  const res = await api(`/notes/${note.id}/checklist/z/toggle`, rCookie, {
    method: "POST",
    body: JSON.stringify({ done: true }),
  });
  assert.equal(res.status, 403);
});

test("non-checklist note rejects the toggle with 400", async () => {
  const owner = await createUser("cl_owner5");
  const oCookie = await login(owner.username);

  const res = await api("/notes", oCookie, {
    method: "POST",
    body: JSON.stringify({ title: "Plain", content: "hi", kind: "text" }),
  });
  const note = await res.json();
  createdNoteIds.push(note.id);

  const tog = await api(`/notes/${note.id}/checklist/a/toggle`, oCookie, {
    method: "POST",
    body: JSON.stringify({ done: true }),
  });
  assert.equal(tog.status, 400);
});

test("unknown itemId yields 404", async () => {
  const owner = await createUser("cl_owner6");
  const oCookie = await login(owner.username);
  const note = await createChecklistNote(oCookie, [
    { id: "real", text: "T", done: false },
  ]);
  const res = await api(`/notes/${note.id}/checklist/missing/toggle`, oCookie, {
    method: "POST",
    body: JSON.stringify({ done: true }),
  });
  assert.equal(res.status, 404);
});

test("invalid body (missing done boolean) yields 400", async () => {
  const owner = await createUser("cl_owner7");
  const oCookie = await login(owner.username);
  const note = await createChecklistNote(oCookie, [
    { id: "a", text: "A", done: false },
  ]);
  const res = await api(`/notes/${note.id}/checklist/a/toggle`, oCookie, {
    method: "POST",
    body: JSON.stringify({}),
  });
  assert.equal(res.status, 400);
});
