// UI tests for the bulk "Clear N finished" button on /my-orders.
//
// Covers the bulk-delete + undo path, which is significant because
// scheduleDelete in artifacts/tx-os/src/pages/my-orders.tsx uses a single
// shared setTimeout that owns multiple order IDs at once. A regression in
// the multi-id timer logic (e.g. Undo only cancelling one ID, or the timer
// firing for some IDs but not others) would only show up when a user
// complains, since the existing order-undo-toast.spec.mjs only exercises
// the single-id path.
//
// Three flows are exercised end-to-end:
//
//   1. Bulk Undo (plural toast, English):
//      - Seed 3 finished orders for one user directly in the DB.
//      - Click "Clear 3 finished" + confirm "Yes, clear".
//      - Verify the plural toast "3 orders deleted" appears
//        (myOrders.clearedCount_other).
//      - Click Undo inside the 7s window.
//      - Verify NO DELETE /api/orders/:id requests are ever made.
//      - Verify all 3 rows still exist in the DB.
//      - Verify all 3 cards are visible again on /my-orders.
//
//   2. Bulk timer expiry (plural toast, English):
//      - Seed 2 finished orders.
//      - Click "Clear 2 finished" + confirm.
//      - Verify the plural toast "2 orders deleted" appears.
//      - Do NOT click Undo. Wait past UNDO_WINDOW_MS for the deletes to
//        actually fire.
//      - Verify both rows have been deleted from the DB.
//
//   3. Bulk singular toast (Arabic):
//      - Seed exactly 1 finished order.
//      - Click the "مسح طلب منتهٍ" (Clear 1 finished) button + confirm.
//      - When ids.length === 1 the bulk path uses myOrders.deleted, so the
//        toast title is "تم حذف الطلب" (NOT clearedCount_one). This
//        intentionally exercises the singular branch of the title selector
//        in scheduleDelete().
//      - Click Undo and verify the row remains in the DB and the card is
//        visible again.
//
// 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 bulk clear-finished 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, 'Bulk Clear 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 insertFinishedOrders(userId, serviceId, count, status = "completed") {
  const ids = [];
  for (let i = 0; i < count; i += 1) {
    ids.push(await insertOrder(userId, serviceId, status));
  }
  return ids;
}

async function countExisting(ids) {
  if (ids.length === 0) return 0;
  const { rows } = await pool.query(
    `SELECT COUNT(*)::int AS n FROM service_orders WHERE id = ANY($1::int[])`,
    [ids],
  );
  return rows[0].n;
}

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

// Track DELETE /api/orders/:id calls so we can assert on them.
function trackDeletes(page, ids) {
  const seen = new Set();
  const expected = new Set(ids.map((id) => `/api/orders/${id}`));
  page.on("request", (req) => {
    const path = new URL(req.url()).pathname;
    if (req.method() === "DELETE" && expected.has(path)) {
      seen.add(path);
    }
  });
  return {
    seen,
    sawAnyDelete: () => seen.size > 0,
    sawAllDeletes: () => seen.size === ids.length,
  };
}

