import { expect } from "e2e";
import { createServer } from "node:http";
import { writeFile } from "node:fs/promises";
import { test, WEB, runPromise } from "./t3.ts";
import { run } from "./shell.ts";

// Replay the generated PR text; Azure, git, the app server, and client are real.
test("Azure pull requests preserve literal @ text on creation and update", WEB, async ({ t3, app, screen, browser }) => {
  const exec = async (program: string, args: string[], cwd?: string) =>
    (await runPromise(run(program, args, { cwd }))).stdout.trim();
  const org = process.env.AZURE_DEVOPS_TEST_ORG!;
  const azureProject = process.env.AZURE_DEVOPS_TEST_PROJECT!;
  const az = (args: string[]) => exec("az", [...args, "--organization", org, "--project", azureProject, "--only-show-errors", "-o", "json"]);
  const repos = JSON.parse(await az(["repos", "list"]));
  const repo = repos.find((r: any) => r.name === "t3-factory-test");
  const remote = repo.remoteUrl;
  const repository = new URL(remote).pathname.slice(1);
  const project = await t3.project("Azure literal text", { "notes.txt": "from the file", "other.txt": "from the other file" });
  const git = (args: string[]) => exec("git", args, project);
  const push = (args: string[]) => git(["-c", 'credential.helper=!f() { echo username=pat; echo "password=$AZURE_DEVOPS_EXT_PAT"; }; f', "push", "origin", ...args]);
  const nonce = `proof-37098-${Date.now()}`;
  const base = `${nonce}-base`;
  const branches: string[] = [];
  const prs: number[] = [];
  let generated = { title: "Original title", body: "Original description" };
  const generator = createServer(async (_req, res) => {
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify({ choices: [{ finish_reason: "stop", message: { content: JSON.stringify(generated) } }] }));
  });
  await new Promise<void>(resolve => generator.listen(0, "127.0.0.1", resolve));
  try {
    await git(["remote", "add", "origin", remote]);
    await git(["config", "credential.helper", '!f() { echo username=pat; echo "password=$AZURE_DEVOPS_EXT_PAT"; }; f']);
    await git(["branch", "-m", base]);
    await push(["-u", base]); branches.push(base);
    const thread = await t3.thread({ project, title: "Azure literal text proof", messages: ["start"], model: "multi-turn" });
    await t3.open(thread);
    await screen.getByText("first fixture turn complete", { exact: false }).waitFor({ state: "visible", timeout: 60000 });
    const environmentId = await browser.evaluate(async () => (await (await fetch("/.well-known/t3/environment")).json()).environmentId);
    const projectId = await browser.evaluate(async (environmentId) => {
      const { appAtomRegistry } = await import("/src/rpc/atomRegistry.ts");
      const { environmentSnapshotAtom } = await import("/src/state/shell.ts");
      const snapshot = appAtomRegistry.get(environmentSnapshotAtom(environmentId));
      return snapshot.projects.find((p: any) => p.title === "Azure literal text").id;
    }, environmentId);
    const address = generator.address() as { port: number };
    await browser.evaluate(async ({ environmentId, port }) => {
      const { appAtomRegistry } = await import("/src/rpc/atomRegistry.ts");
      const { serverEnvironment } = await import("/src/state/server.ts");
      const result = await serverEnvironment.updateSettings.run(appAtomRegistry, { environmentId, input: { patch: {
        textGenerators: { "literal-replay": { kind: "openaiCompatible", label: "Literal replay", baseUrl: `http://127.0.0.1:${port}`, apiKey: "", enabled: true } },
        textGenerationModelSelection: { instanceId: "literal-replay", model: "literal" },
        sourceControlWriterModelSelection: { instanceId: "literal-replay", model: "literal" },
      } } });
      if (result._tag !== "Success") throw new Error(`Settings: ${JSON.stringify(result)}`);
    }, { environmentId, port: address.port });
    const results: any[] = [];
    const read = async (id: number) => JSON.parse(await exec("az", ["repos", "pr", "show", "--id", String(id), "--organization", org, "--only-show-errors", "-o", "json"]));
    const openPr = async (url: string) => {
      const actual = await read(Number(url.split("/").at(-1)));
      await t3.open(`/pull-requests?selectedUrl=${encodeURIComponent(url)}&repository=${encodeURIComponent(repository)}&number=${url.split("/").at(-1)}&projectId=${projectId}&host=dev.azure.com&selectedProjectId=${projectId}&selectedEnvironmentId=${environmentId}`);
      await screen.getByRole("button", "Edit title").waitFor({ state: "visible", timeout: 60000 });
      await screen.getByRole("heading", actual.title, { exact: true }).waitFor({ state: "visible", timeout: 60000 });
      await browser.locator("[data-pr-description]").filter({ hasText: actual.description }).waitFor({ state: "visible", timeout: 60000 });
    };
    const create = async (title: string, body: string, index: number) => {
      generated = { title, body };
      const branch = `${nonce}-case-${index}`;
      await git(["checkout", "-b", branch, base]);
      await writeFile(`${project}/change.txt`, `Change ${index}\n`);
      await git(["add", "change.txt"]);
      await git(["-c", "user.name=Scenario", "-c", "user.email=scenario@example.invalid", "commit", "-m", `Change ${index}`]);
      await git(["config", `branch.${branch}.t3-merge-base-ref`, `refs/heads/${base}`]);
      await push(["-u", branch]); branches.push(branch);
      const created = await browser.evaluate(async ({ environmentId, project, projectId, index }) => {
        const { appAtomRegistry } = await import("/src/rpc/atomRegistry.ts");
        const { vcsActionManager } = await import("/src/state/vcs.ts");
        const result = await vcsActionManager.runStackedAction({ environmentId, cwd: project }).run(appAtomRegistry, { actionId: `literal-${index}`, action: "create_pr", projectId });
        if (result._tag !== "Success") throw new Error(`Create: ${JSON.stringify(result)}`);
        return result.value.pr;
      }, { environmentId, project, projectId, index });
      const id = created.number; prs.push(id);
      const actual = await read(id);
      const url = `${remote}/pullrequest/${id}`;
      await openPr(url);
      await app.screenshot(`create-${index}`);
      results.push({ operation: `create-${index}`, title: actual.title, body: actual.description });
      return { id, url };
    };
    const first = await create("Original title", "Original description", 0);
    for (const [index, title] of ["@notes.txt", "a=@notes.txt"].entries()) {
      await screen.getByRole("button", "Edit title").tap();
      await screen.getByRole("textbox", "Pull request title").fill(title);
      await screen.getByRole("textbox", "Pull request title").press("Enter");
      // Reopen only after the real Azure write has answered, avoiding optimistic client text.
      await screen.getByRole("button", "Edit title").waitFor({ state: "visible" });
      await screen.getByRole("button", "Edit title").tap();
      await browser.locator('input[aria-label="Pull request title"]:enabled').waitFor({ state: "visible", timeout: 60000 });
      await screen.getByRole("textbox", "Pull request title").press("Escape");
      const actual = await read(first.id);
      await openPr(first.url);
      await app.screenshot(`edit-title-${index}`);
      results.push({ operation: `edit-title-${index}`, title: actual.title });
    }
    await screen.getByRole("button", "Edit description").tap();
    await screen.getByRole("textbox", "Pull request description").fill("@notes.txt");
    await screen.getByRole("button", "Save", { exact: true }).tap();
    await screen.getByRole("button", "Edit description").waitFor({ state: "visible", timeout: 60000 });
    const description = await read(first.id);
    await openPr(first.url);
    await app.screenshot("edit-description");
    results.push({ operation: "edit-description", body: description.description });
    await browser.evaluate(async ({ environmentId, projectId, repository, number }) => {
      const { appAtomRegistry } = await import("/src/rpc/atomRegistry.ts");
      const { pullRequestEnvironment } = await import("/src/state/pullRequests.ts");
      const result = await pullRequestEnvironment.update.run(appAtomRegistry, { environmentId, input: { projectId, repository, number, host: "dev.azure.com", title: "@notes.txt", body: "@other.txt" } });
      if (result._tag !== "Success") throw new Error(`Combined update: ${JSON.stringify(result)}`);
    }, { environmentId, projectId, repository, number: first.id });
    const combined = await read(first.id);
    await openPr(first.url);
    await app.screenshot("combined-title-description");
    results.push({ operation: "combined", title: combined.title, body: combined.description });
    await create("@notes.txt", "@notes.txt", 1);
    await create("a=@notes.txt", "a=@notes.txt", 2);
    expect(results).toEqual([
      { operation: "create-0", title: "Original title", body: "Original description" },
      { operation: "edit-title-0", title: "@notes.txt" },
      { operation: "edit-title-1", title: "a=@notes.txt" },
      { operation: "edit-description", body: "@notes.txt" },
      { operation: "combined", title: "@notes.txt", body: "@other.txt" },
      { operation: "create-1", title: "@notes.txt", body: "@notes.txt" },
      { operation: "create-2", title: "a=@notes.txt", body: "a=@notes.txt" },
    ]);
  } finally {
    for (const id of prs) await exec("az", ["repos", "pr", "update", "--id", String(id), "--status", "abandoned", "--organization", org, "--only-show-errors", "-o", "none"]);
    for (const branch of branches.reverse()) await push(["--delete", branch]);
    await new Promise<void>(resolve => generator.close(() => resolve()));
  }
});
