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 createdOrderIds = [];

async function createUser(prefix, roleName) {
  const username = `${prefix}_${Date.now().toString(36)}_${Math.random()
    .toString(36)
    .slice(2, 8)}`;
  const { rows } = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, 'SO Test', 'en', true) RETURNING id`,
    [username, `${username}@example.com`, TEST_PASSWORD_HASH],
  );
  const id = rows[0].id;
  createdUserIds.push(id);
  if (roleName) {
    await pool.query(
      `INSERT INTO user_roles (user_id, role_id) SELECT $1, id FROM roles WHERE name = $2`,
      [id, roleName],
    );
  }
  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 sc = res.headers.get("set-cookie");
  return sc.split(",").map((c) => c.split(";")[0].trim()).find((c) =>
    c.startsWith("connect.sid="),
  );
}

async function call(cookie, method, path, body) {
  return fetch(`${API_BASE}/api${path}`, {
    method,
    headers: { "Content-Type": "application/json", Cookie: cookie },
    body: body ? JSON.stringify(body) : undefined,
  });
}

let serviceId;

before(async () => {
  const { rows } = await pool.query(
    `SELECT id FROM services WHERE is_available = true ORDER BY id LIMIT 1`,
  );
  if (rows.length === 0) throw new Error("No available service to test with");
  serviceId = rows[0].id;
});

after(async () => {
  if (createdOrderIds.length > 0) {
    await pool.query(`DELETE FROM service_orders WHERE id = ANY($1::int[])`, [
      createdOrderIds,
    ]);
  }
  if (createdUserIds.length > 0) {
    await pool.query(`DELETE FROM service_orders WHERE user_id = ANY($1::int[]) OR assigned_to = ANY($1::int[])`, [createdUserIds]);
    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("client can place + list own orders; service summary excludes descriptions", async () => {
  const requester = await createUser("req", "user");
  const cookie = await login(requester.username);

  let res = await call(cookie, "POST", "/orders", {
    serviceId,
    notes: "extra hot",
  });
  assert.equal(res.status, 201);
  const order = await res.json();
  createdOrderIds.push(order.id);
  assert.equal(order.status, "pending");
  assert.equal(order.userId, requester.id);
  assert.ok(order.service);
  assert.ok(order.service.nameAr);
  assert.ok(order.service.nameEn);
  assert.equal(order.service.descriptionAr, undefined);
  assert.equal(order.service.descriptionEn, undefined);

  res = await call(cookie, "GET", "/orders/my");
  assert.equal(res.status, 200);
  const list = await res.json();
  const found = list.find((o) => o.id === order.id);
  assert.ok(found);
  assert.equal(found.service.descriptionAr, undefined);
});

test("non-receiver gets 403 from /incoming and /confirm-receipt", async () => {
  const owner = await createUser("own", "user");
  const cookieOwner = await login(owner.username);

  let res = await call(cookieOwner, "POST", "/orders", { serviceId });
  const order = await res.json();
  createdOrderIds.push(order.id);

  // Non-receiver listing
  res = await call(cookieOwner, "GET", "/orders/incoming");
  assert.equal(res.status, 403);

  // Non-receiver tries to claim
  res = await call(cookieOwner, "PATCH", `/orders/${order.id}/confirm-receipt`);
  assert.equal(res.status, 403);
});

test("user with no roles is 403 on /incoming (middleware has no auto-bypass)", async () => {
  const noRole = await createUser("noro", null);
  const cookie = await login(noRole.username);
  const res = await call(cookie, "GET", "/orders/incoming");
  assert.equal(res.status, 403);
});

test("two parallel confirm-receipt: one wins (200), other 409 already_claimed", async () => {
  const owner = await createUser("own2", "user");
  const r1 = await createUser("r1", "order_receiver");
  const r2 = await createUser("r2", "order_receiver");
  const co = await login(owner.username);
  const c1 = await login(r1.username);
  const c2 = await login(r2.username);

  let res = await call(co, "POST", "/orders", { serviceId });
  const order = await res.json();
  createdOrderIds.push(order.id);

  const [a, b] = await Promise.all([
    call(c1, "PATCH", `/orders/${order.id}/confirm-receipt`),
    call(c2, "PATCH", `/orders/${order.id}/confirm-receipt`),
  ]);
  const codes = [a.status, b.status].sort();
  assert.deepEqual(codes, [200, 409]);

  const losing = a.status === 409 ? a : b;
  const body = await losing.json();
  assert.equal(body.error, "already_claimed");
});

test("status transitions matrix: receiver flows + owner/admin cancel rules", async () => {
  const owner = await createUser("own3", "user");
  const recv = await createUser("recv3", "order_receiver");
  const stranger = await createUser("strg3", "user");
  const adminUser = await createUser("admn3", "admin");
  const co = await login(owner.username);
  const cr = await login(recv.username);
  const cs = await login(stranger.username);
  const ca = await login(adminUser.username);

  // Order 1: receiver flow pending → received → preparing → completed
  let res = await call(co, "POST", "/orders", { serviceId });
  let order = await res.json();
  createdOrderIds.push(order.id);

  // Owner CAN cancel while pending
  res = await call(co, "POST", "/orders", { serviceId });
  const cancelOrder = await res.json();
  createdOrderIds.push(cancelOrder.id);
  res = await call(co, "PATCH", `/orders/${cancelOrder.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).status, "cancelled");

  // Stranger cannot move status to preparing
  res = await call(cs, "PATCH", `/orders/${order.id}/status`, {
    status: "preparing",
  });
  assert.equal(res.status, 403);

  // Receiver claims first
  res = await call(cr, "PATCH", `/orders/${order.id}/confirm-receipt`);
  assert.equal(res.status, 200);

  // Receiver moves to preparing (allowed from received)
  res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
    status: "preparing",
  });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).status, "preparing");

  // Owner cannot cancel once preparing (only pending/received)
  res = await call(co, "PATCH", `/orders/${order.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 403);

  // Admin can cancel anytime
  // (use a NEW preparing order)
  res = await call(co, "POST", "/orders", { serviceId });
  const o2 = await res.json();
  createdOrderIds.push(o2.id);
  res = await call(cr, "PATCH", `/orders/${o2.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${o2.id}/status`, { status: "preparing" });
  assert.equal(res.status, 200);
  res = await call(ca, "PATCH", `/orders/${o2.id}/status`, { status: "cancelled" });
  assert.equal(res.status, 200);

  // Assigned receiver CAN cancel their own assigned order while preparing
  res = await call(co, "POST", "/orders", { serviceId });
  const o3 = await res.json();
  createdOrderIds.push(o3.id);
  res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" });
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "cancelled" });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).status, "cancelled");

  // A different receiver (not the assignee) cannot cancel
  res = await call(co, "POST", "/orders", { serviceId });
  const o4 = await res.json();
  createdOrderIds.push(o4.id);
  res = await call(cr, "PATCH", `/orders/${o4.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  const otherRecv = await createUser("recv4", "order_receiver");
  const cor = await login(otherRecv.username);
  res = await call(cor, "PATCH", `/orders/${o4.id}/status`, { status: "cancelled" });
  assert.equal(res.status, 403);

  // Receiver completes original order
  res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
    status: "completed",
  });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).status, "completed");

  // Cannot transition completed → anything (non-admin); now 409 order_unavailable per Task #80
  res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
    status: "preparing",
  });
  assert.equal(res.status, 409);

  // Admin CAN cancel a completed order
  res = await call(ca, "PATCH", `/orders/${order.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).status, "cancelled");
});

test("requester gets a distinct notification when the receiver cancels a claimed order", async () => {
  const owner = await createUser("ownN", "user");
  const recv = await createUser("recvN", "order_receiver");
  const co = await login(owner.username);
  const cr = await login(recv.username);

  let res = await call(co, "POST", "/orders", { serviceId });
  assert.equal(res.status, 201);
  const order = await res.json();
  createdOrderIds.push(order.id);

  // Receiver claims, then cancels
  res = await call(cr, "PATCH", `/orders/${order.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${order.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 200);

  // Owner should have a cancellation notification with the receiver-cancel body
  const { rows } = await pool.query(
    `SELECT title_ar, title_en, body_ar, body_en
       FROM notifications
      WHERE user_id = $1 AND related_type = 'order' AND related_id = $2
      ORDER BY id DESC`,
    [owner.id, order.id],
  );
  const cancelNotif = rows.find(
    (n) => n.title_en === "Your order was cancelled",
  );
  assert.ok(cancelNotif, "expected a cancellation notification for the owner");
  assert.match(
    cancelNotif.body_en,
    /claimed but later cancelled by the receiver/,
  );
  assert.match(cancelNotif.body_ar, /استلم طلبك ولكن تم إلغاؤه لاحقاً/);
});

