import { expect, it } from "@effect/vitest";
import { Effect, Option, Schema } from "effect";
import { buildToolTypeScriptPreview } from "@executor-js/sdk/core";

import {
  convertGoogleDiscoveryBundleToOpenApi,
  convertGoogleDiscoveryToOpenApi,
  isGoogleDiscoveryUrl,
  normalizeGoogleDiscoveryUrl,
} from "./discovery";
import { extract, parse } from "@executor-js/plugin-openapi";

const ConvertedOperation = Schema.Struct({
  operationId: Schema.String,
  "x-executor-toolPath": Schema.String,
  parameters: Schema.Array(
    Schema.Struct({
      name: Schema.String,
      in: Schema.String,
      required: Schema.Boolean,
      description: Schema.optional(Schema.String),
      schema: Schema.Unknown,
      style: Schema.optional(Schema.String),
      explode: Schema.optional(Schema.Boolean),
      allowReserved: Schema.optional(Schema.Boolean),
    }),
  ),
  servers: Schema.optional(Schema.Array(Schema.Struct({ url: Schema.String }))),
  security: Schema.optional(
    Schema.Array(Schema.Record(Schema.String, Schema.Array(Schema.String))),
  ),
  "x-executor-pathTemplate": Schema.optional(Schema.String),
  requestBody: Schema.optional(Schema.Unknown),
  responses: Schema.Unknown,
  "x-google-scopes": Schema.Array(Schema.String),
});

const ConvertedSpec = Schema.Struct({
  openapi: Schema.String,
  servers: Schema.Array(Schema.Struct({ url: Schema.String })),
  paths: Schema.Record(Schema.String, Schema.Record(Schema.String, ConvertedOperation)),
  components: Schema.Struct({
    schemas: Schema.Record(Schema.String, Schema.Unknown),
  }),
});

const decodeConvertedSpec = Schema.decodeUnknownSync(Schema.fromJsonString(ConvertedSpec));

const OAUTH2_URL = "https://www.googleapis.com/discovery/v1/apis/oauth2/v2/rest";
const OAUTH2_USERINFO_SCOPES = [
  "openid",
  "https://www.googleapis.com/auth/userinfo.email",
  "https://www.googleapis.com/auth/userinfo.profile",
] as const;

const oauth2DiscoveryDoc = {
  name: "oauth2",
  version: "v2",
  title: "Google OAuth2 API",
  rootUrl: "https://www.googleapis.com/",
  servicePath: "",
  auth: {
    oauth2: {
      scopes: {
        openid: { description: "Associate you with your personal info on Google" },
        "https://www.googleapis.com/auth/userinfo.email": {
          description: "See your primary Google Account email address",
        },
        "https://www.googleapis.com/auth/userinfo.profile": {
          description: "See your personal info",
        },
      },
    },
  },
  methods: {
    tokeninfo: {
      id: "oauth2.tokeninfo",
      httpMethod: "POST",
      path: "oauth2/v2/tokeninfo",
      parameters: {
        access_token: {
          location: "query",
          type: "string",
        },
      },
      response: { $ref: "Tokeninfo" },
    },
  },
  resources: {
    userinfo: {
      methods: {
        get: {
          id: "oauth2.userinfo.get",
          httpMethod: "GET",
          path: "oauth2/v2/userinfo",
          scopes: OAUTH2_USERINFO_SCOPES,
          response: { $ref: "Userinfo" },
        },
      },
      resources: {
        v2: {
          resources: {
            me: {
              methods: {
                get: {
                  id: "oauth2.userinfo.v2.me.get",
                  httpMethod: "GET",
                  path: "userinfo/v2/me",
                  scopes: OAUTH2_USERINFO_SCOPES,
                  response: { $ref: "Userinfo" },
                },
              },
            },
          },
        },
      },
    },
  },
  schemas: {
    Tokeninfo: {
      id: "Tokeninfo",
      type: "object",
      properties: {
        audience: { type: "string" },
        scope: { type: "string" },
      },
    },
    Userinfo: {
      id: "Userinfo",
      type: "object",
      properties: {
        email: { type: "string" },
        family_name: { type: "string" },
        gender: { type: "string" },
        given_name: { type: "string" },
        hd: { type: "string" },
        id: { type: "string" },
        link: { type: "string" },
        locale: { type: "string" },
        name: { type: "string" },
        picture: { type: "string" },
        verified_email: { type: "boolean" },
      },
    },
  },
};

