// UI test for the end-to-end order placement and cancellation flow that lives
// across artifacts/tx-os/src/pages/services.tsx, the order modal in
// artifacts/tx-os/src/components/order-service-modal.tsx, and
// artifacts/tx-os/src/pages/my-orders.tsx.
//
// Mirrors the API-side coverage in
// artifacts/api-server/tests/service-orders.test.mjs but at the UI layer:
//   - Logs in as a regular user (seeded via the database).
//   - Navigates to /services and clicks an available service tile.
//   - Submits the order modal and waits for the POST /api/orders 201 response.
//   - Confirms the new order shows up at the top of /my-orders with the
//     "Awaiting receiver" pill (pending status).
//   - Cancels it via the in-card Cancel button + confirmation dialog and waits
//     for the PATCH /api/orders/:id/status 200 response.
//   - Asserts the red cancelled pill replaces the timeline (no timeline step
//     circles remain) and the Cancel button is gone from the card.
//
// Both Arabic and English label paths are exercised via separate tests; the
// language is forced through localStorage before the page loads.
//
// 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
//
// The test seeds its own user + uses an existing available service, then
// cleans up the user, their orders, and any notifications it produced.

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 order placement / cancellation UI test",
  );
}

// Same convention as artifacts/api-server/tests/service-orders.test.mjs:
// 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, preferredLanguage = "en") {
  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, 'Order UI Test', $4, true) RETURNING id`,
    [
      username,
      `${username}@example.com`,
      TEST_PASSWORD_HASH,
      preferredLanguage,
    ],
  );
  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 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) {
    // Catch any orders the user created that we didn't track (defensive).
    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 placeAndCancelFlow(page, lang, labels) {
  // Seed the user with a server-side preferredLanguage matching the test
  // language, because AuthContext / login.tsx call `i18n.changeLanguage(
  // user.preferredLanguage)` after login and would otherwise override our
  // localStorage tx-lang seed.
  const user = await createUser(`order_ui_${lang}`, lang);
  const service = await getAvailableService();
  const serviceName = lang === "ar" ? service.nameAr : service.nameEn;

  await setLanguage(page, lang);
  await loginViaUi(page, user.username, TEST_PASSWORD);

  // ---- Place an order from /services ----
  await page.goto("/services");

  // Service tiles render as <button aria-label={localized name}>; pick the
  // first one matching our chosen service.
  await page
    .getByRole("button", { name: serviceName, exact: true })
    .first()
    .click();

  const orderModal = page.getByRole("dialog");
  await expect(orderModal).toBeVisible();
  // Modal title is "Order: {name}" / "اطلب: {name}".
  await expect(orderModal).toContainText(labels.modalTitle);

  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: labels.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");

  // Modal closes after a successful placement.
  await expect(orderModal).toBeHidden();

  // ---- Verify it appears at the top of /my-orders with the pending pill ----
  await page.goto("/my-orders");

  // OrderCard root element: the order cards are the only `.glass-panel.rounded-xl`
  // containers on this page (the page header uses `border-b`, not `rounded-xl`).
  const orderCards = page.locator(".glass-panel.rounded-xl");
  await expect(orderCards.first()).toBeVisible();
  const firstCard = orderCards.first();
  await expect(firstCard).toContainText(serviceName);
  // Status pill (top-right) shows the "pending" label.
  await expect(firstCard).toContainText(labels.pending);

  // Before cancellation there must be 4 timeline step circles
  // (pending → received → preparing → completed).
  const timelineSteps = firstCard.locator("div.h-7.w-7");
  await expect(timelineSteps).toHaveCount(4);

  // Cancel button is present and clickable.
  const cancelBtn = firstCard.getByRole("button", {
    name: labels.cancelOrder,
    exact: true,
  });
  await expect(cancelBtn).toBeVisible();

  // ---- Cancel the order via the confirmation dialog ----
  await cancelBtn.click();
  const confirmDialog = page.getByRole("alertdialog");
  await expect(confirmDialog).toBeVisible();
  await expect(confirmDialog).toContainText(labels.cancelConfirmTitle);

  const cancelPromise = page.waitForResponse(
    (resp) => {
      const path = new URL(resp.url()).pathname;
      return (
        path === `/api/orders/${placedOrder.id}/status` &&
        resp.request().method() === "PATCH"
      );
    },
    { timeout: 15_000 },
  );
  await confirmDialog
    .getByRole("button", { name: labels.confirmCancel, exact: true })
    .click();
  const cancelResp = await cancelPromise;
  expect(cancelResp.status()).toBe(200);
  const cancelledBody = await cancelResp.json();
  expect(cancelledBody.status).toBe("cancelled");

  // ---- Verify the cancelled pill replaces the timeline + cancel button gone ----
  // After react-query invalidates and refetches, the card re-renders with the
  // centered cancelled pill instead of the 4 step circles.
  const refreshedCard = page.locator(".glass-panel.rounded-xl").first();
  await expect(
    refreshedCard.locator("div.h-7.w-7"),
  ).toHaveCount(0);
  // The red cancelled badge inside the timeline area is now visible.
  await expect(refreshedCard).toContainText(labels.cancelledPill);
  // The Cancel button is gone from the card (cancel is no longer allowed).
  await expect(
    refreshedCard.getByRole("button", {
      name: labels.cancelOrder,
      exact: true,
    }),
  ).toHaveCount(0);
}

test.describe("Order placement + cancellation flow", () => {
  test("English UI: place order from /services, cancel it from /my-orders", async ({
    page,
  }) => {
    await placeAndCancelFlow(page, "en", {
      modalTitle: "Order:",
      send: "Send",
      pending: "Awaiting receiver",
      cancelOrder: "Cancel order",
      cancelConfirmTitle: "Cancel this order?",
      confirmCancel: "Yes, cancel",
      cancelledPill: "Cancelled",
    });
  });

  test("Arabic UI: place order from /services, cancel it from /my-orders", async ({
    page,
  }) => {
    await placeAndCancelFlow(page, "ar", {
      modalTitle: "اطلب:",
      send: "إرسال",
      pending: "بانتظار المستلِم",
      cancelOrder: "إلغاء الطلب",
      cancelConfirmTitle: "إلغاء هذا الطلب؟",
      confirmCancel: "نعم، إلغاء",
      cancelledPill: "ملغى",
    });
  });
});