test("DELETE /orders/:id — owner can delete completed/cancelled, admin can delete any, others 403", async () => {
  const owner = await createUser("delOwn", "user");
  const recv = await createUser("delRecv", "order_receiver");
  const stranger = await createUser("delStr", "user");
  const adminUser = await createUser("delAdm", "admin");
  const co = await login(owner.username);
  const cr = await login(recv.username);
  const cs = await login(stranger.username);
  const ca = await login(adminUser.username);

  // Helper to create an order owned by `co`
  const placeOrder = async () => {
    const r = await call(co, "POST", "/orders", { serviceId });
    assert.equal(r.status, 201);
    const o = await r.json();
    createdOrderIds.push(o.id);
    return o;
  };

  // 1) Pending order: owner cannot delete (not terminal)
  const o1 = await placeOrder();
  let res = await call(co, "DELETE", `/orders/${o1.id}`);
  assert.equal(res.status, 403);

  // 2) Stranger cannot delete a terminal order they don't own
  const o2 = await placeOrder();
  res = await call(co, "PATCH", `/orders/${o2.id}/status`, { status: "cancelled" });
  assert.equal(res.status, 200);
  res = await call(cs, "DELETE", `/orders/${o2.id}`);
  assert.equal(res.status, 403);

  // 3) Owner CAN delete a cancelled order they own
  res = await call(co, "DELETE", `/orders/${o2.id}`);
  assert.equal(res.status, 204);
  // Confirm it's gone from /orders/my
  res = await call(co, "GET", "/orders/my");
  const list1 = await res.json();
  assert.ok(!list1.find((o) => o.id === o2.id));
  // 3b) Deleting the same order again returns 404 (idempotent semantics)
  res = await call(co, "DELETE", `/orders/${o2.id}`);
  assert.equal(res.status, 404);

  // 4) Owner CAN delete a completed order
  const o3 = await placeOrder();
  res = await call(cr, "PATCH", `/orders/${o3.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "preparing" });
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${o3.id}/status`, { status: "completed" });
  assert.equal(res.status, 200);
  res = await call(co, "DELETE", `/orders/${o3.id}`);
  assert.equal(res.status, 204);

  // 5) Admin CAN delete a non-terminal (pending) order
  const o4 = await placeOrder();
  res = await call(ca, "DELETE", `/orders/${o4.id}`);
  assert.equal(res.status, 204);

  // 6) 404 for unknown id
  res = await call(co, "DELETE", `/orders/9999999`);
  assert.equal(res.status, 404);

  // 7) Unauthenticated → 401
  res = await fetch(`${API_BASE}/api/orders/1`, { method: "DELETE" });
  assert.equal(res.status, 401);
});