test.describe("My Orders bulk Clear-finished + undo flow", () => {
  test("English plural: Clear 3 finished + Undo restores all rows", async ({
    page,
  }) => {
    const user = await createUser("clear_undo_en", "en");
    const service = await getAvailableService();
    const serviceName = service.nameEn;

    const ids = await insertFinishedOrders(user.id, service.id, 3, "completed");

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

    // All 3 cards should be visible up front.
    const cards = page
      .locator(".glass-panel.rounded-xl")
      .filter({ hasText: serviceName });
    await expect(cards).toHaveCount(3);

    // The "Clear N finished" button uses myOrders.clearFinished_other for N>=2.
    const clearBtn = page.getByRole("button", {
      name: "Clear 3 finished",
      exact: true,
    });
    await expect(clearBtn).toBeVisible();

    // Fail the test if any DELETE goes out — Undo must cancel the timer.
    const tracker = trackDeletes(page, ids);

    await clearBtn.click();
    const confirmDialog = page.getByRole("alertdialog");
    await expect(confirmDialog).toBeVisible();
    await confirmDialog
      .getByRole("button", { name: "Yes, clear", exact: true })
      .click();

    // Cards are filtered out of the list while delete is pending.
    await expect(cards).toHaveCount(0);

    // Plural toast: myOrders.clearedCount_other => "{count} orders deleted".
    const toast = toastRegion(page);
    await expect(toast).toContainText("3 orders deleted");

    // Click Undo well inside the 7s window.
    await toast.getByRole("button", { name: "Undo" }).click();

    // Cards reappear immediately.
    await expect(cards).toHaveCount(3);

    // Wait past the original delete window to make sure the scheduled
    // shared-timer DELETE never fires for any of the IDs.
    await page.waitForTimeout(UNDO_WINDOW_MS + 1500);

    expect(tracker.sawAnyDelete()).toBe(false);

    // All 3 rows still exist in the DB.
    expect(await countExisting(ids)).toBe(3);

    // Cards still visible after the wait.
    await expect(cards).toHaveCount(3);
  });

  test("English plural: Clear 2 finished + timer expiry deletes all rows", async ({
    page,
  }) => {
    const user = await createUser("clear_expire_en", "en");
    const service = await getAvailableService();
    const serviceName = service.nameEn;

    // Mix completed + cancelled to confirm both finished states are swept.
    const completedIds = await insertFinishedOrders(
      user.id,
      service.id,
      1,
      "completed",
    );
    const cancelledIds = await insertFinishedOrders(
      user.id,
      service.id,
      1,
      "cancelled",
    );
    const ids = [...completedIds, ...cancelledIds];

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

    const cards = page
      .locator(".glass-panel.rounded-xl")
      .filter({ hasText: serviceName });
    await expect(cards).toHaveCount(2);

    const tracker = trackDeletes(page, ids);

    const clearBtn = page.getByRole("button", {
      name: "Clear 2 finished",
      exact: true,
    });
    await clearBtn.click();
    const confirmDialog = page.getByRole("alertdialog");
    await expect(confirmDialog).toBeVisible();
    await confirmDialog
      .getByRole("button", { name: "Yes, clear", exact: true })
      .click();

    // Plural toast appears with "2 orders deleted".
    const toast = toastRegion(page);
    await expect(toast).toContainText("2 orders deleted");

    // Cards are gone from the list while delete is pending.
    await expect(cards).toHaveCount(0);

    // Wait for the shared timer to fire and BOTH DELETE calls to complete.
    await expect
      .poll(() => tracker.seen.size, {
        timeout: UNDO_WINDOW_MS + 10_000,
        intervals: [250, 500, 1000],
      })
      .toBe(2);

    // Both rows are removed from the DB.
    await expect
      .poll(() => countExisting(ids), {
        timeout: 5000,
        intervals: [250, 500],
      })
      .toBe(0);

    // Cards stay gone.
    await expect(cards).toHaveCount(0);
  });

  test("Arabic singular: Clear 1 finished uses the singular toast title", async ({
    page,
  }) => {
    const user = await createUser("clear_singular_ar", "ar");
    const service = await getAvailableService();
    const serviceName = service.nameAr;

    const [orderId] = await insertFinishedOrders(
      user.id,
      service.id,
      1,
      "completed",
    );

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

    const cards = page
      .locator(".glass-panel.rounded-xl")
      .filter({ hasText: serviceName });
    await expect(cards).toHaveCount(1);

    // Fail the test if a DELETE actually goes out — Undo must cancel the timer.
    const tracker = trackDeletes(page, [orderId]);

    // Arabic clearFinished_one = "مسح طلب منتهٍ".
    const clearBtn = page.getByRole("button", {
      name: "مسح طلب منتهٍ",
      exact: true,
    });
    await expect(clearBtn).toBeVisible();
    await clearBtn.click();

    const confirmDialog = page.getByRole("alertdialog");
    await expect(confirmDialog).toBeVisible();
    // Arabic clearConfirm = "نعم، امسح".
    await confirmDialog
      .getByRole("button", { name: "نعم، امسح", exact: true })
      .click();

    // Card filtered out while pending.
    await expect(cards).toHaveCount(0);

    // Singular branch in scheduleDelete uses myOrders.deleted (NOT
    // clearedCount_one). In Arabic that is "تم حذف الطلب".
    const toast = toastRegion(page);
    await expect(toast).toContainText("تم حذف الطلب");

    // Undo within the window. Arabic undo label = "تراجع".
    await toast.getByRole("button", { name: "تراجع" }).click();

    // Card returns immediately.
    await expect(cards).toHaveCount(1);

    // Wait past the original delete window to confirm the timer was cancelled.
    await page.waitForTimeout(UNDO_WINDOW_MS + 1500);

    expect(tracker.sawAnyDelete()).toBe(false);
    expect(await countExisting([orderId])).toBe(1);
    await expect(cards).toHaveCount(1);
  });
});
