import { describe, expect, it } from "@effect/vitest";
import { Effect, Predicate } from "effect";

import {
  AuthTemplateSlug,
  ConnectionName,
  IntegrationSlug,
  OAuthClientSlug,
  ToolName,
} from "./ids";
import { OAuthRegisterDynamicError } from "./oauth-client";
import { definePlugin } from "./plugin";
import { makeTestWorkspaceHarness, memoryCredentialsPlugin } from "./test-config";
import { serveOAuthTestServer } from "./testing/oauth-test-server";

// RFC 7591 Dynamic Client Registration, end to end:
//   probe → registerDynamicClient (no pasted client id/secret) → listClients
//   → start → complete mints a connection via a PUBLIC client (PKCE, no secret).
// The test authorization server's /register endpoint mints a public client when
// `token_endpoint_auth_method: "none"` is requested, and its /token endpoint
// accepts that client WITHOUT a client_secret — proving the public-client path.

const INTEG = IntegrationSlug.make("acme");
const TEMPLATE = AuthTemplateSlug.make("oauth");
const CLIENT = OAuthClientSlug.make("acme-dcr");
const FLOW_REDIRECT_URI = "https://localhost:5394/api/oauth/callback";

const dcrSlugFor = (issuerUrl: string): OAuthClientSlug =>
  OAuthClientSlug.make(
    `dcr-${new URL(issuerUrl).host
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "")}`,
  );

const registerRequestCount = (
  requests: readonly { readonly path: string; readonly method: string }[],
): number => requests.filter((r) => r.path === "/register" && r.method === "POST").length;

const oauthPlugin = definePlugin(() => ({
  id: "acme" as const,
  storage: () => ({}),
  resolveTools: () =>
    Effect.succeed({
      tools: [{ name: ToolName.make("whoami"), description: "whoami" }],
    }),
  invokeTool: ({ credential }) => Effect.succeed({ token: credential.value }),
  extension: (ctx) => ({
    seed: () =>
      ctx.core.integrations.register({
        slug: INTEG,
        description: "Acme",
        config: {},
      }),
  }),
}))();

const plugins = [memoryCredentialsPlugin(), oauthPlugin] as const;

