import { describe, expect, test } from "vitest";
import { ConfigProvider, Effect, Layer } from "effect";
import * as Schema from "effect/Schema";
import * as HttpClient from "effect/http/HttpClient";
import * as HttpClientError from "effect/http/HttpClientError";
import * as HttpClientResponse from "effect/http/HttpClientResponse";
import type * as HttpClientRequest from "effect/http/HttpClientRequest";

import { makeApiClient, operationDefinitions } from "./effect.ts";
import {
  V1CreateASsoProviderInput,
  V1CreateASsoProviderOutput,
  V1DeleteASsoProviderOutput,
  V1GetASsoProviderOutput,
  V1GetDatabaseOpenapiOutput,
  V1ListAllSsoProviderOutput,
  V1UpdateASsoProviderOutput,
} from "./generated/contracts.ts";

const textDecoder = new TextDecoder();

function httpClientLayer(
  handler: (
    request: HttpClientRequest.HttpClientRequest,
  ) => Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>,
) {
  return Layer.succeed(
    HttpClient.HttpClient,
    HttpClient.make((request) => handler(request)),
  );
}

function jsonResponse(
  request: HttpClientRequest.HttpClientRequest,
  status: number,
  body: unknown,
): HttpClientResponse.HttpClientResponse {
  return HttpClientResponse.fromWeb(
    request,
    new Response(JSON.stringify(body), {
      status,
      headers: {
        "content-type": "application/json",
      },
    }),
  );
}

function requestBodyText(request: HttpClientRequest.HttpClientRequest): string {
  if (request.body._tag !== "Uint8Array") {
    throw new Error(`Expected Uint8Array body, got ${request.body._tag}`);
  }
  return textDecoder.decode(request.body.body);
}

const config = {
  baseUrl: "https://api.supabase.com",
  accessToken: "test-token",
  userAgent: "supabase-api/test",
} as const;

describe("SSO provider contracts", () => {
  // The provider payload the Management API actually returns: no `saml.id` or
  // `domains[].id`. Every SSO subcommand decodes one of these five schemas, so a
  // stale required key here breaks the whole command family.
  const SPARSE_PROVIDER = {
    id: "0b0d48f6-878b-4190-88d7-2ca33ed800bc",
    saml: {
      entity_id: "https://example.com",
      metadata_url: "https://example.com",
      metadata_xml: '<?xml version="2.0"?>',
      attribute_mapping: {},
    },
    domains: [
      {
        domain: "example.com",
        created_at: "2023-03-28T13:50:14.464Z",
        updated_at: "2023-03-28T13:50:14.464Z",
      },
    ],
    created_at: "2023-03-28T13:50:14.464Z",
    updated_at: "2023-03-28T13:50:14.464Z",
  };

  const SINGLE_PROVIDER_SCHEMAS = [
    V1GetASsoProviderOutput,
    V1CreateASsoProviderOutput,
    V1UpdateASsoProviderOutput,
    V1DeleteASsoProviderOutput,
  ];

  test("decodes SSO provider responses without nested SAML and domain IDs", () => {
    expect(() =>
      Schema.decodeSync(V1ListAllSsoProviderOutput)({ items: [SPARSE_PROVIDER] }),
    ).not.toThrow();
    for (const schema of SINGLE_PROVIDER_SCHEMAS) {
      expect(() => Schema.decodeSync(schema)(SPARSE_PROVIDER)).not.toThrow();
    }
  });

  test("drops nested SAML and domain IDs the spec no longer declares", () => {
    const withLegacyIds = {
      ...SPARSE_PROVIDER,
      saml: { ...SPARSE_PROVIDER.saml, id: "8682fcf4-4056-455c-bd93-f33295604929" },
      domains: [{ ...SPARSE_PROVIDER.domains[0], id: "9484591c-a203-4500-bea7-d0aaa845e2f5" }],
    };

    for (const schema of SINGLE_PROVIDER_SCHEMAS) {
      const decoded = Schema.decodeSync(schema)(withLegacyIds);
      expect(decoded.saml).not.toHaveProperty("id");
      expect(decoded.domains?.[0]).not.toHaveProperty("id");
    }
  });

  test("accepts object-valued SSO attribute mapping defaults", () => {
    expect(() =>
      Schema.decodeSync(V1CreateASsoProviderInput)({
        ref: "abcdefghijklmnopqrst",
        type: "saml",
        attribute_mapping: {
          keys: {
            role: { default: { department: "engineering" } },
          },
        },
      }),
    ).not.toThrow();
  });
});

