import { expect } from "e2e";
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { DatabaseSync } from "node:sqlite";
import { test, WEB } from "./t3.ts";
import { APP_DIR, T3_HOME } from "../e2e.config.ts";

test("Only the staging caller can send or launch with a pending upload", WEB, async ({ t3, app, screen }) => {
  const workspace = await t3.project("Upload ownership");
  const transcript: unknown[] = [];
  const runtime = JSON.parse(readFileSync(`${T3_HOME}/userdata/server-runtime.json`, "utf8"));
  const origin = runtime.origin;
  const client = async (label: string) => {
    const token = execFileSync(process.execPath, ["apps/server/src/bin.ts", "auth", "session", "issue", "--base-dir", T3_HOME, "--subject", "mcp-client", "--label", label, "--token-only"], { cwd: APP_DIR, encoding: "utf8" }).trim();
    let session: string | null = null;
    let n = 0;
    const request = async (method: string, params: unknown, notify = false) => {
      const response = await fetch(`${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", ...(notify ? {} : { id: ++n }), 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}`);
      if (notify) return;
      const values = text.startsWith("{") ? [JSON.parse(text)] : text.split("\n").filter(line => line.startsWith("data:")).map(line => JSON.parse(line.slice(5)));
      const result = values.find(value => value.id === n);
      if (!result || result.error) throw new Error(JSON.stringify(result ?? text));
      return result.result;
    };
    await request("initialize", { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: label, version: "1.0" } });
    await request("notifications/initialized", {}, true);
    return async (name: string, args: unknown) => {
      const result = await request("tools/call", { name, arguments: args });
      transcript.push({ caller: label, tool: name, result });
      return result;
    };
  };
  const A = await client("Client A");
  const B = await client("Client B");
  const value = (result: any) => {
    if (result.isError) throw new Error(JSON.stringify(result));
    return result.structuredContent;
  };
  const project = value(await A("t3_project_list", {})).projects.find((p: any) => p.workspaceRoot === workspace);
  if (!project) throw new Error("Seeded project missing");
  const launch = (title: string, extra = {}) => ({ projectId: project.id, title, modelSelection: { instanceId: "devReplay", model: "multi-turn" }, ...extra });
  const bThread = value(await B("t3_thread_launch", launch("B thread")));
  const stage = async (name: string) => {
    const bytes = `Synthetic private text owned by Client A: ${name}\n`;
    const metadata = { type: "file", name, mimeType: "text/plain", sizeBytes: Buffer.byteLength(bytes) };
    const upload = value(await A("t3_attachment_prepare_upload", { upload: metadata }));
    const response = await fetch(`${origin}${upload.relativeUrl}`, { method: "POST", body: bytes });
    transcript.push({ caller: "Client A", action: `POST ${name}`, status: response.status });
    if (response.status !== 204) throw new Error(`Upload failed: ${response.status} ${await response.text()}`);
    return { ...metadata, id: upload.attachmentId };
  };
  const first = await stage("a-secret-1.txt");
  const second = await stage("a-secret-2.txt");
  const sent = await B("t3_thread_send_attachments", { threadId: bThread.threadId, message: "Client B tries to send Client A's pending upload", attachments: [first] });
  const launched = await B("t3_thread_launch", launch("B launch with A's upload", { message: "Client B tries to launch with Client A's pending upload", attachments: [second] }));
  const third = await stage("a-discard-control.txt");
  const discarded = await B("t3_attachment_discard", { attachmentId: third.id });
  if (!discarded.isError || !JSON.stringify(discarded).includes("invalid_request")) throw new Error("Cross-caller discard control unexpectedly succeeded");
  const ownerThread = value(await A("t3_thread_launch", launch("A owner control")));
  const ownSend = await stage("a-owner-send.txt");
  value(await A("t3_thread_send_attachments", { threadId: ownerThread.threadId, message: "Client A sends its own upload", attachments: [ownSend] }));
  const ownLaunch = await stage("a-owner-launch.txt");
  const ownerLaunchThread = value(await A("t3_thread_launch", launch("A owner launch control", { message: "Client A launches with its own upload", attachments: [ownLaunch] })));
  const db = new DatabaseSync(`${T3_HOME}/userdata/statev2.sqlite`, { readOnly: true });
  const messages = db.prepare("SELECT thread_id, payload_json FROM orchestration_v2_projection_messages WHERE role = 'user'").all().map((row: any) => ({ threadId: row.thread_id, ...JSON.parse(row.payload_json) }));
  const canonical = messages.find((row: any) => row.threadId === ownerThread.threadId).attachments[0];
  value(await B("t3_thread_send_attachments", { threadId: ownerThread.threadId, message: "Client B reuses an attachment already in this target thread", attachments: [canonical] }));
  transcript.push({ action: "Projection readback before reuse", messages: messages.map((row: any) => ({ threadId: row.threadId, text: row.text, attachments: row.attachments })) });
  db.close();
  const dir = `${process.env.T3_PROOF_OUTPUT_DIR}/run-${Date.now()}`;
  mkdirSync(dir, { recursive: true });
  // Upload URLs and project paths are credentials/private context, so retain only relevant outcomes.
  const sanitized = transcript.filter((row: any) => row.tool !== "t3_attachment_prepare_upload" && row.tool !== "t3_project_list");
  writeFileSync(`${dir}/mcp-results.json`, JSON.stringify(sanitized, null, 2));
  console.log(`MCP outcomes: ${JSON.stringify({ send: sent, launch: launched, discard: discarded })}`);
  await t3.open(bThread);
  await screen.getByRole("textbox").first().waitFor({ state: "visible" });
  // The sync indicator is delayed; let it mount before checking it has gone.
  await new Promise(resolve => setTimeout(resolve, 500));
  await screen.getByText("Syncing messages...", { exact: false }).waitFor({ state: "hidden", timeout: 30_000 });
  if (!sent.isError) await screen.getByText("Client B tries to send Client A\'s pending upload", { exact: false }).first().tap();
  await app.screenshot("B-thread-upload-ownership");
  if (!launched.isError) {
    await t3.open(value(launched));
    await screen.getByRole("textbox").first().waitFor({ state: "visible" });
  // The sync indicator is delayed; let it mount before checking it has gone.
  await new Promise(resolve => setTimeout(resolve, 500));
  await screen.getByText("Syncing messages...", { exact: false }).waitFor({ state: "hidden", timeout: 30_000 });
    await screen.getByText("Client B tries to launch with Client A\'s pending upload", { exact: false }).first().tap();
    await app.screenshot("B-launch-upload-ownership");
  } else {
    await app.screenshot("B-launch-upload-ownership");
  }
  await t3.open(ownerThread);
  await screen.getByRole("textbox").first().waitFor({ state: "visible" });
  // The sync indicator is delayed; let it mount before checking it has gone.
  await new Promise(resolve => setTimeout(resolve, 500));
  await screen.getByText("Syncing messages...", { exact: false }).waitFor({ state: "hidden", timeout: 30_000 });
  await screen.getByText("Client A sends its own upload", { exact: false }).first().tap();
  await app.screenshot("owner-send-control");
  await screen.getByText("Client B reuses an attachment already in this target thread", { exact: false }).first().tap();
  await app.screenshot("target-thread-attachment-reuse-control");
  await t3.open(ownerLaunchThread);
  await screen.getByRole("textbox").first().waitFor({ state: "visible" });
  // The sync indicator is delayed; let it mount before checking it has gone.
  await new Promise(resolve => setTimeout(resolve, 500));
  await screen.getByText("Syncing messages...", { exact: false }).waitFor({ state: "hidden", timeout: 30_000 });
  await screen.getByText("Client A launches with its own upload", { exact: false }).first().tap();
  await app.screenshot("owner-launch-control");
  const code = (result: any) => result.isError && JSON.stringify(result).includes("invalid_request") ? "invalid_request" : "accepted";
  await expect([code(sent), code(launched)]).toEqual(["invalid_request", "invalid_request"]);
});
