// per-recipient inbox delete + bulk delete.
// A recipient can detach their note_recipients row (DELETE
// /notes/received/:id and bulk variant) without touching the
// underlying note or other recipients' rows. Non-recipients get 404.
import { test, before, 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, `login expected 200, got ${res.status}`);
  const setCookie = res.headers.get("set-cookie");
  const sid = setCookie
    .split(",")
    .map((c) => c.split(";")[0].trim())
    .find((c) => c.startsWith("connect.sid="));
  assert.ok(sid, "expected connect.sid cookie");
  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 createNote(cookie, body) {
  const res = await api("/notes", cookie, {
    method: "POST",
    body: JSON.stringify({
      title: "Hi",
      content: "hello",
      color: "blue",
      ...body,
    }),
  });
  assert.ok(res.ok, `create note expected 2xx, got ${res.status}`);
  const json = await res.json();
  createdNoteIds.push(json.id);
  return json;
}

async function sendTo(cookie, noteId, recipientUserIds) {
  const res = await api(`/notes/${noteId}/send`, cookie, {
    method: "POST",
    body: JSON.stringify({ recipientUserIds }),
  });
  assert.ok(res.ok, `send expected 2xx, got ${res.status}`);
  return res.json();
}

after(async () => {
  if (createdNoteIds.length > 0) {
    await pool.query(
      `DELETE FROM note_replies WHERE note_id = ANY($1::int[])`,
      [createdNoteIds],
    );
    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("recipient can DELETE /notes/received/:id; row is gone, note + other recipients survive", async () => {
  const sender = await createUser("ndel_s");
  const r1 = await createUser("ndel_r1");
  const r2 = await createUser("ndel_r2");
  const sCookie = await login(sender.username);
  const r1Cookie = await login(r1.username);
  const r2Cookie = await login(r2.username);

  const note = await createNote(sCookie);
  await sendTo(sCookie, note.id, [r1.id, r2.id]);

  // r1 deletes their inbox row.
  const delRes = await api(`/notes/received/${note.id}`, r1Cookie, {
    method: "DELETE",
  });
  assert.ok(delRes.ok, `delete expected 2xx, got ${delRes.status}`);

  // r1's inbox no longer shows the note.
  const r1Inbox = await (await api(`/notes/received`, r1Cookie)).json();
  assert.ok(
    !r1Inbox.find((n) => n.id === note.id),
    "r1 must no longer see the note in their inbox",
  );

  // The note row + r2's recipient row are intact.
  const { rows: noteRows } = await pool.query(
    `SELECT id FROM notes WHERE id = $1`,
    [note.id],
  );
  assert.equal(noteRows.length, 1, "note row must still exist");
  const { rows: r2Rows } = await pool.query(
    `SELECT id FROM note_recipients WHERE note_id = $1 AND recipient_user_id = $2`,
    [note.id, r2.id],
  );
  assert.equal(r2Rows.length, 1, "r2 recipient row must be intact");

  // r2 still sees the note in their inbox.
  const r2Inbox = await (await api(`/notes/received`, r2Cookie)).json();
  assert.ok(
    r2Inbox.find((n) => n.id === note.id),
    "r2 must still see the note",
  );
});

test("non-recipient gets 404 on DELETE /notes/received/:id (does not leak)", async () => {
  const sender = await createUser("ndel_nrs");
  const recipient = await createUser("ndel_nrr");
  const stranger = await createUser("ndel_nrx");
  const sCookie = await login(sender.username);
  const xCookie = await login(stranger.username);

  const note = await createNote(sCookie);
  await sendTo(sCookie, note.id, [recipient.id]);

  // Stranger has no recipient row → 404.
  const res = await api(`/notes/received/${note.id}`, xCookie, {
    method: "DELETE",
  });
  assert.equal(res.status, 404, "stranger delete must be 404");

  // Sender (owner, but not a recipient) also has no recipient row → 404.
  const res2 = await api(`/notes/received/${note.id}`, sCookie, {
    method: "DELETE",
  });
  assert.equal(res2.status, 404, "sender-as-non-recipient must be 404");

  // Recipient row still intact.
  const { rows } = await pool.query(
    `SELECT id FROM note_recipients WHERE note_id = $1 AND recipient_user_id = $2`,
    [note.id, recipient.id],
  );
  assert.equal(rows.length, 1, "recipient row must remain after stranger 404");
});

test("bulk-delete removes only the caller's rows; mix of valid + missing is reported via notFound", async () => {
  const sender = await createUser("ndel_bs");
  const r1 = await createUser("ndel_br1");
  const other = await createUser("ndel_bo");
  const sCookie = await login(sender.username);
  const r1Cookie = await login(r1.username);

  const noteA = await createNote(sCookie);
  const noteB = await createNote(sCookie);
  const noteC = await createNote(sCookie); // r1 is NOT a recipient
  await sendTo(sCookie, noteA.id, [r1.id, other.id]);
  await sendTo(sCookie, noteB.id, [r1.id]);
  await sendTo(sCookie, noteC.id, [other.id]);

  const res = await api(`/notes/received/bulk-delete`, r1Cookie, {
    method: "POST",
    body: JSON.stringify({ ids: [noteA.id, noteB.id, noteC.id, 999999999] }),
  });
  assert.ok(res.ok, `bulk-delete expected 2xx, got ${res.status}`);
  const body = await res.json();
  assert.deepEqual(
    [...body.ok].sort((a, b) => a - b),
    [noteA.id, noteB.id].sort((a, b) => a - b),
    "must report A and B as successfully deleted",
  );
  assert.deepEqual(
    [...body.notFound].sort((a, b) => a - b),
    [noteC.id, 999999999].sort((a, b) => a - b),
    "non-recipient + missing ids must be reported",
  );

  // r1 inbox no longer has A or B.
  const r1Inbox = await (await api(`/notes/received`, r1Cookie)).json();
  assert.ok(
    !r1Inbox.find((n) => n.id === noteA.id),
    "noteA must be gone from r1 inbox",
  );
  assert.ok(
    !r1Inbox.find((n) => n.id === noteB.id),
    "noteB must be gone from r1 inbox",
  );

  // Other recipient on noteA still has their row.
  const { rows: otherRows } = await pool.query(
    `SELECT id FROM note_recipients WHERE note_id = $1 AND recipient_user_id = $2`,
    [noteA.id, other.id],
  );
  assert.equal(otherRows.length, 1, "other recipient on noteA must survive");

  // Underlying notes still exist (we only delete recipient rows).
  const { rows: notes } = await pool.query(
    `SELECT id FROM notes WHERE id = ANY($1::int[])`,
    [[noteA.id, noteB.id, noteC.id]],
  );
  assert.equal(notes.length, 3, "all underlying notes must remain");
});