describe("oauth.registerDynamicClient", () => {
  it.effect("DCR mints + persists a public (no-secret) client that lists + connects", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveOAuthTestServer({ scopes: ["read"] });
        const { executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();

        // Probe surfaces the registration endpoint + advertised auth methods so
        // the caller knows a public client is allowed.
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });
        expect(probe.issuer).toBe(server.issuerUrl);
        expect(probe.registrationEndpoint).toBe(server.registrationEndpoint);
        expect(probe.tokenEndpointAuthMethodsSupported).toContain("none");
        expect(probe.resource).toBe(server.mcpResourceUrl);

        // Register dynamically — NO client id/secret pasted by the user.
        const slug = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: CLIENT,
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: probe.resource,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Acme DCR",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        const expectedSlug = dcrSlugFor(server.issuerUrl);
        expect(String(slug)).toBe(String(expectedSlug));

        // The minted client appears in listClients with a server-issued
        // client_id and NO secret ever projected.
        const clients = yield* executor.oauth.listClients();
        const minted = clients.find((c) => String(c.slug) === String(expectedSlug));
        expect(minted).toBeDefined();
        expect(minted!.owner).toBe("org");
        expect(minted!.grant).toBe("authorization_code");
        expect(minted!.clientId.length).toBeGreaterThan(0);
        expect(minted!.clientId.startsWith("client_")).toBe(true);
        expect(minted!.origin).toEqual({
          kind: "dynamic_client_registration",
          integration: INTEG,
        });
        for (const client of clients) {
          expect(Object.keys(client)).not.toContain("clientSecret");
          expect(JSON.stringify(client)).not.toContain("secret");
        }

        // The DCR-minted public client drives the full authorization_code +
        // PKCE flow with NO client_secret on the token exchange.
        const started = yield* executor.oauth.start({
          owner: "org",
          client: expectedSlug,
          clientOwner: "org",
          name: ConnectionName.make("main"),
          integration: INTEG,
          template: TEMPLATE,
          redirectUri: FLOW_REDIRECT_URI,
        });
        expect(started.status).toBe("redirect");
        if (started.status !== "redirect") return;
        expect(new URL(started.authorizationUrl).searchParams.get("resource")).toBe(
          server.mcpResourceUrl,
        );

        const callback = yield* server.completeAuthorizationCodeFlow({
          authorizationUrl: started.authorizationUrl,
        });
        expect(callback.state).toBe(String(started.state));

        const connection = yield* executor.oauth.complete({
          state: started.state,
          code: callback.code,
        });
        expect(String(connection.name)).toBe("main");
        expect(String(connection.address)).toBe("tools.acme.org.main");

        // The /token request was made WITHOUT a client_secret (public client).
        const requests = yield* server.requests;
        const registerRequest = requests.find((r) => r.path === "/register" && r.method === "POST");
        expect(registerRequest).toBeDefined();
        expect(registerRequest!.body).toContain(FLOW_REDIRECT_URI);
        expect(registerRequest!.body).toContain("authorization_code");
        expect(registerRequest!.body).toContain("refresh_token");
        const authorizationRequest = requests.find(
          (r) => r.path === "/authorize" && r.method === "GET",
        );
        expect(authorizationRequest).toBeDefined();
        expect(authorizationRequest!.query.resource).toBe(server.mcpResourceUrl);
        const tokenRequest = requests.find(
          (r) => r.path === "/token" && r.method === "POST" && r.body.includes("grant_type"),
        );
        expect(tokenRequest).toBeDefined();
        expect(tokenRequest!.body).not.toContain("client_secret");
        expect(new URLSearchParams(tokenRequest!.body).get("resource")).toBe(server.mcpResourceUrl);
      }),
    ),
  );

  it.effect("reuses an existing DCR client for the same owner and authorization server", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveOAuthTestServer({ scopes: ["read"] });
        const { executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });

        const first = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("first-attempt"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: probe.resource,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Acme DCR",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        yield* server.clearRequests;

        const second = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("second-attempt"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: probe.resource,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Other integration DCR",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: IntegrationSlug.make("other"),
        });

        expect(second).toBe(first);
        const requests = yield* server.requests;
        expect(registerRequestCount(requests)).toBe(0);
      }),
    ),
  );

  it.effect("does not reuse a DCR client across owners", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveOAuthTestServer({ scopes: ["read"] });
        const { executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });

        const orgSlug = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("org-attempt"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: probe.resource,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Org DCR",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        yield* server.clearRequests;

        const userSlug = yield* executor.oauth.registerDynamicClient({
          owner: "user",
          slug: OAuthClientSlug.make("user-attempt"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: probe.resource,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "User DCR",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });

        expect(String(userSlug)).toBe(String(orgSlug));
        const requests = yield* server.requests;
        expect(registerRequestCount(requests)).toBe(1);
      }),
    ),
  );

  it.effect("does NOT reuse a legacy DCR row with no stored issuer (mints fresh)", () =>
    Effect.scoped(
      Effect.gen(function* () {
        // Post-migration, the reuse lookup keys strictly on a non-null
        // origin_issuer: the fuzzy token-host fallback is gone. A legacy row that
        // has not yet been backfilled (null origin_issuer) is therefore NOT
        // reused, so a fresh DCR registration happens. The GC migration then
        // backfills/GCs any duplicate this transient window mints.
        const server = yield* serveOAuthTestServer({ scopes: ["read"] });
        const { config, executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });
        const legacySlug = OAuthClientSlug.make("cloudflare-mcp");

        yield* executor.oauth.createClient({
          owner: "org",
          slug: legacySlug,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: server.mcpResourceUrl,
          grant: "authorization_code",
          clientId: "legacy-dcr-client",
          clientSecret: "",
        });
        yield* Effect.promise(() =>
          config.db.updateMany("oauth_client", {
            where: (b) => b("slug", "=", String(legacySlug)),
            set: { origin_kind: null, origin_integration: null, origin_issuer: null },
          }),
        );
        yield* server.clearRequests;

        const registered = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("new-attempt"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: server.mcpResourceUrl,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Acme DCR",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });

        // A fresh client is registered, not the null-issuer legacy row.
        expect(registered).not.toBe(legacySlug);
        const requests = yield* server.requests;
        expect(registerRequestCount(requests)).toBe(1);
      }),
    ),
  );

  it.effect("reuses a legacy DCR row once its origin_issuer is backfilled", () =>
    Effect.scoped(
      Effect.gen(function* () {
        // The post-backfill counterpart: after the GC migration stamps a legacy
        // row's origin_issuer, the reuse lookup keys on it and mints no
        // duplicate. This is the steady state the migration establishes.
        const server = yield* serveOAuthTestServer({ scopes: ["read"] });
        const { config, executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });
        const legacySlug = OAuthClientSlug.make("cloudflare-mcp");

        yield* executor.oauth.createClient({
          owner: "org",
          slug: legacySlug,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: server.mcpResourceUrl,
          grant: "authorization_code",
          clientId: "legacy-dcr-client",
          clientSecret: "",
        });
        // Simulate the migration's backfill: legacy DCR stamp + issuer set.
        yield* Effect.promise(() =>
          config.db.updateMany("oauth_client", {
            where: (b) => b("slug", "=", String(legacySlug)),
            set: {
              origin_kind: "dynamic_client_registration",
              origin_integration: null,
              origin_issuer: probe.issuer,
            },
          }),
        );
        yield* server.clearRequests;

        const reused = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("new-attempt"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: server.mcpResourceUrl,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Acme DCR",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });

        expect(reused).toBe(legacySlug);
        const requests = yield* server.requests;
        expect(registerRequestCount(requests)).toBe(0);
      }),
    ),
  );

  it.effect("uses resource to distinguish DCR clients only after an issuer already differs", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveOAuthTestServer({ scopes: ["read"] });
        const { executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });
        const resourceA = `${server.issuerUrl}/mcp/a`;
        const resourceB = `${server.issuerUrl}/mcp/b`;

        const first = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("first-resource"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: resourceA,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Resource A",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        expect(String(first)).toBe(String(dcrSlugFor(server.issuerUrl)));

        const second = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("second-resource"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: resourceB,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Resource B",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        expect(String(second)).not.toBe(String(first));
        expect(String(second)).toContain(String(first));
        yield* server.clearRequests;

        const third = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("third-resource"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: resourceB,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Resource B again",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: IntegrationSlug.make("other"),
        });

        expect(third).toBe(second);
        const requests = yield* server.requests;
        expect(registerRequestCount(requests)).toBe(0);
      }),
    ),
  );

  it.effect("does not reuse a resource-scoped DCR client for a resource-less request", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveOAuthTestServer({ scopes: ["read"] });
        const { executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });
        const resourceA = `${server.issuerUrl}/mcp/a`;
        const resourceB = `${server.issuerUrl}/mcp/b`;

        // Two resource-scoped clients for the same authorization server. The first
        // takes the base `dcr-<host>` slug; the second gets a resource-suffixed one.
        const scopedA = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("scoped-a"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: resourceA,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Resource A",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        const scopedB = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("scoped-b"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: resourceB,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Resource B",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        expect(String(scopedB)).not.toBe(String(scopedA));
        yield* server.clearRequests;

        // A resource-LESS request for the same owner + issuer must NOT borrow
        // either resource-scoped client (their tokens are bound to a resource):
        // it registers a fresh resource-less client.
        const resourceLess = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("resource-less"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: null,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Resource-less",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: INTEG,
        });
        expect(String(resourceLess)).not.toBe(String(scopedA));
        expect(String(resourceLess)).not.toBe(String(scopedB));
        const requests = yield* server.requests;
        expect(registerRequestCount(requests)).toBe(1);

        // The minted resource-less client is stored with a null resource, and the
        // two resource-scoped clients still exist (nothing was clobbered).
        const clients = yield* executor.oauth.listClients();
        const minted = clients.find((c) => String(c.slug) === String(resourceLess));
        expect(minted).toBeDefined();
        expect(minted!.resource ?? null).toBeNull();
        const slugs = clients.map((c) => String(c.slug));
        expect(slugs).toContain(String(scopedA));
        expect(slugs).toContain(String(scopedB));

        // A SECOND resource-less request now reuses the resource-less client.
        yield* server.clearRequests;
        const reused = yield* executor.oauth.registerDynamicClient({
          owner: "org",
          slug: OAuthClientSlug.make("resource-less-again"),
          issuer: probe.issuer,
          registrationEndpoint: probe.registrationEndpoint!,
          authorizationUrl: probe.authorizationUrl,
          tokenUrl: probe.tokenUrl,
          resource: null,
          scopes: ["read"],
          tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
          clientName: "Resource-less again",
          redirectUri: FLOW_REDIRECT_URI,
          originIntegration: IntegrationSlug.make("other"),
        });
        expect(String(reused)).toBe(String(resourceLess));
        expect(registerRequestCount(yield* server.requests)).toBe(0);
      }),
    ),
  );

  // Regression: issue #770. Vercel (and other RFC 8252-strict servers) only
  // approve loopback redirect URIs for anonymous DCR, so a hosted/tailnet/LAN
  // origin trips `invalid_redirect_uri`. The failure must explain the loopback
  // requirement and name the offending URI, not dump the raw RFC code.
  it.effect("DCR rejection of a non-loopback redirect URI yields an actionable loopback hint", () =>
    Effect.scoped(
      Effect.gen(function* () {
        const server = yield* serveOAuthTestServer({
          scopes: ["read"],
          // Mirror Vercel: approve only loopback redirect URIs for anonymous DCR.
          approveRedirectUri: (uri) =>
            uri.startsWith("http://localhost") || uri.startsWith("http://127."),
        });
        const { executor } = yield* makeTestWorkspaceHarness({ plugins });
        yield* executor.acme.seed();
        const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });

        const nonLoopback = "https://app.example.com/api/oauth/callback";
        const error = yield* Effect.flip(
          executor.oauth.registerDynamicClient({
            owner: "org",
            slug: CLIENT,
            registrationEndpoint: probe.registrationEndpoint!,
            authorizationUrl: probe.authorizationUrl,
            tokenUrl: probe.tokenUrl,
            resource: probe.resource,
            scopes: ["read"],
            tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
            clientName: "Acme DCR",
            redirectUri: nonLoopback,
            originIntegration: INTEG,
          }),
        );
        // Predicate guard narrows the union so `.message` reads off a typed failure.
        expect(Predicate.isTagged("OAuthRegisterDynamicError")(error)).toBe(true);
        const registerError = error as OAuthRegisterDynamicError;
        const message = registerError.message;
        // Names the loopback-only requirement, the localhost fix, and the URI.
        expect(message).toContain("loopback");
        expect(message).toContain("http://localhost");
        expect(message).toContain(nonLoopback);
      }),
    ),
  );

  // The loopback hint is gated on the redirect URI actually being non-loopback.
  // A server that rejects even a loopback URI keeps the generic message so we
  // never tell the user "use localhost" when they already are.
  it.effect(
    "DCR rejection of a loopback redirect URI keeps the generic message (no false hint)",
    () =>
      Effect.scoped(
        Effect.gen(function* () {
          const server = yield* serveOAuthTestServer({
            scopes: ["read"],
            approveRedirectUri: () => false, // reject every redirect URI, even loopback
          });
          const { executor } = yield* makeTestWorkspaceHarness({ plugins });
          yield* executor.acme.seed();
          const probe = yield* executor.oauth.probe({ url: server.mcpResourceUrl });

          const error = yield* Effect.flip(
            executor.oauth.registerDynamicClient({
              owner: "org",
              slug: CLIENT,
              registrationEndpoint: probe.registrationEndpoint!,
              authorizationUrl: probe.authorizationUrl,
              tokenUrl: probe.tokenUrl,
              resource: probe.resource,
              scopes: ["read"],
              tokenEndpointAuthMethodsSupported: probe.tokenEndpointAuthMethodsSupported,
              clientName: "Acme DCR",
              redirectUri: "http://127.0.0.1:5394/api/oauth/callback",
              originIntegration: INTEG,
            }),
          );
          expect(Predicate.isTagged("OAuthRegisterDynamicError")(error)).toBe(true);
          const registerError = error as OAuthRegisterDynamicError;
          const message = registerError.message;
          expect(message).toContain("Dynamic Client Registration failed: invalid_redirect_uri");
          expect(message).not.toContain("Automatic OAuth setup failed");
        }),
      ),
  );
});
