// API tests for the Web Push subsystem:
//   - GET  /api/push/vapid-public-key
//   - POST /api/push/subscribe   (auth + upsert)
//   - POST /api/push/unsubscribe (auth + removal)
//   - account-switch row reassignment for the same browser endpoint
//
// The full delivery path (web-push -> 410 cleanup) is exercised via a
// direct DB-level simulation: we insert a subscription with a known
// endpoint, then assert it can be looked up + removed through the
// public API. The 410 cleanup itself runs in the server when push
// providers reject — covered by reading the helper from
// `dist/index.mjs` would require a separate integration runner, so
// here we cover the DB/HTTP contract.
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 createdEndpoints = [];

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;
}

function fakeSub(suffix) {
  const endpoint = `https://fcm.example.test/push/${suffix}_${uniq()}`;
  createdEndpoints.push(endpoint);
  return {
    endpoint,
    keys: {
      p256dh: "BPubKey-" + suffix,
      auth: "auth-" + suffix,
    },
  };
}

before(async () => {
  // Sanity: server up?
  const r = await fetch(`${API_BASE}/api/healthz`).catch(() => null);
  if (!r || !r.ok) {
    throw new Error(`API at ${API_BASE} is not reachable`);
  }
});

after(async () => {
  if (createdEndpoints.length > 0) {
    await pool.query(
      `DELETE FROM push_subscriptions WHERE endpoint = ANY($1::text[])`,
      [createdEndpoints],
    );
  }
  if (createdUserIds.length > 0) {
    await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [
      createdUserIds,
    ]);
  }
  await pool.end();
});

test("GET /api/push/vapid-public-key returns a base64url key", async () => {
  const res = await fetch(`${API_BASE}/api/push/vapid-public-key`);
  assert.equal(res.status, 200);
  const body = await res.json();
  assert.ok(
    typeof body.publicKey === "string" && body.publicKey.length > 40,
    "expected non-empty VAPID public key",
  );
});

test("POST /api/push/subscribe requires auth", async () => {
  const res = await fetch(`${API_BASE}/api/push/subscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(fakeSub("noauth")),
  });
  assert.equal(res.status, 401);
});

test("POST /api/push/unsubscribe requires auth", async () => {
  const res = await fetch(`${API_BASE}/api/push/unsubscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ endpoint: "https://example.test/x" }),
  });
  assert.equal(res.status, 401);
});

test("subscribe persists a row and unsubscribe removes it", async () => {
  const user = await createUser("push_basic");
  const cookie = await login(user.username);
  const sub = fakeSub("basic");

  const subRes = await fetch(`${API_BASE}/api/push/subscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: cookie },
    body: JSON.stringify(sub),
  });
  assert.equal(subRes.status, 200);

  const rowsAfter = await pool.query(
    `SELECT user_id, p256dh, auth FROM push_subscriptions WHERE endpoint = $1`,
    [sub.endpoint],
  );
  assert.equal(rowsAfter.rowCount, 1, "expected one row after subscribe");
  assert.equal(rowsAfter.rows[0].user_id, user.id);
  assert.equal(rowsAfter.rows[0].p256dh, sub.keys.p256dh);
  assert.equal(rowsAfter.rows[0].auth, sub.keys.auth);

  const unsubRes = await fetch(`${API_BASE}/api/push/unsubscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: cookie },
    body: JSON.stringify({ endpoint: sub.endpoint }),
  });
  assert.equal(unsubRes.status, 200);

  const rowsGone = await pool.query(
    `SELECT 1 FROM push_subscriptions WHERE endpoint = $1`,
    [sub.endpoint],
  );
  assert.equal(rowsGone.rowCount, 0, "expected row removed after unsubscribe");
});

test("subscribe is idempotent — re-subscribing updates keys without duplicating", async () => {
  const user = await createUser("push_idem");
  const cookie = await login(user.username);
  const sub = fakeSub("idem");

  for (const variant of [sub, { ...sub, keys: { p256dh: "rotated", auth: "rotated" } }]) {
    const res = await fetch(`${API_BASE}/api/push/subscribe`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Cookie: cookie },
      body: JSON.stringify(variant),
    });
    assert.equal(res.status, 200);
  }

  const rows = await pool.query(
    `SELECT p256dh, auth FROM push_subscriptions WHERE endpoint = $1`,
    [sub.endpoint],
  );
  assert.equal(rows.rowCount, 1);
  assert.equal(rows.rows[0].p256dh, "rotated");
  assert.equal(rows.rows[0].auth, "rotated");
});

test("subscribe reassigns endpoint to the new user when a different user signs in on the same browser", async () => {
  const userA = await createUser("push_a");
  const userB = await createUser("push_b");
  const cookieA = await login(userA.username);
  const cookieB = await login(userB.username);
  const sub = fakeSub("switch");

  let res = await fetch(`${API_BASE}/api/push/subscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: cookieA },
    body: JSON.stringify(sub),
  });
  assert.equal(res.status, 200);

  res = await fetch(`${API_BASE}/api/push/subscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: cookieB },
    body: JSON.stringify(sub),
  });
  assert.equal(res.status, 200);

  const rows = await pool.query(
    `SELECT user_id FROM push_subscriptions WHERE endpoint = $1`,
    [sub.endpoint],
  );
  assert.equal(rows.rowCount, 1, "endpoint should never duplicate across users");
  assert.equal(
    rows.rows[0].user_id,
    userB.id,
    "endpoint should now belong to the most recent signer-in",
  );
});

test("subscribe rejects malformed input", async () => {
  const user = await createUser("push_bad");
  const cookie = await login(user.username);
  const res = await fetch(`${API_BASE}/api/push/subscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: cookie },
    body: JSON.stringify({ endpoint: "not-a-url", keys: {} }),
  });
  assert.equal(res.status, 400);
});

test("DELETE /api/push/subscribe?endpoint=... removes the row (spec alias)", async () => {
  const user = await createUser("push_del");
  const cookie = await login(user.username);
  const sub = fakeSub("del");

  const subRes = await fetch(`${API_BASE}/api/push/subscribe`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Cookie: cookie },
    body: JSON.stringify(sub),
  });
  assert.equal(subRes.status, 200);

  const before = await pool.query(
    `SELECT 1 FROM push_subscriptions WHERE endpoint = $1`,
    [sub.endpoint],
  );
  assert.equal(before.rowCount, 1);

  const delRes = await fetch(
    `${API_BASE}/api/push/subscribe?endpoint=${encodeURIComponent(sub.endpoint)}`,
    { method: "DELETE", headers: { Cookie: cookie } },
  );
  assert.equal(delRes.status, 200);

  const after = await pool.query(
    `SELECT 1 FROM push_subscriptions WHERE endpoint = $1`,
    [sub.endpoint],
  );
  assert.equal(after.rowCount, 0, "DELETE alias must remove the subscription row");
});

test("DELETE /api/push/subscribe requires auth and a valid endpoint", async () => {
  const noAuth = await fetch(
    `${API_BASE}/api/push/subscribe?endpoint=https://x.example/abc`,
    { method: "DELETE" },
  );
  assert.equal(noAuth.status, 401);

  const user = await createUser("push_del_bad");
  const cookie = await login(user.username);
  const bad = await fetch(
    `${API_BASE}/api/push/subscribe?endpoint=not-a-url`,
    { method: "DELETE", headers: { Cookie: cookie } },
  );
  assert.equal(bad.status, 400);
});