test("owner can restore (undo) a freshly cancelled order to its prior status; expired window blocks restore", async () => {
  const owner = await createUser("undoOwn", "user");
  const recv = await createUser("undoRecv", "order_receiver");
  const stranger = await createUser("undoStr", "user");
  const co = await login(owner.username);
  const cr = await login(recv.username);
  const cs = await login(stranger.username);

  // Case A: cancel from pending → restore to pending
  let res = await call(co, "POST", "/orders", { serviceId });
  assert.equal(res.status, 201);
  const oA = await res.json();
  createdOrderIds.push(oA.id);

  res = await call(co, "PATCH", `/orders/${oA.id}/status`, { status: "cancelled" });
  assert.equal(res.status, 200);

  // Stranger cannot restore
  res = await call(cs, "PATCH", `/orders/${oA.id}/status`, { status: "pending" });
  assert.equal(res.status, 403);

  // Owner restores within window
  res = await call(co, "PATCH", `/orders/${oA.id}/status`, { status: "pending" });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).status, "pending");

  // Case B: cancel from received → restore to received
  res = await call(co, "POST", "/orders", { serviceId });
  const oB = await res.json();
  createdOrderIds.push(oB.id);
  res = await call(cr, "PATCH", `/orders/${oB.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "cancelled" });
  assert.equal(res.status, 200);
  // Restoring to "pending" should fail because assignedTo is set
  res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "pending" });
  assert.equal(res.status, 400);
  // Correct restore is "received"
  res = await call(co, "PATCH", `/orders/${oB.id}/status`, { status: "received" });
  assert.equal(res.status, 200);
  assert.equal((await res.json()).status, "received");

  // Case C: undo window expired → restore is rejected
  res = await call(co, "POST", "/orders", { serviceId });
  const oC = await res.json();
  createdOrderIds.push(oC.id);
  res = await call(co, "PATCH", `/orders/${oC.id}/status`, { status: "cancelled" });
  assert.equal(res.status, 200);
  // Backdate updatedAt past the 15s server window.
  await pool.query(
    `UPDATE service_orders SET updated_at = NOW() - INTERVAL '60 seconds' WHERE id = $1`,
    [oC.id],
  );
  res = await call(co, "PATCH", `/orders/${oC.id}/status`, { status: "pending" });
  assert.equal(res.status, 400);
  const body = await res.json();
  assert.match(body.error, /undo_window_expired/);
});

test("confirm-receipt returns 409 order_unavailable when the order is cancelled or completed", async () => {
  const owner = await createUser("ownU", "user");
  const recv1 = await createUser("recvU1", "order_receiver");
  const recv2 = await createUser("recvU2", "order_receiver");
  const co = await login(owner.username);
  const cr1 = await login(recv1.username);
  const cr2 = await login(recv2.username);

  // Case A: order cancelled before any claim → second receiver sees order_unavailable
  let res = await call(co, "POST", "/orders", { serviceId });
  assert.equal(res.status, 201);
  const oCancelled = await res.json();
  createdOrderIds.push(oCancelled.id);

  res = await call(co, "PATCH", `/orders/${oCancelled.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 200);

  res = await call(cr1, "PATCH", `/orders/${oCancelled.id}/confirm-receipt`);
  assert.equal(res.status, 409);
  let body = await res.json();
  assert.equal(
    body.error,
    "order_unavailable",
    "cancelled order should report order_unavailable, not already_claimed",
  );

  // Case B: order taken all the way to completed → another receiver sees order_unavailable
  res = await call(co, "POST", "/orders", { serviceId });
  assert.equal(res.status, 201);
  const oCompleted = await res.json();
  createdOrderIds.push(oCompleted.id);

  res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/status`, {
    status: "preparing",
  });
  assert.equal(res.status, 200);
  res = await call(cr1, "PATCH", `/orders/${oCompleted.id}/status`, {
    status: "completed",
  });
  assert.equal(res.status, 200);

  res = await call(cr2, "PATCH", `/orders/${oCompleted.id}/confirm-receipt`);
  assert.equal(res.status, 409);
  body = await res.json();
  assert.equal(
    body.error,
    "order_unavailable",
    "completed order should report order_unavailable, not already_claimed",
  );
});

test("PATCH /orders/:id/status returns 409 order_unavailable when targeting a terminal order", async () => {
  const owner = await createUser("ownT", "user");
  const recv = await createUser("recvT", "order_receiver");
  const co = await login(owner.username);
  const cr = await login(recv.username);

  // Build a completed order (assignee = recv)
  let res = await call(co, "POST", "/orders", { serviceId });
  assert.equal(res.status, 201);
  const oDone = await res.json();
  createdOrderIds.push(oDone.id);

  res = await call(cr, "PATCH", `/orders/${oDone.id}/confirm-receipt`);
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
    status: "preparing",
  });
  assert.equal(res.status, 200);
  res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
    status: "completed",
  });
  assert.equal(res.status, 200);

  // Assignee tries preparing on a completed order → 409 order_unavailable
  res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
    status: "preparing",
  });
  assert.equal(res.status, 409);
  let body = await res.json();
  assert.equal(body.error, "order_unavailable");

  // Assignee tries completed (again) on a completed order → 409 order_unavailable
  res = await call(cr, "PATCH", `/orders/${oDone.id}/status`, {
    status: "completed",
  });
  assert.equal(res.status, 409);
  body = await res.json();
  assert.equal(body.error, "order_unavailable");

  // Owner tries to cancel a completed order → 409 order_unavailable (non-admin)
  res = await call(co, "PATCH", `/orders/${oDone.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 409);
  body = await res.json();
  assert.equal(body.error, "order_unavailable");

  // Build a cancelled order (no assignee)
  res = await call(co, "POST", "/orders", { serviceId });
  assert.equal(res.status, 201);
  const oCxl = await res.json();
  createdOrderIds.push(oCxl.id);

  res = await call(co, "PATCH", `/orders/${oCxl.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 200);

  // Owner tries to cancel an already-cancelled order → 409 order_unavailable (non-admin)
  res = await call(co, "PATCH", `/orders/${oCxl.id}/status`, {
    status: "cancelled",
  });
  assert.equal(res.status, 409);
  body = await res.json();
  assert.equal(body.error, "order_unavailable");
});

test("unauthenticated cannot place order", async () => {
  const res = await fetch(`${API_BASE}/api/orders`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ serviceId }),
  });
  assert.equal(res.status, 401);
});
