// UI test for the receiver-side incoming-orders flow that lives across
// artifacts/tx-os/src/pages/orders-incoming.tsx and the requester's
// artifacts/tx-os/src/pages/my-orders.tsx.
//
// Mirrors part of the API matrix in
// artifacts/api-server/tests/service-orders.test.mjs but at the UI layer:
//   - Seeds a requester (role "user") and a receiver (role "order_receiver").
//   - Logs each into their own browser context.
//   - Has the requester place an order via /services.
//   - Switches to the receiver's /orders-incoming page, claims the order
//     (Confirm Receipt), then walks it through Mark Preparing → Mark
//     Completed.
//   - After each receiver action, asserts that the requester's /my-orders
//     card reflects the new status pill in real time. The push happens via
//     the socket.io "order_updated" event the API server emits to the
//     requester (see artifacts/api-server/src/routes/service-orders.ts +
//     artifacts/tx-os/src/hooks/use-notifications-socket.ts), which
//     invalidates the my-orders query and triggers a refetch.
//   - Separately exercises the receiver-side cancel path: the receiver
//     claims a second order, then cancels it from /orders-incoming, and
//     the requester sees the red "Cancelled" pill.
//
// English UI only — the order-place-cancel.spec.mjs neighbour already
// exercises Arabic translations on the requester side, and the flow under
// test here is identical between languages save for label strings.
//
// Run with:
//   DATABASE_URL=... pnpm --filter @workspace/tx-os test:e2e
//
// Browser binaries can be installed once with:
//   pnpm --filter @workspace/tx-os exec playwright install chromium

import { test, expect } from "@playwright/test";
import pg from "pg";

const DATABASE_URL = process.env.DATABASE_URL;
if (!DATABASE_URL) {
  throw new Error(
    "DATABASE_URL must be set to run the receiver-side incoming-orders UI test",
  );
}

// Same convention as the other UI/API tests: a precomputed bcrypt hash of
// the literal password "TestPass123!".
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 = [];

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

