import { expect } from "e2e";
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, watch } from "node:fs";
import { test, WEB } from "./t3.ts";

test("A clean synchronized branch has no empty Git actions menu", WEB, async ({ t3, app, screen, browser }) => {
  const capture = async (label: string) => {
    let previous = await app.screenshot(`${label}-settling`);
    for (let attempt = 0; attempt < 10; attempt++) {
      await new Promise(resolve => setTimeout(resolve, 500));
      const current = await app.screenshot(label);
      if (readFileSync(previous).equals(readFileSync(current))) return;
      previous = current;
    }
    throw new Error(`Capture did not settle: ${label}`);
  };
  const author = ["-c", "user.name=Scenario", "-c", "user.email=scenario@example.invalid"];
  const repositories: Record<string, string> = {};
  for (const kind of ["Clean", "Publishing", "Detached", "Behind"]) {
    const project = await t3.project(`${kind.toLowerCase()}-demo`, { "README.md": "# Git menu demo\n" });
    repositories[kind] = project;
    const git = (...args: string[]) => execFileSync("git", args, { cwd: project, encoding: "utf8" }).trim();
    if (kind !== "Publishing") {
      const upstream = `${project}-upstream.git`;
      git("init", "--bare", "--quiet", upstream);
      git("remote", "add", "origin", upstream);
      git("push", "--quiet", "-u", "origin", "main");
      git("symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main");
      if (kind === "Behind") {
        const newer = git(...author, "commit-tree", "HEAD^{tree}", "-p", "HEAD", "-m", "Upstream update");
        git("push", "--quiet", "origin", `${newer}:refs/heads/main`);
        git("fetch", "--quiet", "origin");
      }
    }
    if (kind === "Detached") git("checkout", "--quiet", "--detach", "HEAD");
    await t3.thread({ project, title: `${kind} verification`, messages: ["Show the project summary"] });
  }
  await t3.open();
  const options = screen.getByRole("button", "Git action options");
  const openThread = async (kind: string) => {
    await screen.getByTestId("sidebar-row-card").filter({ hasText: `${kind} verification` }).tap();
    await options.waitFor({ state: "visible" });
    await screen.getByText("first fixture turn complete", { exact: true }).waitFor({ state: "visible" });
  };
  await openThread("Clean");
  await screen.getByText("Changes", { exact: true }).first().waitFor({ state: "visible" });
  await screen.getByRole("button", "Commit", { exact: true }).waitFor({ state: "visible" });
  const cleanDisabled = await options.isDisabled();
  const box = await options.boundingBox();
  if (!box) throw new Error("Git actions control has no rectangle");
  await browser.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
  await browser.mouse.down();
  await browser.mouse.up();
  await browser.mouse.move(700, 350);
  await capture("clean-branch-chevron-click");
  await browser.keyboard.press("Escape");

  for (const [kind, text, label] of [
    ["Publishing", "Publish repository...", "publishing-remains-reachable"],
    ["Detached", "Detached HEAD: create and check out a branch to enable push and pull request actions.", "detached-head-notice-remains-reachable"],
    ["Behind", "Behind upstream. Pull/rebase first.", "behind-upstream-notice-remains-reachable"],
  ]) {
    await openThread(kind);
    await expect(options).toBeEnabled();
    await options.tap();
    await screen.getByText(text!, { exact: kind !== "Error" }).waitFor({ state: "visible" });
    await browser.mouse.move(700, 350);
    await capture(label!);
    await browser.keyboard.press("Escape");
  }
  await openThread("Clean");
  await screen.getByRole("button", "Commit", { exact: true }).waitFor({ state: "visible" });
  await screen.getByRole("button", "Toggle terminal drawer").tap();
  const terminal = screen.getByRole("textbox", "Terminal input");
  await terminal.waitFor({ state: "visible" });
  await terminal.focus();
  await browser.mouse.move(700, 350);
  const command = async (text: string, marker: string) => {
    const completed = new Promise<void>(resolve => {
      const watcher = watch(`${repositories.Clean}/.git`, () => {
        if (existsSync(`${repositories.Clean}/.git/${marker}`)) {
          watcher.close();
          resolve();
        }
      });
    });
    await browser.keyboard.type(`${text}; touch .git/${marker}`);
    await browser.keyboard.press("Enter");
    await completed;
  };
  await command("printf '\\nKeyboard edit\\n' >> README.md", "keyboard-done");
  await capture("terminal-edit-before-keyboard-refresh");
  await screen.getByRole("region", "Version Control").getByRole("combobox").focus();
  await browser.keyboard.press("Tab");
  await browser.keyboard.press("Tab");
  await browser.mouse.move(700, 350);
  await capture("terminal-edit-after-keyboard-focus");
  await options.tap();
  await screen.getByRole("menuitem", "Commit", { exact: true }).waitFor({ state: "visible" });
  await browser.mouse.move(700, 350);
  await capture("dirty-actions-after-menu-refresh");
  await browser.keyboard.press("Escape");
  await terminal.focus();
  await command("git restore README.md", "restore-done");
  await options.tap();
  await browser.keyboard.press("Escape");
  await screen.getByRole("button", "Commit", { exact: true }).waitFor({ state: "visible" });
  await capture("terminal-restored-clean");
  await terminal.focus();
  await command("printf '\\nPointer edit\\n' >> README.md", "pointer-done");
  await screen.getByRole("group", "Git actions").hover();
  await browser.mouse.move(700, 350);
  await capture("terminal-edit-after-pointer-refresh");
  const pointerBox = await options.boundingBox();
  if (!pointerBox) throw new Error("Git actions control has no rectangle");
  await browser.mouse.move(pointerBox.x + pointerBox.width / 2, pointerBox.y + pointerBox.height / 2);
  await browser.mouse.down();
  await browser.mouse.up();
  await browser.mouse.move(700, 350);
  await capture("dirty-actions-after-pointer-click");
  await expect(options).toBeEnabled();
  await browser.keyboard.press("Escape");
  await expect(cleanDisabled).toBe(true);
});
