import { execFileSync } from "node:child_process";
import { readlinkSync, utimesSync } from "node:fs";
import { expect } from "e2e";
import { APP_DIR } from "../e2e.config.ts";
import { test, WEB } from "./t3.ts";

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

// Signal only the listening server whose cwd belongs to the app under test.
function serverPid() {
  const sockets = execFileSync("ss", ["-H", "-ltnp"], { encoding: "utf8" });
  const pids = [...new Set([...sockets.matchAll(/pid=(\d+)/g)].map(match => Number(match[1])))];
  const own = pids.filter(pid => {
    try { return readlinkSync(`/proc/${pid}/cwd`) === `${APP_DIR}/apps/server`; }
    catch { return false; }
  });
  if (own.length !== 1) throw new Error(`Expected one app server listener, found ${own.length}`);
  return own[0]!;
}

test("An interrupted image-only first send reports failure behind a newer draft", WEB,
  async ({ t3, app, browser, screen }) => {
    await t3.project("demo");
    await t3.open();
    const composer = screen.getByRole("textbox");
    await composer.waitFor({ state: "visible" });
    await browser.evaluate(async () => {
      const canvas = new OffscreenCanvas(200, 120);
      const context = canvas.getContext("2d")!;
      context.fillStyle = "#3478db";
      context.fillRect(0, 0, 200, 120);
      context.fillStyle = "white";
      context.font = "20px sans-serif";
      context.fillText("Sample image", 20, 65);
      const png = await canvas.convertToBlob({ type: "image/png" });
      await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
    });
    await composer.tap();
    const uploaded = browser.waitForResponse("**/api/attachments/upload/**");
    await browser.keyboard.press("ControlOrMeta+V");
    await uploaded;
    await delay(500);
    await screen.getByRole("button", "Preview image.png", { exact: true }).waitFor({ state: "visible" });
    const pid = serverPid();
    process.kill(pid, "SIGSTOP");
    let killed = false;
    try {
      await composer.press("Enter");
      await screen.getByRole("button", "Submitting message").waitFor({ state: "visible" });
      await composer.fill("next draft");
      await app.screenshot("send-pending-with-new-draft");
      await screen.getByText("Scenario is reconnecting", { exact: false }).waitFor({ state: "visible", timeout: 120_000 });
      process.kill(pid, "SIGKILL");
      killed = true;
      // Node watch waits for a file event after a killed child. Restart the same server.
      const now = new Date();
      utimesSync(`${APP_DIR}/apps/server/src/bin.ts`, now, now);
      await screen.getByText("Scenario is reconnecting", { exact: false }).waitFor({ state: "hidden", timeout: 120_000 });
      await delay(8_000);
      await app.screenshot("interrupted-image-send");
      await expect(screen.getByText("Message may not have been sent", { exact: true })).toBeVisible();
      await expect(screen.getByText("Your new draft was kept.", { exact: true })).toBeVisible();
      await expect(composer).toHaveText("next draft");
      await expect(screen.getByRole("button", "Copy message")).toHaveCount(0);
      await expect(screen.getByRole("button", "Copy")).toHaveCount(0);
      await delay(10_000);
      await app.screenshot("notice-persists");
      await expect(screen.getByText("Message may not have been sent", { exact: true })).toBeVisible();
      await screen.getByRole("button", "Dismiss notification").tap();
      await expect(screen.getByText("Message may not have been sent", { exact: true })).toBeHidden();
      await expect(composer).toHaveText("next draft");
      await app.screenshot("notice-dismissed-draft-kept");
    } finally {
      if (!killed) process.kill(pid, "SIGCONT");
    }
  });
