// E2E test for: notification sounds use Web Audio so they
// reliably play on iPad Safari after a single user gesture. Verifies
// (1) the AudioContext reaches `running` state after the user clicks
// the per-sound preview button in notification settings, (2) the play
// counter increments, and (3) the testPlay path is not subject to the
// 3-second burst-coalescing throttle.
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 UI test");
}

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

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

const createdUserIds = [];

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

async function createUser(prefix, displayEn) {
  const username = `${prefix}_${uniq()}`;
  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, displayEn],
  );
  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 };
}

test.afterAll(async () => {
  if (createdUserIds.length > 0) {
    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 loginViaUi(page, username) {
  await page.goto("/login");
  await page.locator("#username").fill(username);
  await page.locator("#password").fill(TEST_PASSWORD);
  await Promise.all([
    page.waitForURL((url) => !url.pathname.endsWith("/login"), {
      timeout: 15_000,
    }),
    page.locator('form button[type="submit"]').click(),
  ]);
}

test("preview sound button activates Web Audio and increments play counter", async ({
  browser,
}) => {
  test.setTimeout(60_000);
  const user = await createUser("audio_unlock", "Audio Unlock");
  const ctx = await browser.newContext();
  const page = await ctx.newPage();

  try {
    await loginViaUi(page, user.username);
    await page.goto("/");
    // Reset counter so we can assert exactly the clicks we drive below.
    await page.evaluate(() => {
      window.__txosNotifPlayCount = 0;
      window.__txosNotifLastSound = undefined;
    });

    // Open the notification settings popover via its bell trigger
    // (aria-label "Notification settings" / "إعدادات الإشعارات"). The
    // login defaults to English.
    await page.getByRole("button", { name: /notification sounds/i }).click();

    // Radix Popover renders into a portal — wait until the popover
    // body is on screen before reaching for the preview buttons.
    await expect(
      page.getByText(/notification sounds/i).last(),
    ).toBeVisible({ timeout: 5_000 });

    // First per-sound preview play — this is the gesture that should
    // bring the AudioContext to `running` state on every browser.
    const previews = page.getByRole("button", { name: /^preview$/i });
    await expect(previews.first()).toBeVisible({ timeout: 5_000 });
    await previews.first().click();

    // The play counter is bumped synchronously inside play().
    await expect
      .poll(() => page.evaluate(() => window.__txosNotifPlayCount ?? 0), {
        timeout: 3_000,
      })
      .toBe(1);

    const lastSound = await page.evaluate(() => window.__txosNotifLastSound);
    expect(typeof lastSound).toBe("string");
    expect(lastSound && lastSound.length).toBeGreaterThan(0);

    // testPlay must bypass the 3 s throttle so users can audition
    // several sounds back-to-back from settings. Click a second
    // preview immediately and require the counter to advance.
    await previews.nth(1).click();
    await expect
      .poll(() => page.evaluate(() => window.__txosNotifPlayCount ?? 0), {
        timeout: 3_000,
      })
      .toBe(2);

    // Verify the actual production singleton player's AudioContext
    // reached `running` state as a result of the preview-button
    // gesture. This is what iPad Safari requires to subsequently
    // play out-of-gesture, socket-driven chimes.
    await expect
      .poll(
        () =>
          page.evaluate(() => {
            const fn = /** @type {any} */ (window).__txosNotifCtxState;
            return typeof fn === "function" ? fn() : null;
          }),
        { timeout: 3_000 },
      )
      .toBe("running");
  } finally {
    await ctx.close();
  }
});
