import { expect, test } from "@playwright/test";

const CACHE_PREFIX = "who-need-help-static-";
const STALE_CACHE = "who-need-help-static-v0";
const SHELL_PATHS = [
  "/offline.html",
  "/manifest.webmanifest",
  "/assets/css/app.css",
  "/assets/js/app.js",
  "/images/logo.svg",
  "/images/pwa-192.png",
  "/images/pwa-512.png",
  "/images/pwa-maskable-512.png",
];

async function waitForController(page: import("@playwright/test").Page): Promise<void> {
  await page.evaluate(async () => {
    await navigator.serviceWorker.ready;

    if (!navigator.serviceWorker.controller) {
      await new Promise<void>((resolve) => {
        navigator.serviceWorker.addEventListener("controllerchange", () => resolve(), {
          once: true,
        });
      });
    }
  });
}

test("public HTTPS PWA installs, updates its cache, and falls back safely offline", async ({
  context,
  page,
  request,
}) => {
  const consoleFailures: string[] = [];

  page.on("console", (message) => {
    if (["warning", "error"].includes(message.type())) {
      consoleFailures.push(`${message.type()}: ${message.text()}`);
    }
  });
  page.on("pageerror", (error) => consoleFailures.push(`pageerror: ${error.message}`));

  const manifestResponse = await request.get("/manifest.webmanifest");
  expect(manifestResponse.status()).toBe(200);
  expect(manifestResponse.headers()["content-type"]).toContain("application/manifest+json");

  const serviceWorkerResponse = await request.get("/sw.js");
  expect(serviceWorkerResponse.status()).toBe(200);
  expect(serviceWorkerResponse.headers()["content-type"]).toContain("javascript");
  const serviceWorker = await serviceWorkerResponse.text();
  const cacheVersionMatch = serviceWorker.match(
    /const CACHE = `\$\{CACHE_PREFIX\}(v[0-9]+)`/,
  );
  expect(cacheVersionMatch).not.toBeNull();

  if (!cacheVersionMatch) {
    throw new Error("The service worker does not declare a versioned static cache");
  }

  const cacheName = `${CACHE_PREFIX}${cacheVersionMatch[1]}`;

  const manifest = (await manifestResponse.json()) as {
    name: string;
    short_name: string;
    id: string;
    start_url: string;
    scope: string;
    display: string;
    icons: Array<{ src: string; sizes: string; type: string; purpose: string }>;
  };

  expect(manifest).toMatchObject({
    name: "Who Need Help",
    short_name: "Who Need Help",
    id: "/",
    start_url: "/",
    scope: "/",
    display: "standalone",
  });
  expect(manifest.icons).toEqual(
    expect.arrayContaining([
      expect.objectContaining({ sizes: "192x192", purpose: "any" }),
      expect.objectContaining({ sizes: "512x512", purpose: "any" }),
      expect.objectContaining({ sizes: "512x512", purpose: "maskable" }),
      expect.objectContaining({ sizes: "1024x1024", purpose: "maskable" }),
      expect.objectContaining({ sizes: "any", purpose: "monochrome" }),
    ]),
  );

  await page.goto("/");
  await waitForController(page);

  const iconDimensions = await page.evaluate(async () => {
    const dimensions: Record<string, [number, number]> = {};

    for (const path of [
      "/images/pwa-192.png",
      "/images/pwa-512.png",
      "/images/pwa-maskable-512.png",
      "/images/pwa-maskable-1024.png",
    ]) {
      const response = await fetch(path);
      const image = await createImageBitmap(await response.blob());
      dimensions[path] = [image.width, image.height];
      image.close();
    }

    return dimensions;
  });
  expect(iconDimensions).toEqual({
    "/images/pwa-192.png": [192, 192],
    "/images/pwa-512.png": [512, 512],
    "/images/pwa-maskable-512.png": [512, 512],
    "/images/pwa-maskable-1024.png": [1024, 1024],
  });

  await expect
    .poll(() => page.evaluate(() => caches.keys()))
    .toContain(cacheName);

  const cachedShell = await page.evaluate(async (activeCacheName) => {
    const cache = await caches.open(activeCacheName);
    return (await cache.keys()).map((entry) => new URL(entry.url).pathname).sort();
  }, cacheName);
  expect(cachedShell).toEqual(expect.arrayContaining(SHELL_PATHS));

  await page.goto("/safety");
  await page.goto("/users/log-in");
  const cachedDocuments = await page.evaluate(async () => {
    const entries = await Promise.all(
      (await caches.keys()).map(async (cacheName) => {
        const cache = await caches.open(cacheName);
        return Promise.all(
          (await cache.keys()).map(async (entry) => ({
            path: new URL(entry.url).pathname,
            destination: entry.destination,
            contentType: (await cache.match(entry))?.headers.get("content-type") ?? "",
          })),
        );
      }),
    );

    return entries.flat().filter((entry) => entry.contentType.includes("text/html"));
  });
  expect(cachedDocuments).toEqual([
    expect.objectContaining({ path: "/offline.html" }),
  ]);

  await page.evaluate((staleCache) => caches.open(staleCache), STALE_CACHE);
  expect(await page.evaluate(() => caches.keys())).toContain(STALE_CACHE);
  await page.evaluate(async () => {
    const registrations = await navigator.serviceWorker.getRegistrations();
    await Promise.all(registrations.map((registration) => registration.unregister()));
  });
  await page.close();

  const updatedPage = await context.newPage();
  await updatedPage.goto("/");
  await waitForController(updatedPage);
  await expect
    .poll(() => updatedPage.evaluate(() => caches.keys()))
    .not.toContain(STALE_CACHE);
  expect(await updatedPage.evaluate(() => caches.keys())).toContain(cacheName);

  await context.setOffline(true);
  const offlineResponse = await updatedPage.goto("/requests");
  expect(offlineResponse?.status()).toBe(200);
  await expect(updatedPage.getByRole("heading", { name: "You're offline" })).toBeVisible();
  await expect(updatedPage.getByRole("button", { name: "Try again" })).toBeVisible();
  await expect(updatedPage.locator("img[src='/images/logo.svg']")).toBeVisible();
  expect(await updatedPage.evaluate(() => navigator.onLine)).toBe(false);

  await context.setOffline(false);
  await updatedPage.reload();
  await expect(updatedPage.getByRole("heading", { name: "Log in", exact: true })).toBeVisible();
  expect(consoleFailures).toEqual([]);
});
