// UI tests for the 7-second "Undo" toast that appears after cancelling or
// deleting an order on /my-orders.
//
// Covers two regression-prone paths:
//
//   1. Cancel undo:
//      - Place a pending order via the UI (so /my-orders has a fresh card).
//      - Cancel it through the in-card Cancel + confirm dialog.
//      - Click "Undo" inside the toast within UNDO_WINDOW_MS (7s).
//      - Wait for the restore PATCH /api/orders/:id/status -> "pending".
//      - Verify the card is back to the pending state (timeline reappears,
//        status pill is "Awaiting receiver") and the DB row is also pending.
//
//   2. Delete undo:
//      - Seed a `completed` order directly in the DB so the trash button is
//        available.
//      - Click the trash icon + confirm "Yes, delete".
//      - Click "Undo" inside the toast within the 7s window.
//      - Wait long enough for the original delete window to pass and verify:
//          * NO DELETE /api/orders/:id request is ever made.
//          * The order row still exists in the DB.
//          * The card is still visible on /my-orders.
//
// Both English and Arabic label paths are exercised at least once: the
// cancel-undo flow runs in English and the delete-undo flow runs in Arabic.
//
// 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 order undo-toast UI test",
  );
}

// Same precomputed bcrypt hash used by the other UI specs ("TestPass123!").
const TEST_PASSWORD = "TestPass123!";
const TEST_PASSWORD_HASH =
  "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";

// UNDO_WINDOW_MS in artifacts/tx-os/src/pages/my-orders.tsx
const UNDO_WINDOW_MS = 7000;

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 Undo UI', $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];
}

async function insertOrder(userId, serviceId, status) {
  const { rows } = await pool.query(
    `INSERT INTO service_orders (user_id, service_id, status)
     VALUES ($1, $2, $3) RETURNING id`,
    [userId, serviceId, status],
  );
  const id = rows[0].id;
  createdOrderIds.push(id);
  return id;
}

async function getOrderStatus(id) {
  const { rows } = await pool.query(
    `SELECT status FROM service_orders WHERE id = $1`,
    [id],
  );
  return rows[0]?.status ?? null;
}

async function orderExists(id) {
  const { rows } = await pool.query(
    `SELECT 1 FROM service_orders WHERE id = $1`,
    [id],
  );
  return rows.length > 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: also wipe anything else attached to the test users.
    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(),
  ]);
}

// Locate the live toast root. The shadcn Toaster renders Radix's
// <ToastViewport> as an <ol> containing one <li data-state="open"> per active
// toast. We pick the most recently opened one.
function toastRegion(page) {
  return page.locator('li[data-state="open"]').last();
}

