// ---------------------------------------------------------------------------
// Tests for OAuth2 flow extraction in previewSpec. Covers:
//   - authorizationCode flow extraction (URLs + scopes)
//   - clientCredentials flow extraction
//   - security schemes defined via $ref are no longer silently dropped
//   - bearerFormat / openIdConnectUrl are captured
//   - invalid flows (missing tokenUrl) are ignored
// ---------------------------------------------------------------------------

import { describe, expect, it } from "@effect/vitest";
import { Effect, Option, Schema } from "effect";
import { FetchHttpClient } from "effect/unstable/http";
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi";

import { previewSpec as previewSpecRaw } from "./preview";

const previewSpec = (input: string) =>
  previewSpecRaw(input).pipe(Effect.provide(FetchHttpClient.layer));

const PreviewGroup = HttpApiGroup.make("default", { topLevel: true }).add(
  HttpApiEndpoint.get("ping", "/ping", { success: Schema.Unknown }),
);

const PreviewApi = HttpApi.make("previewOauth2Test")
  .add(PreviewGroup)
  .annotateMerge(
    OpenApi.annotations({
      title: "Test API",
      version: "1.0.0",
      servers: [{ url: "https://api.example.com" }],
    }),
  );

const minimalSpec = (
  securitySchemes: Record<string, unknown>,
  components: Record<string, unknown> = {},
) =>
  OpenApi.fromApi(
    PreviewApi.annotateMerge(
      OpenApi.annotations({
        transform: (spec) => ({
          ...spec,
          components: { ...components, securitySchemes },
        }),
      }),
    ),
  );

describe("previewSpec OAuth2 extraction", () => {
  it.effect("extracts authorizationCode flow with URLs, scopes, refreshUrl", () =>
    Effect.gen(function* () {
      const spec = minimalSpec({
        oauth_app: {
          type: "oauth2",
          flows: {
            authorizationCode: {
              authorizationUrl: "https://example.com/oauth/authorize",
              tokenUrl: "https://example.com/oauth/token",
              refreshUrl: "https://example.com/oauth/refresh",
              scopes: {
                read: "Read access",
                write: "Write access",
              },
            },
          },
        },
      });
      const preview = yield* previewSpec(JSON.stringify(spec));

      expect(preview.securitySchemes).toHaveLength(1);
      const scheme = preview.securitySchemes[0]!;
      expect(scheme.name).toBe("oauth_app");
      expect(scheme.type).toBe("oauth2");
      expect(Option.isSome(scheme.flows)).toBe(true);
      const flows = Option.getOrThrow(scheme.flows);
      expect(Option.isSome(flows.authorizationCode)).toBe(true);
      const flow = Option.getOrThrow(flows.authorizationCode);
      expect(flow.authorizationUrl).toBe("https://example.com/oauth/authorize");
      expect(flow.tokenUrl).toBe("https://example.com/oauth/token");
      expect(Option.getOrElse(flow.refreshUrl, () => "")).toBe("https://example.com/oauth/refresh");
      expect(flow.scopes).toEqual({
        read: "Read access",
        write: "Write access",
      });

      // A preset should be generated for this flow.
      expect(preview.oauth2Presets).toHaveLength(1);
      expect(preview.headerPresets).toHaveLength(0);
      const preset = preview.oauth2Presets[0]!;
      expect(preset.flow).toBe("authorizationCode");
      expect(preset.securitySchemeName).toBe("oauth_app");
      expect(preset.tokenUrl).toBe("https://example.com/oauth/token");
      expect(preset.label).toContain("Authorization Code");
      expect(preset.label).toContain("oauth_app");
      expect(preset.scopes).toEqual({
        read: "Read access",
        write: "Write access",
      });
    }),
  );

  it.effect("extracts clientCredentials flow alongside authorizationCode", () =>
    Effect.gen(function* () {
      const spec = minimalSpec({
        oauth_app: {
          type: "oauth2",
          flows: {
            authorizationCode: {
              authorizationUrl: "https://example.com/auth",
              tokenUrl: "https://example.com/token",
              scopes: {},
            },
            clientCredentials: {
              tokenUrl: "https://example.com/token",
              scopes: { "admin:read": "Admin read" },
            },
          },
        },
      });
      const preview = yield* previewSpec(JSON.stringify(spec));

      expect(preview.oauth2Presets).toHaveLength(2);
      const flowKinds = preview.oauth2Presets.map((p) => p.flow).sort();
      expect(flowKinds).toEqual(["authorizationCode", "clientCredentials"]);

      const cc = preview.oauth2Presets.find((p) => p.flow === "clientCredentials")!;
      expect(Option.isNone(cc.authorizationUrl)).toBe(true);
      expect(cc.scopes).toEqual({ "admin:read": "Admin read" });
    }),
  );

  it.effect("skips authorizationCode flow missing tokenUrl or authorizationUrl", () =>
    Effect.gen(function* () {
      const spec = minimalSpec({
        broken: {
          type: "oauth2",
          flows: {
            authorizationCode: {
              authorizationUrl: "https://example.com/auth",
              // tokenUrl missing
              scopes: {},
            },
          },
        },
      });
      const preview = yield* previewSpec(JSON.stringify(spec));

      expect(preview.securitySchemes).toHaveLength(1);
      // Scheme is still captured but with no flows.
      expect(Option.isNone(preview.securitySchemes[0]!.flows)).toBe(true);
      expect(preview.oauth2Presets).toHaveLength(0);
    }),
  );

  it.effect("resolves security schemes defined via $ref", () =>
    Effect.gen(function* () {
      const spec = minimalSpec({
        api_token: { $ref: "#/components/securitySchemes/_api_token_impl" },
        _api_token_impl: {
          type: "http",
          scheme: "bearer",
          bearerFormat: "JWT",
          description: "Internal token scheme",
        },
      });
      // Note: `api_token` should resolve through the sibling
      // `_api_token_impl` scheme in `components.securitySchemes`.

      const preview = yield* previewSpec(JSON.stringify(spec));
      // Both keys are present, but the `api_token` entry should resolve to
      // the http bearer scheme (previously it was silently dropped).
      const apiToken = preview.securitySchemes.find((s) => s.name === "api_token");
      expect(apiToken).toBeDefined();
      expect(apiToken!.type).toBe("http");
      expect(Option.getOrElse(apiToken!.scheme, () => "")).toBe("bearer");
      expect(Option.getOrElse(apiToken!.bearerFormat, () => "")).toBe("JWT");
    }),
  );
  it.effect("captures openIdConnectUrl for openIdConnect schemes", () =>
    Effect.gen(function* () {
      const spec = minimalSpec({
        oidc: {
          type: "openIdConnect",
          openIdConnectUrl: "https://example.com/.well-known/openid-configuration",
        },
      });
      const preview = yield* previewSpec(JSON.stringify(spec));
      const scheme = preview.securitySchemes[0]!;
      expect(scheme.type).toBe("openIdConnect");
      expect(Option.getOrElse(scheme.openIdConnectUrl, () => "")).toBe(
        "https://example.com/.well-known/openid-configuration",
      );
    }),
  );
});
