import { createHash } from "node:crypto";
import type { AddressInfo } from "node:net";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { afterEach, describe, expect, it } from "vitest";
import { createApp } from "../src/app.js";
import { makeTestConfig } from "./helpers.js";

const cleanupTasks: Array<() => Promise<void>> = [];

afterEach(async () => {
  while (cleanupTasks.length) await cleanupTasks.pop()!();
});

describe("OAuth-protected MCP endpoint", () => {
  it("registers a ChatGPT client, issues tokens and exposes authenticated tools", async () => {
    const { config, cleanup } = await makeTestConfig();
    cleanupTasks.push(cleanup);
    const { app } = createApp(config);
    const httpServer = app.listen(0, "127.0.0.1");
    await new Promise<void>((resolve) => httpServer.once("listening", resolve));
    cleanupTasks.push(
      () =>
        new Promise<void>((resolve, reject) =>
          httpServer.close((error) => (error ? reject(error) : resolve())),
        ),
    );
    const port = (httpServer.address() as AddressInfo).port;
    const origin = `http://127.0.0.1:${port}`;

    const unauthenticated = await fetch(`${origin}/mcp`, {
      method: "POST",
      headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
    });
    expect(unauthenticated.status).toBe(401);
    expect(unauthenticated.headers.get("www-authenticate")).toContain(
      "/.well-known/oauth-protected-resource/mcp",
    );

    const registration = await fetch(`${origin}/register`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        redirect_uris: ["https://chatgpt.com/oauth/callback"],
        token_endpoint_auth_method: "client_secret_post",
        grant_types: ["authorization_code", "refresh_token"],
        response_types: ["code"],
        client_name: "Test ChatGPT",
      }),
    });
    expect(registration.status).toBe(201);
    const client = (await registration.json()) as { client_id: string; client_secret: string };

    const verifier = "test-verifier-with-more-than-forty-three-characters-123456789";
    const challenge = createHash("sha256").update(verifier).digest("base64url");
    const authorizeUrl = new URL(`${origin}/authorize`);
    authorizeUrl.searchParams.set("response_type", "code");
    authorizeUrl.searchParams.set("client_id", client.client_id);
    authorizeUrl.searchParams.set("redirect_uri", "https://chatgpt.com/oauth/callback");
    authorizeUrl.searchParams.set("code_challenge", challenge);
    authorizeUrl.searchParams.set("code_challenge_method", "S256");
    authorizeUrl.searchParams.set("scope", "mail.read mail.draft");
    authorizeUrl.searchParams.set("resource", config.mcpUrl.href);
    authorizeUrl.searchParams.set("state", "state-123");

    const authorization = await fetch(authorizeUrl);
    expect(authorization.status).toBe(200);
    const page = await authorization.text();
    const transaction = page.match(/name="transaction" value="([^"]+)"/)?.[1];
    expect(transaction).toBeTruthy();

    const approval = await fetch(`${origin}/oauth/approve`, {
      method: "POST",
      redirect: "manual",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        transaction: transaction!,
        password: "correct horse battery staple",
        decision: "approve",
      }),
    });
    expect(approval.status).toBe(302);
    const callback = new URL(approval.headers.get("location")!);
    expect(callback.searchParams.get("state")).toBe("state-123");
    const code = callback.searchParams.get("code");
    expect(code).toBeTruthy();

    const tokenResponse = await fetch(`${origin}/token`, {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        client_id: client.client_id,
        client_secret: client.client_secret,
        code: code!,
        code_verifier: verifier,
        redirect_uri: "https://chatgpt.com/oauth/callback",
        resource: config.mcpUrl.href,
      }),
    });
    expect(tokenResponse.status).toBe(200);
    const tokens = (await tokenResponse.json()) as { access_token: string; refresh_token: string };

    const replayedCode = await fetch(`${origin}/token`, {
      method: "POST",
      headers: { "content-type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "authorization_code",
        client_id: client.client_id,
        client_secret: client.client_secret,
        code: code!,
        code_verifier: verifier,
        redirect_uri: "https://chatgpt.com/oauth/callback",
        resource: config.mcpUrl.href,
      }),
    });
    expect(replayedCode.status).toBe(400);

    const transport = new StreamableHTTPClientTransport(new URL(`${origin}/mcp`), {
      requestInit: { headers: { Authorization: `Bearer ${tokens.access_token}` } },
    });
    const mcpClient = new Client({ name: "integration-test", version: "1.0.0" });
    await mcpClient.connect(transport);
    const tools = await mcpClient.listTools();
    expect(tools.tools.map((tool) => tool.name)).toEqual(
      expect.arrayContaining(["get_profile", "list_mailboxes", "search_emails", "create_draft"]),
    );
    expect(tools.tools.map((tool) => tool.name)).not.toContain("send_prepared_email");
    const profile = await mcpClient.callTool({ name: "get_profile", arguments: {} });
    expect(profile.structuredContent).toMatchObject({ id: "prf_test", name: "Test User" });
    const mailboxes = await mcpClient.callTool({ name: "list_mailboxes", arguments: {} });
    expect(mailboxes.structuredContent).toMatchObject({
      mailboxes: [{ id: "test-mailbox", can_send: false }],
    });
    await mcpClient.close();
  });
});