async function createUser(prefix, roleName, displayNameEn) {
  const username = `${prefix}_${uniqueSuffix()}`;
  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,
      displayNameEn ?? username,
    ],
  );
  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 = $2`,
    [id, roleName],
  );
  return { id, username, displayName: displayNameEn ?? username };
}

async function getAvailableService() {
  const { rows } = await pool.query(
    `SELECT id,
            name_en AS "nameEn",
            name_ar AS "nameAr"
       FROM services
      WHERE is_available = true
      ORDER BY id ASC
      LIMIT 1`,
  );
  if (rows.length === 0) {
    throw new Error(
      "No available service found in DB to test ordering against",
    );
  }
  return rows[0];
}

test.afterAll(async () => {
  if (createdOrderIds.length > 0) {
    await pool.query(`DELETE FROM service_orders WHERE id = ANY($1::int[])`, [
      createdOrderIds,
    ]);
  }
  if (createdUserIds.length > 0) {
    // Defensive cleanup: catch any orders created against these users that
    // we didn't explicitly track.
    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();
});

async function setLanguage(page, lang) {
  // i18n.ts reads `localStorage.getItem("tx-lang")` at module-init time, so
  // seed it before any page script runs.
  await page.addInitScript((l) => {
    try {
      window.localStorage.setItem("tx-lang", l);
    } catch {
      /* localStorage may not be available before navigation; safe to ignore */
    }
  }, lang);
}

async function loginViaUi(page, username, password) {
  await page.goto("/login");
  await page.locator("#username").fill(username);
  await page.locator("#password").fill(password);
  await Promise.all([
    page.waitForURL((url) => !url.pathname.endsWith("/login"), {
      timeout: 15_000,
    }),
    page.locator('form button[type="submit"]').click(),
  ]);
}

async function placeOrderViaUi(page, serviceNameEn) {
  await page.goto("/services");
  await page
    .getByRole("button", { name: serviceNameEn, exact: true })
    .first()
    .click();
  const orderModal = page.getByRole("dialog");
  await expect(orderModal).toBeVisible();

  const placePromise = page.waitForResponse(
    (resp) => {
      const path = new URL(resp.url()).pathname;
      return path === "/api/orders" && resp.request().method() === "POST";
    },
    { timeout: 15_000 },
  );
  await orderModal
    .getByRole("button", { name: "Send", exact: true })
    .click();
  const placeResp = await placePromise;
  expect(placeResp.status()).toBe(201);
  const placedOrder = await placeResp.json();
  createdOrderIds.push(placedOrder.id);
  expect(placedOrder.status).toBe("pending");
  await expect(orderModal).toBeHidden();
  return placedOrder;
}

// On /my-orders, the StatusPill renders the localized status text in a
// compact rounded-full span (see artifacts/tx-os/src/pages/my-orders.tsx).
// Asserting on the pill text avoids ambiguity with other text on the card
// (notably "Awaiting receiver" containing the substring "Received").
async function expectMyOrdersStatus(card, expectedText) {
  await expect(
    card.getByText(expectedText, { exact: true }).first(),
  ).toBeVisible({ timeout: 15_000 });
}

// On /orders-incoming, locate the card belonging to the order placed by
// `requesterDisplayName` for `serviceNameEn`. The card includes both
// (e.g. "<service name>" and "From <requester display name>"), so combining
// them uniquely identifies the order even if the page already has other
// cards from prior runs / unrelated users.
function incomingCardFor(page, serviceNameEn, requesterDisplayName) {
  return page
    .locator(".glass-panel.rounded-xl")
    .filter({ hasText: serviceNameEn })
    .filter({ hasText: requesterDisplayName });
}

async function waitForStatusPatch(page, orderId) {
  return page.waitForResponse(
    (resp) => {
      const path = new URL(resp.url()).pathname;
      return (
        path === `/api/orders/${orderId}/status` &&
        resp.request().method() === "PATCH"
      );
    },
    { timeout: 15_000 },
  );
}

async function waitForConfirmReceipt(page, orderId) {
  return page.waitForResponse(
    (resp) => {
      const path = new URL(resp.url()).pathname;
      return (
        path === `/api/orders/${orderId}/confirm-receipt` &&
        resp.request().method() === "PATCH"
      );
    },
    { timeout: 15_000 },
  );
}

test.describe("Receiver-side incoming-orders flow", () => {
  test("receiver claims, prepares, completes — requester sees live status updates", async ({
    browser,
  }) => {
    const requester = await createUser(
      "ord_req",
      "user",
      `Req ${uniqueSuffix()}`,
    );
    const receiver = await createUser(
      "ord_recv",
      "order_receiver",
      `Recv ${uniqueSuffix()}`,
    );
    const service = await getAvailableService();

    const requesterContext = await browser.newContext();
    const receiverContext = await browser.newContext();
    try {
      const requesterPage = await requesterContext.newPage();
      const receiverPage = await receiverContext.newPage();

      await setLanguage(requesterPage, "en");
      await setLanguage(receiverPage, "en");

      await loginViaUi(requesterPage, requester.username, TEST_PASSWORD);
      await loginViaUi(receiverPage, receiver.username, TEST_PASSWORD);

      // ---- Requester places an order via /services ----
      const placedOrder = await placeOrderViaUi(requesterPage, service.nameEn);

      // ---- Requester opens /my-orders so the live updates have a target ----
      await requesterPage.goto("/my-orders");
      const requesterCard = requesterPage
        .locator(".glass-panel.rounded-xl")
        .first();
      await expect(requesterCard).toContainText(service.nameEn);
      await expectMyOrdersStatus(requesterCard, "Awaiting receiver");

      // ---- Receiver opens /orders/incoming and claims it ----
      await receiverPage.goto("/orders/incoming");
      const incomingCard = incomingCardFor(
        receiverPage,
        service.nameEn,
        requester.displayName,
      );
      await expect(incomingCard.first()).toBeVisible();

      const claimPromise = waitForConfirmReceipt(receiverPage, placedOrder.id);
      await incomingCard
        .first()
        .getByRole("button", { name: "Confirm Receipt", exact: true })
        .click();
      const claimResp = await claimPromise;
      expect(claimResp.status()).toBe(200);

      // Requester should see "Received" pill via the order_updated socket
      // push that invalidates the my-orders query.
      await expectMyOrdersStatus(requesterCard, "Received");

      // ---- Receiver moves it to Preparing ----
      // After the refetch, the card has moved to the "My active orders"
      // section but still uniquely identifiable by service + requester.
      const mineCardReceived = incomingCardFor(
        receiverPage,
        service.nameEn,
        requester.displayName,
      );
      await expect(
        mineCardReceived
          .first()
          .getByRole("button", { name: "Mark Preparing", exact: true }),
      ).toBeVisible();

      const preparingPromise = waitForStatusPatch(receiverPage, placedOrder.id);
      await mineCardReceived
        .first()
        .getByRole("button", { name: "Mark Preparing", exact: true })
        .click();
      const preparingResp = await preparingPromise;
      expect(preparingResp.status()).toBe(200);

      await expectMyOrdersStatus(requesterCard, "Preparing");

      // ---- Receiver moves it to Completed ----
      const mineCardPreparing = incomingCardFor(
        receiverPage,
        service.nameEn,
        requester.displayName,
      );
      await expect(
        mineCardPreparing
          .first()
          .getByRole("button", { name: "Mark Completed", exact: true }),
      ).toBeVisible();

      const completedPromise = waitForStatusPatch(receiverPage, placedOrder.id);
      await mineCardPreparing
        .first()
        .getByRole("button", { name: "Mark Completed", exact: true })
        .click();
      const completedResp = await completedPromise;
      expect(completedResp.status()).toBe(200);

      await expectMyOrdersStatus(requesterCard, "Completed");
    } finally {
      await requesterContext.close();
      await receiverContext.close();
    }
  });

  test("receiver cancels after claiming — requester sees Cancelled pill", async ({
    browser,
  }) => {
    const requester = await createUser(
      "ord_req_cx",
      "user",
      `ReqCx ${uniqueSuffix()}`,
    );
    const receiver = await createUser(
      "ord_recv_cx",
      "order_receiver",
      `RecvCx ${uniqueSuffix()}`,
    );
    const service = await getAvailableService();

    const requesterContext = await browser.newContext();
    const receiverContext = await browser.newContext();
    try {
      const requesterPage = await requesterContext.newPage();
      const receiverPage = await receiverContext.newPage();

      await setLanguage(requesterPage, "en");
      await setLanguage(receiverPage, "en");

      await loginViaUi(requesterPage, requester.username, TEST_PASSWORD);
      await loginViaUi(receiverPage, receiver.username, TEST_PASSWORD);

      const placedOrder = await placeOrderViaUi(requesterPage, service.nameEn);

      await requesterPage.goto("/my-orders");
      const requesterCard = requesterPage
        .locator(".glass-panel.rounded-xl")
        .first();
      await expect(requesterCard).toContainText(service.nameEn);
      await expectMyOrdersStatus(requesterCard, "Awaiting receiver");

      // Receiver claims first so the cancel path under test exercises the
      // "received → cancelled" transition (the only cancel path the
      // receiver UI offers, see orders-incoming.tsx handleCancel guard).
      await receiverPage.goto("/orders/incoming");
      const incomingCard = incomingCardFor(
        receiverPage,
        service.nameEn,
        requester.displayName,
      );
      const claimPromise = waitForConfirmReceipt(receiverPage, placedOrder.id);
      await incomingCard
        .first()
        .getByRole("button", { name: "Confirm Receipt", exact: true })
        .click();
      const claimResp = await claimPromise;
      expect(claimResp.status()).toBe(200);
      await expectMyOrdersStatus(requesterCard, "Received");

      // ---- Receiver cancels the claimed order ----
      const mineCard = incomingCardFor(
        receiverPage,
        service.nameEn,
        requester.displayName,
      );
      const cancelBtn = mineCard
        .first()
        .getByRole("button", { name: "Cancel", exact: true });
      await expect(cancelBtn).toBeVisible();

      const cancelPromise = waitForStatusPatch(receiverPage, placedOrder.id);
      await cancelBtn.click();
      const cancelResp = await cancelPromise;
      expect(cancelResp.status()).toBe(200);
      const cancelBody = await cancelResp.json();
      expect(cancelBody.status).toBe("cancelled");

      // Requester sees the cancelled pill via the live socket update; the
      // timeline circles are replaced with a centered "Cancelled" badge.
      await expectMyOrdersStatus(requesterCard, "Cancelled");
      await expect(requesterCard.locator("div.h-7.w-7")).toHaveCount(0);
    } finally {
      await requesterContext.close();
      await receiverContext.close();
    }
  });
});
