import { expect, test } from "@playwright/test";
import {
  clickUntilVisible,
  gotoLiveView,
  markerKinds,
  projectEmail,
  projectText,
  registerAndConfirm,
  selectOptionContaining,
  waitForLiveViewConnected,
} from "./helpers";

async function positionCount(page: import("@playwright/test").Page): Promise<number> {
  const raw = await page.locator("#e2e-runtime-node").getAttribute("data-position-count");
  return Number(raw ?? "-1");
}

async function runtimeSignature(
  page: import("@playwright/test").Page,
): Promise<{ node: string; bootID: string } | null> {
  const runtime = page.locator("#e2e-runtime-node");
  const [node, bootID] = await Promise.all([
    runtime.getAttribute("data-node"),
    runtime.getAttribute("data-boot-id"),
  ]);

  return node && bootID ? { node, bootID } : null;
}

test("active chat and browser tracking recover after the serving BEAM node restarts", async ({
  browser,
  request,
}, testInfo) => {
  const requester = await registerAndConfirm(
    browser,
    request,
    projectEmail("failover-requester", testInfo.project.name),
    "Failover Requester",
  );
  const helper = await registerAndConfirm(
    browser,
    request,
    projectEmail("failover-helper", testInfo.project.name),
    "Failover Helper",
  );
  const requestTitle = projectText("Failover medicine pickup", testInfo.project.name);

  await gotoLiveView(requester.page, "/requests/new");
  await selectOptionContaining(requester.page, "Category", "Medicine pickup");
  await requester.page.getByLabel("Medicine pickup status").selectOption("reserved");
  await requester.page.getByLabel("Short title").fill(requestTitle);
  await requester.page
    .getByLabel("What help do you need?")
    .fill("A run-scoped reserved medicine pickup for failover verification.");
  await requester.page.getByLabel("Urgency").selectOption("now");
  await requester.page
    .getByLabel("Request expires (UTC)")
    .fill(new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString().slice(0, 16));
  await requester.page.getByLabel("Approximate area").fill("Failover district");
  await requester.page.getByLabel("Location visibility").selectOption("approximate_public");
  await requester.page.getByLabel("Latitude").fill("50.4501");
  await requester.page.getByLabel("Longitude").fill("30.5234");
  await requester.page.locator("#request-form input[type=checkbox]").check();
  await requester.page.getByRole("button", { name: "Publish request" }).click();
  await expect(requester.page).toHaveURL(/\/requests\/[0-9a-f-]+$/);

  const requestURL = requester.page.url();
  const origin = new URL(process.env.BASE_URL!).origin;
  await helper.context.grantPermissions(["geolocation"], { origin });
  await helper.context.setGeolocation({
    latitude: 50.4504,
    longitude: 30.5237,
    accuracy: 5,
  });
  await gotoLiveView(helper.page, requestURL);
  await clickUntilVisible(
    helper.page.getByRole("button", { name: "I can help" }),
    helper.page.getByRole("heading", { name: "Private match chat" }),
  );
  await expect(requester.page.getByRole("heading", { name: "Private match chat" })).toBeVisible();

  const browserPosition = await helper.page.evaluate(
    () =>
      new Promise<{ latitude: number; longitude: number }>((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(
          ({ coords }) =>
            resolve({
              latitude: coords.latitude,
              longitude: coords.longitude,
            }),
          ({ code, message }) => reject(new Error(`geolocation ${code}: ${message}`)),
        );
      }),
  );
  expect(browserPosition.latitude).toBeCloseTo(50.4504, 4);
  expect(browserPosition.longitude).toBeCloseTo(30.5237, 4);

  await helper.page.getByRole("button", { name: "Share location" }).click();
  await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
  await expect.poll(() => positionCount(helper.page)).toBe(1);
  await expect.poll(() => positionCount(requester.page)).toBe(1);
  await expect.poll(() => markerKinds(requester.page)).toContain("Shared location");

  await helper.page
    .getByPlaceholder("Write a safe coordination message…")
    .fill("Message before the serving node restart.");
  await helper.page.getByRole("button", { name: "Send", exact: true }).click();
  await expect(requester.page.getByText("Message before the serving node restart.")).toBeVisible();

  await helper.page.evaluate(() => {
    const state = { sawDisconnected: false };

    (
      window as typeof window & {
        __wnhFailoverState?: { sawDisconnected: boolean };
      }
    ).__wnhFailoverState = state;

    window.addEventListener("phx:page-loading-start", (event) => {
      const detail = (event as CustomEvent<{ kind?: string }>).detail;

      if (detail?.kind === "error") {
        state.sawDisconnected = true;
      }
    });
  });

  const originalRuntime = await runtimeSignature(helper.page);
  expect(originalRuntime).not.toBeNull();

  const crash = await request.post("/__e2e__/crash-node", {
    data: {
      confirmation: "crash-exact-e2e-node",
      node: originalRuntime!.node,
    },
  });
  expect(crash.status()).toBe(202);
  expect(await crash.json()).toMatchObject({
    status: "scheduled",
    node: originalRuntime!.node,
  });

  await expect
    .poll(
      () =>
        helper.page.evaluate(
          () =>
            (
              window as typeof window & {
                __wnhFailoverState?: { sawDisconnected: boolean };
              }
            ).__wnhFailoverState?.sawDisconnected ?? false,
        ),
      { timeout: 30_000 },
    )
    .toBe(true);

  await waitForLiveViewConnected(helper.page);
  let recoveredRuntime: Awaited<ReturnType<typeof runtimeSignature>> = null;
  await expect
    .poll(
      async () => {
        recoveredRuntime = await runtimeSignature(helper.page);
        return recoveredRuntime
          ? `${recoveredRuntime.node}/${recoveredRuntime.bootID}`
          : null;
      },
      { timeout: 30_000 },
    )
    .not.toBe(`${originalRuntime!.node}/${originalRuntime!.bootID}`);

  await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
  await expect.poll(() => positionCount(helper.page), { timeout: 30_000 }).toBe(1);
  await expect.poll(() => positionCount(requester.page), { timeout: 30_000 }).toBe(1);
  await expect.poll(() => markerKinds(requester.page), { timeout: 30_000 }).toContain(
    "Shared location",
  );

  await helper.page
    .getByPlaceholder("Write a safe coordination message…")
    .fill("Message after LiveView recovered on the available replicas.");
  await helper.page.getByRole("button", { name: "Send", exact: true }).click();
  await expect(
    requester.page.getByText("Message after LiveView recovered on the available replicas."),
  ).toBeVisible();

  await helper.page.getByRole("button", { name: "Stop and delete position" }).click();
  await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible();
  await expect.poll(() => positionCount(helper.page)).toBe(0);
  await expect.poll(() => positionCount(requester.page)).toBe(0);
  await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location");

  await testInfo.attach("active-failover-evidence", {
    contentType: "application/json",
    body: Buffer.from(
      JSON.stringify(
        {
          browser: testInfo.project.name,
          original_runtime: originalRuntime,
          recovered_runtime: recoveredRuntime,
          saw_liveview_disconnect: true,
          tracking_recovered: true,
          chat_recovered: true,
          tracking_position_deleted: true,
        },
        null,
        2,
      ),
    ),
  });

  await requester.context.close();
  await helper.context.close();
});