describe("database OpenAPI response contract", () => {
  test("accepts a normal non-empty OpenAPI document", () => {
    expect(() =>
      Schema.decodeSync(V1GetDatabaseOpenapiOutput)({
        openapi: "3.0.0",
        info: { title: "Example", version: "1.0.0" },
        paths: {
          "/users": {
            get: {
              responses: { "200": { description: "ok" } },
            },
          },
        },
      }),
    ).not.toThrow();
  });
});

describe("makeApiClient", () => {
  test("allows raw operations to override generated request headers", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        let accept: string | undefined;

        const client = yield* makeApiClient(config).pipe(
          Effect.provide(
            httpClientLayer((request) => {
              accept = request.headers.accept;
              return Effect.succeed(jsonResponse(request, 200, {}));
            }),
          ),
        );

        yield* client.executeRaw(
          operationDefinitions.v1GetAFunctionBody,
          {
            ref: "abcdefghijklmnopqrst",
            function_slug: "hello-world",
          },
          { headers: { Accept: "multipart/form-data" } },
        );

        expect(accept).toBe("multipart/form-data");
      }),
    ));

  test("forwards per-request options through generated typed methods", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        let seen: { url: string; authorization: string | undefined } | undefined;

        const client = yield* makeApiClient(config).pipe(
          Effect.provide(
            httpClientLayer((request) => {
              seen = { url: request.url, authorization: request.headers.authorization };
              return Effect.succeed(jsonResponse(request, 200, []));
            }),
          ),
        );

        yield* client.v1.listAllProjects(
          {},
          { baseUrl: "https://api.example.test", accessToken: "per-request-token" },
        );

        expect(seen).toEqual({
          url: "https://api.example.test/v1/projects",
          authorization: "Bearer per-request-token",
        });
      }),
    ));

  test("uses the default API URL when baseUrl is omitted", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        const seenRequests: Array<{ method: string; url: string }> = [];

        const client = yield* makeApiClient({ accessToken: "test-token" }).pipe(
          Effect.provide(
            httpClientLayer((request) => {
              seenRequests.push({
                method: request.method,
                url: request.url,
              });
              return Effect.succeed(
                jsonResponse(request, 200, {
                  id: "project-id",
                  ref: "abcdefghijklmnopqrst",
                  organization_id: "org-id",
                  organization_slug: "my-org",
                  name: "project-name",
                  region: "us-east-1",
                  created_at: "2026-03-13T12:00:00.000Z",
                  status: "ACTIVE_HEALTHY",
                  database: {
                    host: "db.supabase.internal",
                    version: "17.0.1",
                    postgres_engine: "17",
                    release_channel: "ga",
                  },
                }),
              );
            }),
          ),
        );

        yield* client.v1.getProject({
          ref: "abcdefghijklmnopqrst",
        });

        expect(seenRequests).toEqual([
          {
            method: "GET",
            url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst",
          },
        ]);
      }),
    ));

  test("reads the access token from the environment when omitted", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        const seenRequests: Array<{ authorization: string | undefined }> = [];

        const client = yield* makeApiClient().pipe(
          Effect.provide(
            Layer.mergeAll(
              httpClientLayer((request) => {
                seenRequests.push({
                  authorization: request.headers.authorization,
                });
                return Effect.succeed(
                  jsonResponse(request, 200, {
                    id: "project-id",
                    ref: "abcdefghijklmnopqrst",
                    organization_id: "org-id",
                    organization_slug: "my-org",
                    name: "project-name",
                    region: "us-east-1",
                    created_at: "2026-03-13T12:00:00.000Z",
                    status: "ACTIVE_HEALTHY",
                    database: {
                      host: "db.supabase.internal",
                      version: "17.0.1",
                      postgres_engine: "17",
                      release_channel: "ga",
                    },
                  }),
                );
              }),
              ConfigProvider.layer(
                ConfigProvider.fromUnknown({
                  SUPABASE_ACCESS_TOKEN: "env-token",
                }),
              ),
            ),
          ),
        );

        yield* client.v1.getProject({
          ref: "abcdefghijklmnopqrst",
        });

        expect(seenRequests).toEqual([{ authorization: "Bearer env-token" }]);
      }),
    ));

  test("passes configured default headers through the facade client", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        const seenRequests: Array<{
          command: string | undefined;
          commandRunId: string | undefined;
          authorization: string | undefined;
        }> = [];

        const client = yield* makeApiClient({
          ...config,
          headers: {
            "X-Supabase-Command": "projects get",
            "X-Supabase-Command-Run-ID": "run-456",
          },
        }).pipe(
          Effect.provide(
            httpClientLayer((request) => {
              seenRequests.push({
                command: request.headers["x-supabase-command"],
                commandRunId: request.headers["x-supabase-command-run-id"],
                authorization: request.headers.authorization,
              });
              return Effect.succeed(
                jsonResponse(request, 200, {
                  id: "project-id",
                  ref: "abcdefghijklmnopqrst",
                  organization_id: "org-id",
                  organization_slug: "my-org",
                  name: "project-name",
                  region: "us-east-1",
                  created_at: "2026-03-13T12:00:00.000Z",
                  status: "ACTIVE_HEALTHY",
                  database: {
                    host: "db.supabase.internal",
                    version: "17.0.1",
                    postgres_engine: "17",
                    release_channel: "ga",
                  },
                }),
              );
            }),
          ),
        );

        yield* client.v1.getProject({
          ref: "abcdefghijklmnopqrst",
        });

        expect(seenRequests).toEqual([
          {
            command: "projects get",
            commandRunId: "run-456",
            authorization: "Bearer test-token",
          },
        ]);
      }),
    ));

  test("fails early when no access token is configured", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        const exit = yield* makeApiClient().pipe(
          Effect.exit,
          Effect.provide(
            Layer.mergeAll(
              httpClientLayer((request) =>
                Effect.succeed(
                  jsonResponse(request, 200, {
                    ok: true,
                  }),
                ),
              ),
              ConfigProvider.layer(ConfigProvider.fromUnknown({})),
            ),
          ),
        );

        expect(exit._tag).toBe("Failure");
        if (exit._tag === "Failure") {
          expect(String(exit.cause)).toContain("Missing access token");
        }
      }),
    ));

  test("returns only versioned methods under the v1 namespace", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        const seenRequests: Array<{ method: string; url: string }> = [];

        const client = yield* makeApiClient(config).pipe(
          Effect.provide(
            httpClientLayer((request) => {
              seenRequests.push({
                method: request.method,
                url: request.url,
              });

              if (
                request.method === "POST" &&
                request.url === "https://api.supabase.com/v1/projects"
              ) {
                return Effect.succeed(
                  jsonResponse(request, 200, {
                    id: "project-id",
                    ref: "abcdefghijklmnopqrst",
                    organization_id: "org-id",
                    organization_slug: "my-org",
                    name: "project-name",
                    region: "us-east-1",
                    created_at: "2026-03-13T12:00:00.000Z",
                    status: "ACTIVE_HEALTHY",
                  }),
                );
              }

              if (
                request.method === "GET" &&
                request.url === "https://api.supabase.com/v1/projects"
              ) {
                return Effect.succeed(
                  jsonResponse(request, 200, [
                    {
                      id: "project-id",
                      ref: "abcdefghijklmnopqrst",
                      organization_id: "org-id",
                      organization_slug: "my-org",
                      name: "project-name",
                      region: "us-east-1",
                      created_at: "2026-03-13T12:00:00.000Z",
                      status: "ACTIVE_HEALTHY",
                      database: {
                        host: "db.supabase.internal",
                        version: "17.0.1",
                        postgres_engine: "17",
                        release_channel: "ga",
                      },
                    },
                  ]),
                );
              }

              return Effect.succeed(
                jsonResponse(request, 200, {
                  id: "project-id",
                  ref: "abcdefghijklmnopqrst",
                  organization_id: "org-id",
                  organization_slug: "my-org",
                  name: "project-name",
                  region: "us-east-1",
                  created_at: "2026-03-13T12:00:00.000Z",
                  status: "ACTIVE_HEALTHY",
                  database: {
                    host: "db.supabase.internal",
                    version: "17.0.1",
                    postgres_engine: "17",
                    release_channel: "ga",
                  },
                }),
              );
            }),
          ),
        );

        expect("createAProject" in client).toBe(false);
        expect("getProject" in client).toBe(false);
        expect("listAllProjects" in client).toBe(false);
        expect(typeof client.v1.createAProject).toBe("function");
        expect(typeof client.v1.getProject).toBe("function");
        expect(typeof client.v1.listAllProjects).toBe("function");

        const created = yield* client.v1.createAProject({
          db_pass: "hunter2",
          name: "project-name",
          organization_slug: "my-org",
        });
        const project = yield* client.v1.getProject({
          ref: "abcdefghijklmnopqrst",
        });
        const projects = yield* client.v1.listAllProjects();

        expect(created.ref).toBe("abcdefghijklmnopqrst");
        expect(project.database.host).toBe("db.supabase.internal");
        expect(projects).toHaveLength(1);
        expect(seenRequests).toEqual([
          {
            method: "POST",
            url: "https://api.supabase.com/v1/projects",
          },
          {
            method: "GET",
            url: "https://api.supabase.com/v1/projects/abcdefghijklmnopqrst",
          },
          {
            method: "GET",
            url: "https://api.supabase.com/v1/projects",
          },
        ]);
      }),
    ));

  test("addresses same-named v1 and v2 operations independently by namespace", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        const seenRequests: Array<{ method: string; url: string }> = [];

        const client = yield* makeApiClient(config).pipe(
          Effect.provide(
            httpClientLayer((request) => {
              seenRequests.push({
                method: request.method,
                url: request.url,
              });

              if (request.url === "https://api.supabase.com/v1/organizations/my-org/members") {
                return Effect.succeed(
                  jsonResponse(request, 200, [
                    {
                      user_id: "user-id",
                      user_name: "user-name",
                      role_name: "Owner",
                      mfa_enabled: false,
                      avatar_url: null,
                    },
                  ]),
                );
              }

              return Effect.succeed(
                jsonResponse(request, 200, {
                  data: [],
                  links: { prev: null, next: null },
                }),
              );
            }),
          ),
        );

        expect(typeof client.v1.listOrganizationMembers).toBe("function");
        expect(typeof client.v2.listOrganizationMembers).toBe("function");

        const v1Members = yield* client.v1.listOrganizationMembers({ slug: "my-org" });
        const v2Members = yield* client.v2.listOrganizationMembers({ slug: "my-org" });

        expect(v1Members).toEqual([
          {
            user_id: "user-id",
            user_name: "user-name",
            role_name: "Owner",
            mfa_enabled: false,
            avatar_url: null,
          },
        ]);
        expect(v2Members.data).toEqual([]);
        expect(seenRequests).toEqual([
          {
            method: "GET",
            url: "https://api.supabase.com/v1/organizations/my-org/members",
          },
          {
            method: "GET",
            url: "https://api.supabase.com/v2/organizations/my-org/members",
          },
        ]);
      }),
    ));

  test("serializes generated binary methods through the effect facade", () =>
    Effect.runPromise(
      Effect.gen(function* () {
        let seenRequest: HttpClientRequest.HttpClientRequest | undefined;

        const client = yield* makeApiClient(config).pipe(
          Effect.provide(
            httpClientLayer((request) => {
              seenRequest = request;
              return Effect.succeed(
                jsonResponse(request, 201, {
                  id: "function-id",
                  slug: "demo",
                  name: "Demo Function",
                  status: "ACTIVE",
                  version: 1,
                  created_at: 1_710_000_000,
                  updated_at: 1_710_000_001,
                  verify_jwt: true,
                  entrypoint_path: "functions/demo/index.ts",
                  import_map_path: "functions/demo/deno.json",
                  ezbr_sha256: "abc123",
                }),
              );
            }),
          ),
        );

        const body = new Blob(["console.log('blob body');"]);
        const result = yield* client.v1.createAFunction({
          ref: "abcdefghijklmnopqrst",
          slug: "demo",
          verify_jwt: true,
          entrypoint_path: "functions/demo/index.ts",
          body,
        });

        expect(result.slug).toBe("demo");
        expect(seenRequest?.headers["content-type"]).toBe("application/vnd.denoland.eszip");
        expect(new URL(seenRequest!.url).pathname).toBe(
          "/v1/projects/abcdefghijklmnopqrst/functions",
        );
        expect(requestBodyText(seenRequest!)).toBe("console.log('blob body');");
      }),
    ));
});