test.describe("My Orders undo-toast flow", () => {
  test("English: cancel an order and Undo restores it to pending", async ({
    page,
  }) => {
    const user = await createUser("undo_cancel_en", "en");
    const service = await getAvailableService();
    const serviceName = service.nameEn;

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

    // Place a fresh pending order via the services page.
    await page.goto("/services");
    await page
      .getByRole("button", { name: serviceName, 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");

    // Go to /my-orders and cancel via the in-card confirm dialog.
    await page.goto("/my-orders");
    const firstCard = page.locator(".glass-panel.rounded-xl").first();
    await expect(firstCard).toContainText(serviceName);
    await expect(firstCard).toContainText("Awaiting receiver");

    const statusUrl = `/api/orders/${placedOrder.id}/status`;

    const cancelPatch = page.waitForResponse(
      (r) =>
        new URL(r.url()).pathname === statusUrl &&
        r.request().method() === "PATCH",
      { timeout: 15_000 },
    );
    await firstCard
      .getByRole("button", { name: "Cancel order", exact: true })
      .click();
    const confirmDialog = page.getByRole("alertdialog");
    await expect(confirmDialog).toBeVisible();
    await confirmDialog
      .getByRole("button", { name: "Yes, cancel", exact: true })
      .click();
    const cancelResp = await cancelPatch;
    expect(cancelResp.status()).toBe(200);
    expect((await cancelResp.json()).status).toBe("cancelled");

    // The toast should show with an Undo action; click it well within 7s.
    const toast = toastRegion(page);
    await expect(toast).toContainText("Order cancelled");

    const restorePatch = page.waitForResponse(
      (r) =>
        new URL(r.url()).pathname === statusUrl &&
        r.request().method() === "PATCH",
      { timeout: UNDO_WINDOW_MS - 1000 },
    );
    await toast.getByRole("button", { name: "Undo" }).click();
    const restoreResp = await restorePatch;
    expect(restoreResp.status()).toBe(200);
    const restoredBody = await restoreResp.json();
    expect(restoredBody.status).toBe("pending");

    // UI re-renders: timeline reappears and the cancel button is back.
    const restoredCard = page.locator(".glass-panel.rounded-xl").first();
    await expect(restoredCard.locator("div.h-7.w-7")).toHaveCount(4);
    await expect(restoredCard).toContainText("Awaiting receiver");
    await expect(
      restoredCard.getByRole("button", { name: "Cancel order", exact: true }),
    ).toBeVisible();

    // DB confirms the row was restored.
    expect(await getOrderStatus(placedOrder.id)).toBe("pending");
  });

  test("Arabic: delete a finished order and Undo aborts the deletion", async ({
    page,
  }) => {
    const user = await createUser("undo_delete_ar", "ar");
    const service = await getAvailableService();
    const serviceName = service.nameAr;

    // Seed a completed order so the trash button is available immediately.
    const orderId = await insertOrder(user.id, service.id, "completed");

    await setLanguage(page, "ar");
    await loginViaUi(page, user.username, TEST_PASSWORD);
    await page.goto("/my-orders");

    const card = page.locator(".glass-panel.rounded-xl").first();
    await expect(card).toContainText(serviceName);
    await expect(card).toContainText("مكتمل"); // orderStatus.completed

    // Fail the test if a DELETE actually goes out — Undo must cancel the timer.
    let deleteRequestSeen = false;
    page.on("request", (req) => {
      const path = new URL(req.url()).pathname;
      if (
        path === `/api/orders/${orderId}` &&
        req.method() === "DELETE"
      ) {
        deleteRequestSeen = true;
      }
    });

    // Trash button uses aria-label = t("myOrders.delete") => "حذف" in Arabic.
    await card.getByRole("button", { name: "حذف", exact: true }).click();
    const confirmDialog = page.getByRole("alertdialog");
    await expect(confirmDialog).toBeVisible();
    await confirmDialog
      .getByRole("button", { name: "نعم، احذف", exact: true })
      .click();

    // Card disappears from the list while the delete is pending.
    await expect(
      page.locator(".glass-panel.rounded-xl").filter({ hasText: serviceName }),
    ).toHaveCount(0);

    // Toast title in Arabic = "تم حذف الطلب", action label = "تراجع".
    const toast = toastRegion(page);
    await expect(toast).toContainText("تم حذف الطلب");
    await toast.getByRole("button", { name: "تراجع" }).click();

    // The card should come back to the list immediately after Undo.
    await expect(
      page.locator(".glass-panel.rounded-xl").filter({ hasText: serviceName }),
    ).toHaveCount(1);

    // Wait past the original delete window to make absolutely sure the
    // scheduled DELETE was cancelled (and that no late delete fires).
    await page.waitForTimeout(UNDO_WINDOW_MS + 1500);

    expect(deleteRequestSeen).toBe(false);
    expect(await orderExists(orderId)).toBe(true);
    expect(await getOrderStatus(orderId)).toBe("completed");

    // And the card is still on screen with the same finished state.
    const stillVisible = page
      .locator(".glass-panel.rounded-xl")
      .filter({ hasText: serviceName })
      .first();
    await expect(stillVisible).toBeVisible();
    await expect(stillVisible).toContainText("مكتمل");
  });
});
