// Browser tests for the inline dependency counts on the admin lists.
//
// Locks in three things for the Apps / Services / Users panels:
//   1. The `[data-testid^="<entity>-counts-"]` container + each
//      populated child chip render when dependent records exist.
//   2. The container is omitted (count = 0 in DOM) for an entity
//      with zero dependents.
//   3. Layout + localized text hold up in both English and Arabic.

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 this test");
}

const TEST_PASSWORD = "TestPass123!";
const TEST_PASSWORD_HASH =
  "$2b$10$Bs636ukPMyz01nKrsi.5m.JlDXSN22AVCvn8cgPWWDbo5yJRQX2vu";

const pool = new pg.Pool({ connectionString: DATABASE_URL });

const createdUserIds = [];
const createdAppIds = [];
const createdServiceIds = [];
const createdGroupIds = [];

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

async function createUser(prefix, opts = {}) {
  const username = `${prefix}_${uniqueSuffix()}`;
  const { rows } = await pool.query(
    `INSERT INTO users (username, email, password_hash, display_name_ar, display_name_en, preferred_language, is_active)
     VALUES ($1, $2, $3, $4, $5, $6, true) RETURNING id`,
    [
      username,
      `${username}@example.com`,
      TEST_PASSWORD_HASH,
      opts.displayNameAr ?? prefix,
      opts.displayNameEn ?? prefix,
      opts.preferredLanguage ?? "en",
    ],
  );
  const id = rows[0].id;
  createdUserIds.push(id);
  const roleName = opts.admin ? "admin" : "user";
  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 createApp(prefix) {
  const slug = `${prefix}_${uniqueSuffix()}`;
  const { rows } = await pool.query(
    `INSERT INTO apps (name_ar, name_en, slug, icon_name, route, color, sort_order, description_en)
     VALUES ($1, $2, $3, 'Grid2X2', '/test', '#000000', 999, 'inline counts test app')
     RETURNING id`,
    [`تطبيق ${prefix}`, `App ${prefix}`, slug],
  );
  const id = rows[0].id;
  createdAppIds.push(id);
  return { id, slug };
}

async function createGroup(prefix) {
  const name = `${prefix}_${uniqueSuffix()}`;
  const { rows } = await pool.query(
    `INSERT INTO groups (name, description_en) VALUES ($1, 'inline counts test group') RETURNING id`,
    [name],
  );
  const id = rows[0].id;
  createdGroupIds.push(id);
  return { id, name };
}

async function attachAppToGroup(appId, groupId) {
  await pool.query(
    `INSERT INTO group_apps (group_id, app_id) VALUES ($1, $2)`,
    [groupId, appId],
  );
}

async function attachRestrictionToApp(appId) {
  // Any permission row is fine — the count is just count(*) on
  // app_permissions for this appId.
  const { rows } = await pool.query(`SELECT id FROM permissions LIMIT 1`);
  if (rows.length === 0) {
    throw new Error("No permissions exist in DB; cannot attach restriction");
  }
  await pool.query(
    `INSERT INTO app_permissions (app_id, permission_id) VALUES ($1, $2)`,
    [appId, rows[0].id],
  );
}

async function recordAppOpen(userId, appId) {
  await pool.query(
    `INSERT INTO app_opens (user_id, app_id) VALUES ($1, $2)`,
    [userId, appId],
  );
}

async function createService(prefix) {
  const nameEn = `Service ${prefix} ${uniqueSuffix()}`;
  const nameAr = `خدمة ${prefix} ${uniqueSuffix()}`;
  const { rows } = await pool.query(
    `INSERT INTO services (name_ar, name_en, price, is_available, sort_order)
     VALUES ($1, $2, '1.00', true, 999) RETURNING id`,
    [nameAr, nameEn],
  );
  const id = rows[0].id;
  createdServiceIds.push(id);
  return { id, nameEn, nameAr };
}

async function createOrder(userId, serviceId) {
  await pool.query(
    `INSERT INTO service_orders (user_id, service_id) VALUES ($1, $2)`,
    [userId, serviceId],
  );
}

async function createNote(userId) {
  await pool.query(
    `INSERT INTO notes (user_id, title, content) VALUES ($1, 'inline counts', 'test')`,
    [userId],
  );
}

test.afterAll(async () => {
  // Order matters: notes / service_orders must go before users
  // (user_id has no ON DELETE rule). service_orders must also go
  // before services (service_id is ON DELETE RESTRICT).
  if (createdUserIds.length > 0) {
    await pool.query(
      `DELETE FROM notes WHERE user_id = ANY($1::int[])`,
      [createdUserIds],
    );
    await pool.query(
      `DELETE FROM service_orders WHERE user_id = ANY($1::int[])`,
      [createdUserIds],
    );
  }
  if (createdServiceIds.length > 0) {
    await pool.query(
      `DELETE FROM service_orders WHERE service_id = ANY($1::int[])`,
      [createdServiceIds],
    );
    await pool.query(
      `DELETE FROM services WHERE id = ANY($1::int[])`,
      [createdServiceIds],
    );
  }
  if (createdAppIds.length > 0) {
    await pool.query(
      `DELETE FROM app_opens WHERE app_id = ANY($1::int[])`,
      [createdAppIds],
    );
    await pool.query(
      `DELETE FROM app_permissions WHERE app_id = ANY($1::int[])`,
      [createdAppIds],
    );
    await pool.query(
      `DELETE FROM group_apps WHERE app_id = ANY($1::int[])`,
      [createdAppIds],
    );
    await pool.query(
      `DELETE FROM apps WHERE id = ANY($1::int[])`,
      [createdAppIds],
    );
  }
  if (createdGroupIds.length > 0) {
    await pool.query(
      `DELETE FROM user_groups WHERE group_id = ANY($1::int[])`,
      [createdGroupIds],
    );
    await pool.query(
      `DELETE FROM group_apps WHERE group_id = ANY($1::int[])`,
      [createdGroupIds],
    );
    await pool.query(
      `DELETE FROM groups WHERE id = ANY($1::int[])`,
      [createdGroupIds],
    );
  }
  if (createdUserIds.length > 0) {
    await pool.query(
      `DELETE FROM user_roles WHERE user_id = ANY($1::int[])`,
      [createdUserIds],
    );
    await pool.query(
      `DELETE FROM user_groups WHERE user_id = ANY($1::int[])`,
      [createdUserIds],
    );
    await pool.query(`DELETE FROM users WHERE id = ANY($1::int[])`, [
      createdUserIds,
    ]);
  }
  await pool.end();
});

async function login(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 setLanguage(page, lang) {
  // The app persists language via localStorage key `tx-lang` (see
  // artifacts/tx-os/src/i18n.ts). Set it before any page mount so the
  // very first render is in the chosen language.
  await page.addInitScript((l) => {
    try {
      window.localStorage.setItem("tx-lang", l);
    } catch {}
  }, lang);
}

// Localized chip labels pulled from artifacts/tx-os/src/locales/{en,ar}.json.
// We assert against the unit word so future plural variants still pass.
const COUNT_TEXT = {
  en: {
    appGroups: "groups",
    appRestrictions: "restrictions",
    appOpens: "opens",
    serviceOrders: "orders",
    userNotes: "notes",
    userOrders: "orders",
  },
  ar: {
    appGroups: "مجموعة",
    appRestrictions: "قيد",
    appOpens: "فتحة",
    serviceOrders: "طلب",
    userNotes: "ملاحظة",
    userOrders: "طلب",
  },
};

for (const lang of ["en", "ar"]) {
  test.describe(`Admin inline dependency counts (${lang})`, () => {
    test("counts render for entities with deps and are omitted when zero", async ({
      page,
    }) => {
      const labels = COUNT_TEXT[lang];

      // ---- Setup: admin + paired (with-deps / no-deps) entities ----
      const admin = await createUser("counts_admin", {
        admin: true,
        preferredLanguage: lang,
      });

      const appWithDeps = await createApp("withdeps");
      const appNoDeps = await createApp("nodeps");
      const groupForApp = await createGroup("appgroup");
      await attachAppToGroup(appWithDeps.id, groupForApp.id);
      await attachRestrictionToApp(appWithDeps.id);
      await recordAppOpen(admin.id, appWithDeps.id);

      const serviceWithOrders = await createService("withorders");
      const serviceNoOrders = await createService("noorders");
      await createOrder(admin.id, serviceWithOrders.id);

      const userWithDeps = await createUser("u_withdeps");
      const userNoDeps = await createUser("u_nodeps");
      await createNote(userWithDeps.id);
      // Pile the user's order onto the SAME service that already has
      // one so `serviceNoOrders` stays at zero orders and we can assert
      // its counts row is omitted.
      await createOrder(userWithDeps.id, serviceWithOrders.id);

      // ---- Login ----
      await setLanguage(page, lang);
      await login(page, admin.username, TEST_PASSWORD);

      // ===== Apps panel =====
      await page.goto("/admin#section=apps");

      const withCounts = page.locator(
        `[data-testid="app-counts-${appWithDeps.id}"]`,
      );
      await expect(withCounts).toBeVisible({ timeout: 10_000 });

      const groupsChip = page.locator(
        `[data-testid="app-counts-groups-${appWithDeps.id}"]`,
      );
      const restrictionsChip = page.locator(
        `[data-testid="app-counts-restrictions-${appWithDeps.id}"]`,
      );
      const opensChip = page.locator(
        `[data-testid="app-counts-opens-${appWithDeps.id}"]`,
      );
      await expect(groupsChip).toBeVisible();
      await expect(restrictionsChip).toBeVisible();
      await expect(opensChip).toBeVisible();
      // Localized text: "1 groups" (en) or "1 مجموعة" (ar) etc.
      await expect(groupsChip).toContainText("1");
      await expect(groupsChip).toContainText(labels.appGroups);
      await expect(restrictionsChip).toContainText("1");
      await expect(restrictionsChip).toContainText(labels.appRestrictions);
      await expect(opensChip).toContainText("1");
      await expect(opensChip).toContainText(labels.appOpens);

      // The companion app row exists but its counts container is omitted.
      // The delete button anchors the row so we know the app rendered.
      await expect(
        page.locator(`[data-testid="app-delete-${appNoDeps.id}"]`),
      ).toBeVisible();
      await expect(
        page.locator(`[data-testid="app-counts-${appNoDeps.id}"]`),
      ).toHaveCount(0);

      // ===== Services panel =====
      // page.goto that only changes the URL hash on the same path does
      // NOT trigger a full reload, and the admin page reads `?section=`
      // only on mount — pair every goto with reload() to re-apply it.
      await page.goto("/admin#section=services");
      await page.reload();

      const serviceCounts = page.locator(
        `[data-testid="service-counts-${serviceWithOrders.id}"]`,
      );
      await expect(serviceCounts).toBeVisible({ timeout: 10_000 });
      const ordersChip = page.locator(
        `[data-testid="service-counts-orders-${serviceWithOrders.id}"]`,
      );
      await expect(ordersChip).toBeVisible();
      // Two orders ride on this service (one from the admin and one
      // from `userWithDeps`), so just assert the chip is non-empty and
      // ends in the localized noun rather than pinning the exact count.
      await expect(ordersChip).toContainText(labels.serviceOrders);
      await expect(ordersChip).toContainText(/[1-9]/);

      // The bare service: row present (delete button), counts row absent.
      await expect(
        page.locator(`[data-testid="service-delete-${serviceNoOrders.id}"]`),
      ).toBeVisible();
      await expect(
        page.locator(`[data-testid="service-counts-${serviceNoOrders.id}"]`),
      ).toHaveCount(0);

      // ===== Users panel =====
      await page.goto("/admin#section=users");
      await page.reload();

      // Filter by username so the test rows are guaranteed to render.
      const search = page.locator('[data-testid="user-search"]');
      await expect(search).toBeVisible({ timeout: 10_000 });
      // The two test users share the prefix `u_` — search by that to
      // narrow the list to just our created rows.
      await search.fill("u_");

      // Wait for the row with deps to appear. The counts container is
      // the assertion target; if it's visible the row obviously is too.
      const userCounts = page.locator(
        `[data-testid="user-counts-${userWithDeps.id}"]`,
      );
      await expect(userCounts).toBeVisible({ timeout: 10_000 });

      const notesChip = page.locator(
        `[data-testid="user-counts-notes-${userWithDeps.id}"]`,
      );
      const userOrdersChip = page.locator(
        `[data-testid="user-counts-orders-${userWithDeps.id}"]`,
      );
      await expect(notesChip).toBeVisible();
      await expect(userOrdersChip).toBeVisible();
      await expect(notesChip).toContainText("1");
      await expect(notesChip).toContainText(labels.userNotes);
      await expect(userOrdersChip).toContainText("1");
      await expect(userOrdersChip).toContainText(labels.userOrders);

      // The bare user: row present (delete button), counts row absent.
      await expect(
        page.locator(`[data-testid="user-delete-${userNoDeps.id}"]`),
      ).toBeVisible();
      await expect(
        page.locator(`[data-testid="user-counts-${userNoDeps.id}"]`),
      ).toHaveCount(0);

      // ---- Bilingual layout sanity check ----
      // The chip row uses `flex flex-wrap` and inherits text direction
      // from <html dir>. Verify the document direction matches the
      // selected language so a future regression that hard-codes LTR
      // would fail here. Also assert the chip row itself is laid out as
      // a flex container with non-zero width in the active direction.
      const expectedDir = lang === "ar" ? "rtl" : "ltr";
      await expect(page.locator("html")).toHaveAttribute("dir", expectedDir);

      const counts = await userCounts.evaluate((el) => {
        const cs = window.getComputedStyle(el);
        return {
          display: cs.display,
          direction: cs.direction,
          width: el.getBoundingClientRect().width,
        };
      });
      expect(counts.display).toBe("flex");
      expect(counts.direction).toBe(expectedDir);
      expect(counts.width).toBeGreaterThan(0);
    });
  });
}
