import { expect } from "e2e";
import { execFileSync } from "node:child_process";
import { mkdir, readFile, symlink, utimes, access } from "node:fs/promises";
import { join } from "node:path";
import { test, WEB, runPromise } from "./t3.ts";
import * as Effect from "effect/Effect";
import * as NetService from "@t3tools/shared/Net";
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as References from "effect/References";
import * as EnvironmentAuth from "../../apps/server/src/auth/EnvironmentAuth.ts";
import * as ServerConfig from "../../apps/server/src/config.ts";
import { resolveCliAuthConfig } from "../../apps/server/src/cli/config.ts";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";

test("Cleanup preserves a checkout shared through a symlinked parent", WEB, async ({ t3, app, screen, browser }) => {
  const cli = (...args: string[]) => execFileSync(process.execPath, ["apps/server/src/bin.ts", ...args, "--base-dir", T3_HOME], { cwd: APP_DIR, encoding: "utf8", env: { ...process.env, T3CODE_DEV_SEED: "0" } }).trim();
  const project = await t3.project("cleanup-demo");
  const runtimePath = join(T3_HOME, "userdata/server-runtime.json");
  const runtime = JSON.parse(await readFile(runtimePath, "utf8"));
  const token = await runPromise(Effect.gen(function* () {
    const config = yield* resolveCliAuthConfig({ baseDir: Option.some(T3_HOME) }, Option.none(), { quietLogs: true });
    return yield* Effect.gen(function* () {
      const auth = yield* EnvironmentAuth.EnvironmentAuth;
      const issued = yield* auth.issueMcpClientSession({ label: "cleanup-scenario", access: "full-access", peerLink: false, client: { deviceType: "bot" } });
      return issued.token;
    }).pipe(Effect.provide(EnvironmentAuth.layerRuntime.pipe(Layer.provide(ServerConfig.layer(config)), Layer.provide(Layer.succeed(References.MinimumLogLevel, "Error")))));
  }).pipe(Effect.scoped, Effect.provide([NetService.layer, NodeCrypto.layer])));
  let session = "";
  let id = 0;
  const rpc = async (method: string, params: unknown) => {
    const response = await fetch(`${runtime.origin}/mcp`, { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json, text/event-stream", ...(session ? { "mcp-session-id": session } : {}) }, body: JSON.stringify({ jsonrpc: "2.0", id: ++id, method, params }) });
    session = response.headers.get("mcp-session-id") ?? session;
    const text = await response.text();
    if (!response.ok) throw new Error(`MCP HTTP ${response.status}: ${text}`);
    const data = text.startsWith("data:") || text.includes("\ndata:") ? JSON.parse(text.split("\n").find(line => line.startsWith("data:"))!.slice(5)) : JSON.parse(text);
    if (data.error) throw new Error(JSON.stringify(data.error));
    return data.result;
  };
  await rpc("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "cleanup-scenario", version: "1" } });
  const tool = async (name: string, args: unknown) => {
    const result = await rpc("tools/call", { name, arguments: args });
    if (result.isError) throw new Error(JSON.stringify(result));
    return result.structuredContent;
  };
  const projects = await tool("t3_project_list", {});
  const projectId = projects.projects.find((p: { workspaceRoot: string }) => p.workspaceRoot === project).id;
  const parent = join(T3_HOME, "worktrees/cleanup-demo");
  await mkdir(parent, { recursive: true });
  const shared = join(parent, "shared-checkout");
  const control = join(parent, "single-thread-control");
  const git = (...args: string[]) => execFileSync("git", args, { cwd: project, encoding: "utf8" });
  git("worktree", "add", "-b", "shared-checkout", shared, "main");
  git("worktree", "add", "-b", "single-thread-control", control, "main");
  const alias = join(T3_HOME, "linked-parent");
  await symlink(parent, alias);
  for (const [title, path] of [["Physical path thread", shared], ["Symlink path thread", join(alias, "shared-checkout")], ["Removal control", control]]) {
    const launched = await tool("t3_thread_launch", { projectId, title, message: "Complete this verification turn.", workspaceStrategy: { type: "existing_worktree", worktreePath: path }, modelSelection: { instanceId: "devReplay", model: "multi-turn" } });
    let waited;
    do { waited = await tool("t3_thread_wait", { threadId: launched.threadId, runId: launched.runId, timeoutMs: 15_000 }); } while (waited.timedOut);
    if (waited.status !== "completed") throw new Error(`Replay turn ended ${waited.status}`);
  }
  await t3.open();
  await screen.getByText("Physical path thread", { exact: true }).first().waitFor({ state: "visible" });
  await app.open("/settings/worktrees");
  await screen.getByRole("button", "Advanced", { exact: true }).last().tap();
  await screen.getByRole("switch", "Delete unchanged worktrees").tap();
  // The dev server's file watcher performs a real restart without changing source contents.
  const watched = join(APP_DIR, "apps/server/src/storageCleanup.ts");
  await utimes(watched, new Date(), new Date());
  const deadline = Date.now() + 90_000;
  while (true) {
    try {
      const current = JSON.parse(await readFile(runtimePath, "utf8"));
      if (current.pid !== runtime.pid && (await fetch(`${current.origin}/.well-known/t3/environment`)).ok) break;
    } catch {}
    if (Date.now() > deadline) throw new Error("The dev server did not restart");
    await new Promise(resolve => setTimeout(resolve, 250));
  }
  await app.open("/settings/maintenance");
  // Redact synthetic absolute paths in the evidence, leaving outcomes and thread labels intact.
  await browser.evaluate(() => {
    const style = document.createElement("style");
    style.textContent = "code { visibility: hidden !important; }";
    document.head.append(style);
  });
  await screen.getByRole("button", "Last run", { exact: false }).tap();
  await browser.evaluate(() => document.querySelector("#storage-cleanup-last-run")?.scrollIntoView({ block: "start" }));
  const exists = async (path: string) => access(path).then(() => true, () => false);
  const sharedExists = await exists(shared);
  const controlExists = await exists(control);
  const bindings = JSON.parse(cli("thread", "list", "--project", project, "--json"));
  console.log(JSON.stringify({ sharedCheckoutExists: sharedExists, controlCheckoutExists: controlExists, retainedSharedBindings: bindings.map((entry: { threadId: string }) => JSON.parse(cli("thread", "show", entry.threadId, "--messages", "0", "--json")).thread).filter((thread: { worktreePath: string }) => thread.worktreePath?.endsWith("/shared-checkout")).length }));
  await browser.evaluate(() => document.querySelector("#storage-cleanup-last-run")?.scrollIntoView({ block: "start" }));
  let previous = await app.screenshot("settling-cleanup-report");
  for (let attempt = 0; attempt < 10; attempt++) {
    await new Promise(resolve => setTimeout(resolve, 500));
    const current = await app.screenshot("cleanup-after-restart");
    if ((await readFile(previous)).equals(await readFile(current))) break;
    previous = current;
    if (attempt === 9) throw new Error("Cleanup report did not become visually stable");
  }
  if (controlExists) throw new Error("Removal control was not removed");
  await expect(screen.getByText("Shared by 2 threads", { exact: true })).toBeVisible();
  expect(sharedExists).toBe(true);
});