it("accepts only supported HTTPS Google Discovery endpoints", () => {
  expect(
    normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest/"),
  ).toBe("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest");
  expect(
    normalizeGoogleDiscoveryUrl(
      "https://www.googleapis.com/discovery/v1/apis/photospicker/v1/rest",
    ),
  ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1");
  expect(
    normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/forms/v1/rest"),
  ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1");
  expect(
    normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/keep/v1/rest"),
  ).toBe("https://keep.googleapis.com/$discovery/rest?version=v1");
  expect(
    normalizeGoogleDiscoveryUrl("https://chat.googleapis.com/$discovery/rest?version=v1"),
  ).toBe("https://www.googleapis.com/discovery/v1/apis/chat/v1/rest");
  expect(
    normalizeGoogleDiscoveryUrl("https://photospicker.googleapis.com/$discovery/rest?version=v1"),
  ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1");
  expect(
    normalizeGoogleDiscoveryUrl("https://forms.googleapis.com/$discovery/rest?version=v1"),
  ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1");
  expect(
    normalizeGoogleDiscoveryUrl("https://keep.googleapis.com/$discovery/rest?version=v1"),
  ).toBe("https://keep.googleapis.com/$discovery/rest?version=v1");

  expect(isGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe(
    true,
  );
  expect(isGoogleDiscoveryUrl("https://evilgoogleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe(
    false,
  );
  expect(isGoogleDiscoveryUrl("http://www.googleapis.com/discovery/v1/apis/gmail/v1/rest")).toBe(
    false,
  );
  expect(
    isGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest?next=x"),
  ).toBe(false);
  expect(
    isGoogleDiscoveryUrl("https://token@www.googleapis.com/discovery/v1/apis/gmail/v1/rest"),
  ).toBe(false);
});

const normalizeOpenApiRefsForPreview = (node: unknown): unknown => {
  if (node == null || typeof node !== "object") return node;
  if (Array.isArray(node)) return node.map(normalizeOpenApiRefsForPreview);
  const obj = node as Record<string, unknown>;
  if (typeof obj.$ref === "string") {
    const match = obj.$ref.match(/^#\/components\/schemas\/(.+)$/);
    return match ? { ...obj, $ref: `#/$defs/${match[1]}` } : obj;
  }
  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => [key, normalizeOpenApiRefsForPreview(value)]),
  );
};

it.effect("converts Google Discovery documents into Executor-preserving OpenAPI 3 specs", () =>
  Effect.gen(function* () {
    const result = yield* convertGoogleDiscoveryToOpenApi({
      discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest",
      // @effect-diagnostics-next-line preferSchemaOverJson:off
      documentText: JSON.stringify({
        name: "gmail",
        version: "v1",
        title: "Gmail API",
        rootUrl: "https://gmail.googleapis.com/",
        servicePath: "",
        auth: {
          oauth2: {
            scopes: {
              "https://www.googleapis.com/auth/gmail.metadata": {
                description: "Read metadata",
              },
            },
          },
        },
        resources: {
          users: {
            resources: {
              messages: {
                methods: {
                  list: {
                    id: "gmail.users.messages.list",
                    httpMethod: "GET",
                    path: "gmail/v1/users/{userId}/messages",
                    scopes: ["https://www.googleapis.com/auth/gmail.metadata"],
                    parameters: {
                      userId: {
                        location: "path",
                        required: true,
                        type: "string",
                        description: "The user's email address. The special value me can be used.",
                      },
                      metadataHeaders: {
                        location: "query",
                        repeated: true,
                        type: "string",
                      },
                    },
                  },
                },
              },
              drafts: {
                methods: {
                  create: {
                    id: "gmail.users.drafts.create",
                    httpMethod: "POST",
                    path: "gmail/v1/users/{userId}/drafts",
                    request: { $ref: "Draft" },
                    response: { $ref: "Draft" },
                    scopes: ["https://www.googleapis.com/auth/gmail.metadata"],
                    parameters: {
                      userId: {
                        location: "path",
                        required: true,
                        type: "string",
                      },
                    },
                  },
                },
              },
            },
          },
        },
        schemas: {
          Draft: {
            id: "Draft",
            type: "object",
            description: "A draft email.",
            properties: {
              id: {
                type: "string",
                description: "The immutable ID of the draft.",
              },
              message: {
                $ref: "Message",
              },
            },
          },
          Message: {
            id: "Message",
            type: "object",
            properties: {
              id: {
                type: "string",
              },
              labelIds: {
                type: "array",
                items: { type: "string" },
              },
            },
          },
        },
      }),
    });

    const spec = decodeConvertedSpec(result.specText);
    const operation = spec.paths["/gmail/v1/users/{userId}/messages"]?.get;
    const createDraft = spec.paths["/gmail/v1/users/{userId}/drafts"]?.post;
    expect(spec.openapi).toBe("3.1.0");
    expect(spec.servers).toEqual([{ url: "https://gmail.googleapis.com/" }]);
    expect(result.specText).not.toContain("_tag");
    expect(operation).toMatchObject({
      operationId: "users.messages.list",
      "x-executor-toolPath": "users.messages.list",
      "x-google-scopes": ["https://www.googleapis.com/auth/gmail.metadata"],
    });
    expect(operation?.security).toEqual([
      { googleOAuth2: ["https://www.googleapis.com/auth/gmail.metadata"] },
    ]);
    expect(operation?.parameters).toContainEqual(
      expect.objectContaining({
        name: "metadataHeaders",
        in: "query",
        style: "form",
        explode: true,
      }),
    );
    expect(operation?.parameters).toContainEqual(
      expect.objectContaining({
        name: "userId",
        description: "The user's email address. The special value me can be used.",
        schema: expect.objectContaining({ type: "string" }),
      }),
    );
    expect(createDraft).toMatchObject({
      operationId: "users.drafts.create",
      "x-executor-toolPath": "users.drafts.create",
    });
    expect(createDraft).toMatchObject({
      requestBody: {
        content: {
          "application/json": {
            schema: { $ref: "#/components/schemas/Draft" },
          },
        },
      },
    });
    expect(createDraft?.parameters).toContainEqual(
      expect.objectContaining({
        name: "userId",
        schema: expect.objectContaining({ type: "string" }),
      }),
    );

    const parsed = yield* parse(result.specText);
    const extracted = yield* extract(parsed);
    const extractedDraftCreate = extracted.operations.find(
      (candidate) => candidate.operationId === "users.drafts.create",
    );
    expect(extractedDraftCreate?.operationId).toBe("users.drafts.create");
    const preview = yield* Effect.promise(() =>
      buildToolTypeScriptPreview({
        inputSchema: normalizeOpenApiRefsForPreview(
          extractedDraftCreate
            ? Option.getOrUndefined(extractedDraftCreate.inputSchema)
            : undefined,
        ),
        outputSchema: normalizeOpenApiRefsForPreview(
          extractedDraftCreate
            ? Option.getOrUndefined(extractedDraftCreate.outputSchema)
            : undefined,
        ),
        defs: new Map(
          Object.entries(spec.components.schemas).map(([name, schema]) => [
            name,
            normalizeOpenApiRefsForPreview(schema),
          ]),
        ),
      }),
    );
    expect(preview.inputTypeScript).toBe("{ userId: string; body?: Draft; }");
    expect(preview.outputTypeScript).toBe("Draft");
    expect(preview.typeScriptDefinitions).toMatchObject({
      Draft: "{ id?: string; message?: Message; }",
      Message: "{ id?: string; labelIds?: string[]; }",
    });
    // v2: the conversion now exposes a v2 oauth `authenticationTemplate` rather
    // than v1's `oauth2` source config.
    // removed: identityScopes assertion - that field belonged to the v1
    // OAuth2SourceConfig slot model, which no longer exists in v2.
    const oauthTemplate = result.authenticationTemplate?.find((entry) => entry.kind === "oauth2");
    expect(oauthTemplate).toBeDefined();
  }),
);

it.effect("converts Google OAuth2 v2 top-level and aliased userinfo methods", () =>
  Effect.gen(function* () {
    const result = yield* convertGoogleDiscoveryToOpenApi({
      discoveryUrl: OAUTH2_URL,
      // @effect-diagnostics-next-line preferSchemaOverJson:off
      documentText: JSON.stringify(oauth2DiscoveryDoc),
    });

    const spec = decodeConvertedSpec(result.specText);
    const userinfo = spec.paths["/oauth2/v2/userinfo"]?.get;
    const userinfoMe = spec.paths["/userinfo/v2/me"]?.get;
    const tokeninfo = spec.paths["/oauth2/v2/tokeninfo"]?.post;

    expect(userinfo).toMatchObject({
      operationId: "userinfo.get",
      "x-executor-toolPath": "userinfo.get",
      "x-google-scopes": [...OAUTH2_USERINFO_SCOPES],
    });
    expect(userinfo?.security).toEqual([{ googleOAuth2: [...OAUTH2_USERINFO_SCOPES] }]);
    expect(userinfo?.responses).toMatchObject({
      "200": {
        content: {
          "application/json": {
            schema: { $ref: "#/components/schemas/Userinfo" },
          },
        },
      },
    });
    expect(userinfoMe).toMatchObject({
      operationId: "userinfo.v2.me.get",
      "x-executor-toolPath": "userinfo.v2.me.get",
      "x-google-scopes": [...OAUTH2_USERINFO_SCOPES],
    });
    expect(tokeninfo).toMatchObject({
      operationId: "tokeninfo",
      "x-executor-toolPath": "tokeninfo",
    });
  }),
);

it.effect("marks Google Discovery media-download methods as binary responses", () =>
  Effect.gen(function* () {
    const result = yield* convertGoogleDiscoveryToOpenApi({
      discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
      // @effect-diagnostics-next-line preferSchemaOverJson:off
      documentText: JSON.stringify({
        name: "drive",
        version: "v3",
        title: "Drive API",
        rootUrl: "https://www.googleapis.com/",
        servicePath: "drive/v3/",
        resources: {
          files: {
            methods: {
              export: {
                id: "drive.files.export",
                httpMethod: "GET",
                path: "files/{fileId}/export",
                supportsMediaDownload: true,
                useMediaDownloadService: true,
                parameters: {
                  fileId: { location: "path", required: true, type: "string" },
                  mimeType: { location: "query", required: true, type: "string" },
                },
              },
            },
          },
        },
        schemas: {},
      }),
    });

    const spec = decodeConvertedSpec(result.specText);
    const operation = spec.paths["/files/{fileId}/export"]?.get;
    expect(operation?.responses).toMatchObject({
      "200": {
        content: {
          "application/octet-stream": {
            schema: { type: "string", format: "binary" },
          },
        },
      },
    });

    const parsed = yield* parse(result.specText);
    const extracted = yield* extract(parsed);
    const exportOperation = extracted.operations.find(
      (candidate) => candidate.operationId === "files.export",
    );
    expect(exportOperation?.operationId).toBe("files.export");
    const responseFileHint = Option.flatMap(
      exportOperation?.responseBody ?? Option.none(),
      (body) => body.fileHint,
    );
    expect(Option.isSome(responseFileHint)).toBe(true);
  }),
);

it.effect("supplies documented scopes when Picker Discovery omits auth metadata", () =>
  Effect.gen(function* () {
    const result = yield* convertGoogleDiscoveryToOpenApi({
      discoveryUrl: "https://photospicker.googleapis.com/$discovery/rest?version=v1",
      // @effect-diagnostics-next-line preferSchemaOverJson:off
      documentText: JSON.stringify({
        name: "photospicker",
        version: "v1",
        title: "Google Photos Picker API",
        rootUrl: "https://photospicker.googleapis.com/",
        servicePath: "v1/",
        resources: {
          mediaItems: {
            methods: {
              list: {
                id: "photospicker.mediaItems.list",
                httpMethod: "GET",
                path: "mediaItems",
                parameters: {},
              },
            },
          },
        },
        schemas: {},
      }),
    });

    const pickerScope = "https://www.googleapis.com/auth/photospicker.mediaitems.readonly";
    const oauthTemplate = result.authenticationTemplate?.find((entry) => entry.kind === "oauth2");
    expect(oauthTemplate?.kind === "oauth2" ? oauthTemplate.scopes : undefined).toEqual([
      pickerScope,
    ]);

    const spec = decodeConvertedSpec(result.specText);
    const operation = spec.paths["/mediaItems"]?.get;
    expect(operation).toMatchObject({
      operationId: "mediaItems.list",
      "x-google-scopes": [pickerScope],
      security: [{ googleOAuth2: [pickerScope] }],
    });
  }),
);

it.effect(
  "generates a separate media-upload operation for Google Discovery methods with supportsMediaUpload",
  () =>
    Effect.gen(function* () {
      const result = yield* convertGoogleDiscoveryToOpenApi({
        discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
        // @effect-diagnostics-next-line preferSchemaOverJson:off
        documentText: JSON.stringify({
          name: "drive",
          version: "v3",
          title: "Drive API",
          rootUrl: "https://www.googleapis.com/",
          servicePath: "drive/v3/",
          resources: {
            files: {
              methods: {
                create: {
                  id: "drive.files.create",
                  httpMethod: "POST",
                  path: "files",
                  request: { $ref: "File" },
                  response: { $ref: "File" },
                  supportsMediaUpload: true,
                  mediaUpload: {
                    accept: ["*/*"],
                    maxSize: "5497558138880",
                    protocols: {
                      simple: {
                        multipart: true,
                        path: "/upload/drive/v3/files",
                      },
                      resumable: {
                        multipart: true,
                        path: "/resumable/upload/drive/v3/files",
                      },
                    },
                  },
                  scopes: ["https://www.googleapis.com/auth/drive.file"],
                  parameters: {
                    enforceSingleParent: {
                      location: "query",
                      type: "boolean",
                    },
                  },
                },
                update: {
                  id: "drive.files.update",
                  httpMethod: "PATCH",
                  path: "files/{fileId}",
                  request: { $ref: "File" },
                  response: { $ref: "File" },
                  supportsMediaUpload: true,
                  mediaUpload: {
                    accept: ["*/*"],
                    protocols: {
                      simple: {
                        multipart: true,
                        path: "/upload/drive/v3/files/{fileId}",
                      },
                    },
                  },
                  scopes: ["https://www.googleapis.com/auth/drive.file"],
                  parameters: {
                    fileId: { location: "path", required: true, type: "string" },
                  },
                },
                export: {
                  id: "drive.files.export",
                  httpMethod: "GET",
                  path: "files/{fileId}/export",
                  supportsMediaDownload: true,
                  useMediaDownloadService: true,
                  parameters: {
                    fileId: { location: "path", required: true, type: "string" },
                    mimeType: { location: "query", required: true, type: "string" },
                  },
                },
              },
            },
          },
          schemas: {
            File: {
              id: "File",
              type: "object",
              properties: {
                name: { type: "string" },
              },
            },
          },
        }),
      });

      const spec = decodeConvertedSpec(result.specText);
      // The metadata-only operation should keep the original JSON behavior.
      const createMetadata = spec.paths["/files"]?.post;
      expect(createMetadata).toMatchObject({
        operationId: "files.create",
        "x-executor-toolPath": "files.create",
        requestBody: {
          content: {
            "application/json": {
              schema: { $ref: "#/components/schemas/File" },
            },
          },
        },
      });

      // A separate media-upload operation should be emitted with the simple upload path.
      const createMedia = spec.paths["/upload/drive/v3/files"]?.post;
      expect(createMedia).toMatchObject({
        operationId: "files.createMedia",
        "x-executor-toolPath": "files.createMedia",
        "x-executor-pathTemplate": "/upload/drive/v3/files",
      });
      expect(createMedia?.parameters).toContainEqual(
        expect.objectContaining({
          name: "uploadType",
          in: "query",
          required: true,
          schema: {
            type: "string",
            description: "The upload type for the media upload.",
            enum: ["media"],
            default: "media",
          },
        }),
      );
      expect(createMedia?.requestBody).toMatchObject({
        content: {
          "application/octet-stream": {
            schema: { type: "string", format: "binary" },
          },
        },
      });

      // The updateMedia operation should preserve path parameters from the original method.
      const updateMedia = spec.paths["/upload/drive/v3/files/{fileId}"]?.patch;
      expect(updateMedia).toMatchObject({
        operationId: "files.updateMedia",
        "x-executor-toolPath": "files.updateMedia",
        "x-executor-pathTemplate": "/upload/drive/v3/files/{fileId}",
      });
      expect(updateMedia?.parameters).toContainEqual(
        expect.objectContaining({
          name: "fileId",
          in: "path",
          required: true,
        }),
      );
      expect(updateMedia?.parameters).toContainEqual(
        expect.objectContaining({
          name: "uploadType",
          in: "query",
          required: true,
          schema: {
            type: "string",
            description: "The upload type for the media upload.",
            enum: ["media"],
            default: "media",
          },
        }),
      );

      // Extraction should produce a usable input schema that includes bodyBase64.
      const parsed = yield* parse(result.specText);
      const extracted = yield* extract(parsed);
      const createMediaOp = extracted.operations.find(
        (candidate) => candidate.operationId === "files.createMedia",
      );
      expect(createMediaOp?.operationId).toBe("files.createMedia");
      const inputSchema = createMediaOp
        ? (Option.getOrUndefined(createMediaOp.inputSchema) as
            | {
                properties?: Record<string, unknown>;
                required?: string[];
              }
            | undefined)
        : undefined;
      expect(inputSchema).toBeDefined();
      expect(inputSchema?.properties?.bodyBase64).toMatchObject({
        type: "string",
        contentEncoding: "base64",
        contentMediaType: "application/octet-stream",
      });
      expect(inputSchema?.required).toContain("bodyBase64");
      expect(inputSchema?.properties?.body).toBeUndefined();
    }),
);

it.effect("bundles Google Discovery documents into one Google OpenAPI source", () =>
  Effect.gen(function* () {
    const result = yield* convertGoogleDiscoveryBundleToOpenApi({
      documents: [
        {
          discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest",
          // @effect-diagnostics-next-line preferSchemaOverJson:off
          documentText: JSON.stringify({
            name: "gmail",
            version: "v1",
            title: "Gmail API",
            rootUrl: "https://gmail.googleapis.com/",
            servicePath: "",
            auth: {
              oauth2: {
                scopes: {
                  "https://www.googleapis.com/auth/gmail.metadata": {
                    description: "Read metadata",
                  },
                },
              },
            },
            resources: {
              users: {
                resources: {
                  messages: {
                    methods: {
                      list: {
                        id: "gmail.users.messages.list",
                        httpMethod: "GET",
                        path: "gmail/v1/users/{userId}/messages",
                        response: { $ref: "Message" },
                        scopes: ["https://www.googleapis.com/auth/gmail.metadata"],
                        parameters: {
                          userId: {
                            location: "path",
                            required: true,
                            type: "string",
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
            schemas: {
              Message: {
                type: "object",
                properties: {
                  id: { type: "string" },
                },
              },
            },
          }),
        },
        {
          discoveryUrl: "https://chat.googleapis.com/$discovery/rest?version=v1",
          // @effect-diagnostics-next-line preferSchemaOverJson:off
          documentText: JSON.stringify({
            name: "chat",
            version: "v1",
            title: "Google Chat API",
            rootUrl: "https://chat.googleapis.com/",
            servicePath: "",
            auth: {
              oauth2: {
                scopes: {
                  "https://www.googleapis.com/auth/chat.spaces.readonly": {
                    description: "Read spaces",
                  },
                },
              },
            },
            resources: {
              spaces: {
                methods: {
                  get: {
                    id: "chat.spaces.get",
                    httpMethod: "GET",
                    path: "v1/{+name}",
                    response: { $ref: "Space" },
                    scopes: ["https://www.googleapis.com/auth/chat.spaces.readonly"],
                    parameters: {
                      name: {
                        location: "path",
                        required: true,
                        type: "string",
                      },
                    },
                  },
                },
                resources: {
                  messages: {
                    methods: {
                      get: {
                        id: "chat.spaces.messages.get",
                        httpMethod: "GET",
                        path: "v1/{+name}",
                        response: { $ref: "Message" },
                        scopes: ["https://www.googleapis.com/auth/chat.spaces.readonly"],
                        parameters: {
                          name: {
                            location: "path",
                            required: true,
                            type: "string",
                          },
                        },
                      },
                    },
                  },
                },
              },
            },
            schemas: {
              Space: {
                type: "object",
                properties: {
                  name: { type: "string" },
                },
              },
              Message: {
                type: "object",
                properties: {
                  name: { type: "string" },
                },
              },
            },
          }),
        },
      ],
    });

    const spec = decodeConvertedSpec(result.specText);
    expect(result.title).toBe("Google");
    expect(result.baseUrl).toBe("https://www.googleapis.com/");
    expect(result.discoveryUrls).toEqual([
      "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest",
      "https://www.googleapis.com/discovery/v1/apis/chat/v1/rest",
    ]);
    expect(spec.servers).toEqual([{ url: "https://www.googleapis.com/" }]);
    expect(spec.components.schemas).toHaveProperty("gmail_v1_Message");
    expect(spec.components.schemas).toHaveProperty("chat_v1_Message");

    const gmailList = spec.paths["/gmail/v1/users/{userId}/messages"]?.get;
    expect(gmailList).toMatchObject({
      operationId: "gmail.users.messages.list",
      "x-executor-toolPath": "gmail.users.messages.list",
      servers: [{ url: "https://gmail.googleapis.com/" }],
    });
    expect(gmailList?.responses).toMatchObject({
      "200": {
        content: {
          "application/json": {
            schema: { $ref: "#/components/schemas/gmail_v1_Message" },
          },
        },
      },
    });

    const chatSpaceGet = spec.paths["/v1/{name}"]?.get;
    const chatMessageGet = spec.paths["/chat.spaces.messages.get"]?.get;
    expect(chatSpaceGet).toMatchObject({
      operationId: "chat.spaces.get",
      "x-executor-pathTemplate": "/v1/{+name}",
      servers: [{ url: "https://chat.googleapis.com/" }],
    });
    expect(chatMessageGet).toMatchObject({
      operationId: "chat.spaces.messages.get",
      "x-executor-pathTemplate": "/v1/{+name}",
      servers: [{ url: "https://chat.googleapis.com/" }],
    });
    expect(chatSpaceGet?.parameters).toContainEqual(
      expect.objectContaining({
        name: "name",
        in: "path",
        allowReserved: true,
      }),
    );

    const parsed = yield* parse(result.specText);
    const extracted = yield* extract(parsed);
    const extractedGmail = extracted.operations.find(
      (candidate) => candidate.operationId === "gmail.users.messages.list",
    );
    const extractedChatMessage = extracted.operations.find(
      (candidate) => candidate.operationId === "chat.spaces.messages.get",
    );
    expect(extractedGmail?.servers[0]?.url).toBe("https://gmail.googleapis.com/");
    expect(extractedChatMessage?.pathTemplate).toBe("/v1/{+name}");
    expect(extractedChatMessage?.servers[0]?.url).toBe("https://chat.googleapis.com/");
    // v2: the bundled oauth scopes are carried on the oauth auth template.
    const oauthTemplate = result.authenticationTemplate?.find((entry) => entry.kind === "oauth2");
    expect(oauthTemplate?.kind === "oauth2" ? oauthTemplate.scopes : undefined).toEqual([
      "https://www.googleapis.com/auth/gmail.metadata",
      "https://www.googleapis.com/auth/chat.spaces.readonly",
    ]);
  }),
);

// ---------------------------------------------------------------------------
// The merged bundle scope set is the COMPACTED + FILTERED union: sub-scopes
// collapse under their broad parent (`gmail.*` → `mail.google.com/`,
// `calendar.*` → `calendar`, `userinfo.email` → `email`), and scopes a user
// OAuth consent screen can't show (`chat.bot`, `chat.app.*`, `keep`) are
// dropped. The persisted auth template, the spec `securitySchemes.googleOAuth2`
// flow scopes, and the root `security` entry all agree - so the preview the
// picker shows and the set `oauth.start` requests at connect are the same.
// Per-operation `x-google-scopes`/`security` stay RAW (they describe per-method
// scope needs, not consent).
// ---------------------------------------------------------------------------

const ConvertedSpecSecurity = Schema.Struct({
  components: Schema.Struct({
    securitySchemes: Schema.Record(
      Schema.String,
      Schema.Struct({
        type: Schema.String,
        flows: Schema.Struct({
          authorizationCode: Schema.Struct({
            scopes: Schema.Record(Schema.String, Schema.String),
          }),
        }),
      }),
    ),
  }),
  security: Schema.Array(Schema.Record(Schema.String, Schema.Array(Schema.String))),
  paths: Schema.Record(
    Schema.String,
    Schema.Record(Schema.String, Schema.Struct({ "x-google-scopes": Schema.Array(Schema.String) })),
  ),
});
const decodeConvertedSpecSecurity = Schema.decodeUnknownSync(
  Schema.fromJsonString(ConvertedSpecSecurity),
);

it.effect("generates media-upload operations for bundled Google Discovery documents", () =>
  Effect.gen(function* () {
    const result = yield* convertGoogleDiscoveryBundleToOpenApi({
      documents: [
        {
          discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
          // @effect-diagnostics-next-line preferSchemaOverJson:off
          documentText: JSON.stringify({
            name: "drive",
            version: "v3",
            title: "Drive API",
            rootUrl: "https://www.googleapis.com/",
            servicePath: "drive/v3/",
            auth: {
              oauth2: {
                scopes: {
                  "https://www.googleapis.com/auth/drive.file": {
                    description: "Drive file access",
                  },
                },
              },
            },
            resources: {
              files: {
                methods: {
                  create: {
                    id: "drive.files.create",
                    httpMethod: "POST",
                    path: "files",
                    request: { $ref: "File" },
                    response: { $ref: "File" },
                    supportsMediaUpload: true,
                    mediaUpload: {
                      accept: ["*/*"],
                      protocols: {
                        simple: {
                          multipart: true,
                          path: "/upload/drive/v3/files",
                        },
                      },
                    },
                    scopes: ["https://www.googleapis.com/auth/drive.file"],
                    parameters: {},
                  },
                },
              },
            },
            schemas: {
              File: {
                id: "File",
                type: "object",
                properties: {
                  name: { type: "string" },
                },
              },
            },
          }),
        },
      ],
    });

    const spec = decodeConvertedSpec(result.specText);
    const createMedia = spec.paths["/upload/drive/v3/files"]?.post;
    expect(createMedia).toMatchObject({
      operationId: "drive.files.createMedia",
      "x-executor-toolPath": "drive.files.createMedia",
      "x-executor-pathTemplate": "/upload/drive/v3/files",
    });
    expect(createMedia?.parameters).toContainEqual(
      expect.objectContaining({
        name: "uploadType",
        in: "query",
        required: true,
      }),
    );
    expect(createMedia?.requestBody).toMatchObject({
      content: {
        "application/octet-stream": {
          schema: { type: "string", format: "binary" },
        },
      },
    });
  }),
);

it.effect("compacts and filters the merged bundle scope set into a clean consent set", () =>
  Effect.gen(function* () {
    const result = yield* convertGoogleDiscoveryBundleToOpenApi({
      documents: [
        {
          discoveryUrl: "https://www.googleapis.com/discovery/v1/apis/gmail/v1/rest",
          // @effect-diagnostics-next-line preferSchemaOverJson:off
          documentText: JSON.stringify({
            name: "gmail",
            version: "v1",
            title: "Gmail API",
            rootUrl: "https://gmail.googleapis.com/",
            servicePath: "",
            auth: {
              oauth2: {
                scopes: {
                  // Broad parent + a sub-scope that must collapse under it.
                  "https://mail.google.com/": { description: "Full Gmail access" },
                  "https://www.googleapis.com/auth/gmail.readonly": { description: "Read Gmail" },
                  // Identity scope normalized to `email`.
                  "https://www.googleapis.com/auth/userinfo.email": { description: "Email" },
                },
              },
            },
            resources: {
              users: {
                resources: {
                  messages: {
                    methods: {
                      list: {
                        id: "gmail.users.messages.list",
                        httpMethod: "GET",
                        path: "gmail/v1/users/{userId}/messages",
                        scopes: ["https://www.googleapis.com/auth/gmail.readonly"],
                        parameters: {
                          userId: { location: "path", required: true, type: "string" },
                        },
                      },
                    },
                  },
                },
              },
            },
            schemas: {},
          }),
        },
        {
          discoveryUrl: "https://chat.googleapis.com/$discovery/rest?version=v1",
          // @effect-diagnostics-next-line preferSchemaOverJson:off
          documentText: JSON.stringify({
            name: "chat",
            version: "v1",
            title: "Google Chat API",
            rootUrl: "https://chat.googleapis.com/",
            servicePath: "",
            auth: {
              oauth2: {
                scopes: {
                  // A keepable consent scope plus two that the user-consent filter
                  // must drop (`chat.bot`, `chat.app.*`).
                  "https://www.googleapis.com/auth/chat.spaces.readonly": { description: "Spaces" },
                  "https://www.googleapis.com/auth/chat.bot": { description: "Bot" },
                  "https://www.googleapis.com/auth/chat.app.spaces": { description: "App spaces" },
                },
              },
            },
            resources: {
              spaces: {
                methods: {
                  get: {
                    id: "chat.spaces.get",
                    httpMethod: "GET",
                    path: "v1/{+name}",
                    scopes: ["https://www.googleapis.com/auth/chat.bot"],
                    parameters: {
                      name: { location: "path", required: true, type: "string" },
                    },
                  },
                },
              },
            },
            schemas: {},
          }),
        },
      ],
    });

    const expectedConsentScopes = [
      "https://mail.google.com/",
      "email",
      "https://www.googleapis.com/auth/chat.spaces.readonly",
    ];

    // The derived oauth auth template carries the compacted/filtered set
    // (gmail.readonly collapsed, userinfo.email → email, chat.bot/chat.app.* dropped).
    const oauthTemplate = result.authenticationTemplate?.find((entry) => entry.kind === "oauth2");
    expect(oauthTemplate?.kind === "oauth2" ? [...oauthTemplate.scopes].sort() : undefined).toEqual(
      [...expectedConsentScopes].sort(),
    );

    const spec = decodeConvertedSpecSecurity(result.specText);
    // The spec's securitySchemes flow scopes match the consent set exactly.
    expect(
      Object.keys(spec.components.securitySchemes["googleOAuth2"]!.flows.authorizationCode.scopes)
        .slice()
        .sort(),
    ).toEqual([...expectedConsentScopes].sort());
    // The root security entry references the same compacted set.
    expect([...(spec.security[0]?.["googleOAuth2"] ?? [])].sort()).toEqual(
      [...expectedConsentScopes].sort(),
    );
    // Per-operation x-google-scopes stay RAW - a dropped consent scope can still
    // be the scope a given method advertises.
    expect(spec.paths["/v1/{name}"]?.get?.["x-google-scopes"]).toEqual([
      "https://www.googleapis.com/auth/chat.bot",
    ]);
  }),
);
