import { formatISO, subHours } from 'date-fns';
import { humanId } from 'human-id';
import z from 'zod';
import { NoopLogger } from '@hive/api/modules/shared/providers/logger';
import { createRedisClient } from '@hive/api/modules/shared/providers/redis';
import { createPostgresDatabasePool, psql } from '@hive/postgres';
import type { Report } from '../../packages/libraries/core/src/client/usage.js';
import { authenticate, userEmail } from './auth';
import {
  CreateCollectionMutation,
  CreateOperationMutation,
  DeleteCollectionMutation,
  DeleteOperationMutation,
  UpdateCollectionMutation,
  UpdateOperationMutation,
  UpdatePreflightScriptMutation,
} from './collections';
import { ensureEnv } from './env';
import {
  addAlert,
  addAlertChannel,
  assignMemberRole,
  checkSchema,
  compareToPreviousVersion,
  createCdnAccess,
  createMemberRole,
  createOrganization,
  createOrganizationAccessToken,
  createProject,
  createTarget,
  createToken,
  deleteMemberRole,
  deleteSchema,
  deleteTokens,
  fetchLatestSchema,
  fetchLatestValidSchema,
  fetchMetadataFromCDN,
  fetchSchemaFromCDN,
  fetchSupergraphFromCDN,
  fetchVersions,
  getOrganization,
  getOrganizationMembers,
  getOrganizationProjects,
  inviteToOrganization,
  joinOrganization,
  pollFor,
  publishSchema,
  readClientStats,
  readOperationBody,
  readOperationsStats,
  readTokenInfo,
  updateBaseSchema,
  updateMemberRole,
  updateTargetValidationSettings,
} from './flow';
import * as GraphQLSchema from './gql/graphql';
import {
  ProjectType,
  SchemaPolicyInput,
  TargetAccessScope,
  UpdateOrgRateLimitDocument,
} from './gql/graphql';
import { execute } from './graphql';
import { createOIDCIntegration } from './oidc-integration.js';
import {
  CreateSavedFilterMutation,
  DeleteSavedFilterMutation,
  GetSavedFilterQuery,
  GetSavedFiltersQuery,
  TrackSavedFilterViewMutation,
  UpdateSavedFilterMutation,
} from './saved-filters';
import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from './schema-policy';
import { collect, CollectedOperation, legacyCollect } from './usage';
import { generateUnique, getServiceHost, pollForEmailVerificationLink } from './utils';

function getPGConnectionString() {
  const pg = {
    user: ensureEnv('POSTGRES_USER'),
    password: ensureEnv('POSTGRES_PASSWORD'),
    host: ensureEnv('POSTGRES_HOST'),
    port: ensureEnv('POSTGRES_PORT'),
    db: ensureEnv('POSTGRES_DB'),
  };

  return `postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`;
}

function createConnectionPool() {
  return createPostgresDatabasePool({
    connectionParameters: getPGConnectionString(),
  });
}

async function createDbConnection() {
  const pool = await createConnectionPool();
  return {
    pool,
    [Symbol.asyncDispose]: async () => {
      await pool.end();
    },
  };
}

let sharedDBPoolPromise: ReturnType<typeof createDbConnection>;

export function initSeed() {
  function getPool() {
    if (!sharedDBPoolPromise) {
      sharedDBPoolPromise = createDbConnection();
    }
    return sharedDBPoolPromise.then(res => res.pool);
  }

  async function doAuthenticate(
    email: string,
    opts?: {
      oidcIntegrationId?: string;
      verifyEmail?: boolean;
    },
  ) {
    const auth = await authenticate(await getPool(), email, opts?.oidcIntegrationId);

    if (opts?.verifyEmail ?? true) {
      const pool = await getPool();
      await pool.query(psql`
        INSERT INTO "email_verifications" ("user_identity_id", "email", "verified_at")
        VALUES (${auth.supertokensUserId}, ${email}, NOW())
      `);
    }

    return auth;
  }

  async function purgeOrganizationAccessTokenById(id: string) {
    const registryAddress = await getServiceHost('server', 8082);
    const purged: { deleted: boolean } = await fetch(
      'http://' + registryAddress + '/cache/organization-access-token-cache/delete/' + id,
      {
        method: 'POST',
      },
    ).then(res => res.json());
    expect(purged.deleted).toBe(true);
  }

  return {
    pollForEmailVerificationLink,
    getPGConnectionString,
    async purgeOIDCDomains() {
      const pool = await getPool();
      await pool.query(psql`
        TRUNCATE "oidc_integration_domains"
      `);
    },
    async forgeOIDCDNSChallenge(orgSlug: string) {
      const pool = await getPool();

      const domainChallengeId = await pool
        .oneFirst(
          psql`
      SELECT "oidc_integration_domains"."id"
      FROM "oidc_integration_domains" INNER JOIN "organizations" ON "oidc_integration_domains"."organization_id" = "organizations"."id"
      WHERE "organizations"."clean_id" = ${orgSlug}
      `,
        )
        .then(z.string().parse);
      const key = `hive:oidcDomainChallenge:${domainChallengeId}`;

      const challenge = {
        id: domainChallengeId,
        recordName: `_hive-challenge`,
        // hardcoded value
        value: 'a894723a5d52a30d73790752b0169835e6f81dd77d2737dba809bee7fde39092',
      };

      const redis = createRedisClient(
        '',
        {
          host: ensureEnv('REDIS_HOST'),
          password: ensureEnv('REDIS_PASSWORD'),
          port: parseInt(ensureEnv('REDIS_PORT'), 10),
          tlsEnabled: false,
        },
        new NoopLogger(),
      );

      await redis.set(key, JSON.stringify(challenge));
      await redis.disconnect();
    },
    createDbConnection,
    authenticate: doAuthenticate,
    generateEmail: () => userEmail(generateUnique()),
    purgeOrganizationAccessTokenById,
    async createOwner(verifyEmail: boolean = true) {
      const ownerEmail = userEmail(generateUnique());
      const auth = await doAuthenticate(ownerEmail, {
        verifyEmail,
      });

      const ownerRefreshToken = auth.refresh_token;
      const ownerToken = auth.access_token;

      return {
        ownerEmail,
        ownerToken,
        ownerRefreshToken,
        async createOrg() {
          const orgSlug = generateUnique();
          const orgResult = await createOrganization({ slug: orgSlug }, ownerToken).then(r =>
            r.expectNoGraphQLErrors(),
          );

          const organization =
            orgResult.createOrganization.ok!.createdOrganizationPayload.organization;

          return {
            organization,
            async overrideOrgPlan(plan: 'PRO' | 'ENTERPRISE' | 'HOBBY') {
              const pool = await createConnectionPool();

              await pool.query(psql`
                UPDATE organizations SET plan_name = ${plan} WHERE id = ${organization.id}
              `);

              await pool.end();
            },
            async updateOrgRateLimit(newLimit: number, token = ownerToken) {
              const result = await execute({
                document: UpdateOrgRateLimitDocument,
                variables: {
                  selector: {
                    organizationSlug: organization.slug,
                  },
                  monthlyLimits: {
                    operations: newLimit,
                  },
                },
                authToken: token,
              }).then(r => r.expectNoGraphQLErrors());

              return result.updateOrgRateLimit;
            },
            async createOrganizationAccessToken(
              args: {
                permissions: Array<string>;
                resources: GraphQLSchema.ResourceAssignmentInput;
              },
              /** Override the used access token. */
              accessToken?: string,
            ) {
              const result = await createOrganizationAccessToken(
                {
                  title: 'an access token',
                  description: 'access token',
                  organization: {
                    byId: organization.id,
                  },
                  permissions: args.permissions,
                  resources: args.resources,
                },
                accessToken ?? ownerToken,
              ).then(r => r.expectNoGraphQLErrors());

              if (result.createOrganizationAccessToken.error) {
                throw new Error(result.createOrganizationAccessToken.error.message);
              }

              return result.createOrganizationAccessToken.ok!;
            },
            async setFeatureFlag(name: string, value: boolean | string[]) {
              const pool = await createConnectionPool();

              await pool.query(psql`
                UPDATE organizations SET feature_flags = ${psql.jsonb({
                  [name]: value,
                })}
                WHERE id = ${organization.id}
              `);

              await pool.end();
            },
            async setDataRetention(days: number) {
              const pool = await createConnectionPool();

              await pool.query(psql`
                UPDATE organizations SET limit_retention_days = ${days} WHERE id = ${organization.id}
              `);

              await pool.end();
            },
            async setOrganizationSchemaPolicy(policy: SchemaPolicyInput, allowOverrides: boolean) {
              const result = await execute({
                document: UpdateSchemaPolicyForOrganization,
                variables: {
                  allowOverrides,
                  selector: {
                    organizationSlug: organization.slug,
                  },
                  policy,
                },
                authToken: ownerToken,
              }).then(r => r.expectNoGraphQLErrors());

              return result.updateSchemaPolicyForOrganization;
            },
            async fetchOrganizationInfo() {
              const result = await getOrganization(organization.slug, ownerToken).then(r =>
                r.expectNoGraphQLErrors(),
              );

              return result.organization!;
            },
            async inviteMember(
              email = 'some@email.com',
              inviteToken = ownerToken,
              memberRoleId?: string,
            ) {
              const inviteResult = await inviteToOrganization(
                {
                  organization: {
                    bySelector: {
                      organizationSlug: organization.slug,
                    },
                  },
                  email,
                  memberRoleId,
                },
                inviteToken,
              ).then(r => r.expectNoGraphQLErrors());

              return inviteResult.inviteToOrganizationByEmail;
            },
            async joinMemberUsingCode(inviteCode: string, memberToken: string) {
              return await joinOrganization(inviteCode, memberToken);
            },
            async members() {
              const membersResult = await getOrganizationMembers(
                { organizationSlug: organization.slug },
                ownerToken,
              ).then(r => r.expectNoGraphQLErrors());

              const members = membersResult.organization?.members?.edges?.map(edge => edge.node);

              if (!members) {
                throw new Error(`Could not get members for org ${organization.slug}`);
              }

              return members;
            },
            /** Expires tokens  */
            async forceExpireTokens(tokenIds: string[]) {
              const pool = await createConnectionPool();
              const result = await pool.any(psql`
                UPDATE "organization_access_tokens"
                SET "expires_at" = NOW()
                WHERE id IN (${psql.join(tokenIds, psql.fragment`, `)}) AND organization_id = ${organization.id}
                RETURNING
                  "id"
              `);
              await pool.end();
              expect(result.length).toBe(tokenIds.length);
              for (const id of tokenIds) {
                await purgeOrganizationAccessTokenById(id);
              }
            },
            async projects(token = ownerToken) {
              const projectsResult = await getOrganizationProjects(
                { organizationSlug: organization.slug },
                token,
              ).then(r => r.expectNoGraphQLErrors());

              const projects = projectsResult.organization?.projects.edges.map(edge => edge.node);

              if (!projects) {
                throw new Error(`Could not get projects for org ${organization.slug}`);
              }

              return projects;
            },
            async createProject(projectType: ProjectType = ProjectType.Single) {
              const projectResult = await createProject(
                {
                  organization: {
                    bySelector: {
                      organizationSlug: organization.slug,
                    },
                  },
                  type: projectType,
                  slug: generateUnique(),
                },
                ownerToken,
              ).then(r => r.expectNoGraphQLErrors());

              const targets = projectResult.createProject.ok!.createdTargets;
              const target = targets[0];
              const project = projectResult.createProject.ok!.createdProject;

              return {
                project,
                targets,
                target,
                async setNativeFederation(enabled: boolean) {
                  const pool = await createConnectionPool();

                  await pool.query(psql`
                    UPDATE projects SET native_federation = ${enabled} WHERE id = ${project.id}
                  `);

                  await pool.end();
                },
                async setProjectSchemaPolicy(policy: SchemaPolicyInput) {
                  const result = await execute({
                    document: UpdateSchemaPolicyForProject,
                    variables: {
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                      },
                      policy,
                    },
                    authToken: ownerToken,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.updateSchemaPolicyForProject;
                },
                async removeTokens(tokenIds: string[]) {
                  return await deleteTokens(
                    {
                      organizationSlug: organization.slug,
                      projectSlug: project.slug,
                      targetSlug: target.slug,
                      tokenIds,
                    },
                    ownerToken,
                  )
                    .then(r => r.expectNoGraphQLErrors())
                    .then(r => r.deleteTokens.deletedTokens);
                },
                async createDocumentCollection({
                  name,
                  description,
                  token = ownerToken,
                }: {
                  name: string;
                  description: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: CreateCollectionMutation,
                    variables: {
                      input: {
                        name,
                        description,
                      },
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.createDocumentCollection;
                },
                async updatePreflightScript({
                  sourceCode,
                  token = ownerToken,
                }: {
                  sourceCode: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: UpdatePreflightScriptMutation,
                    variables: {
                      input: {
                        selector: {
                          organizationSlug: organization.slug,
                          projectSlug: project.slug,
                          targetSlug: target.slug,
                        },
                        sourceCode,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.updatePreflightScript;
                },
                async updateDocumentCollection({
                  collectionId,
                  name,
                  description,
                  token = ownerToken,
                }: {
                  collectionId: string;
                  name: string;
                  description: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: UpdateCollectionMutation,
                    variables: {
                      input: {
                        collectionId,
                        name,
                        description,
                      },
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.updateDocumentCollection;
                },
                async deleteDocumentCollection({
                  collectionId,
                  token = ownerToken,
                }: {
                  collectionId: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: DeleteCollectionMutation,
                    variables: {
                      id: collectionId,
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.deleteDocumentCollection;
                },
                async createOperationInCollection(input: {
                  collectionId: string;
                  name: string;
                  query: string;
                  variables?: string;
                  headers?: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: CreateOperationMutation,
                    variables: {
                      input: {
                        collectionId: input.collectionId,
                        name: input.name,
                        query: input.query,
                        headers: input.headers,
                        variables: input.variables,
                      },
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: input.token || ownerToken,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.createOperationInDocumentCollection;
                },
                async deleteOperationInCollection(input: { operationId: string; token?: string }) {
                  const result = await execute({
                    document: DeleteOperationMutation,
                    variables: {
                      id: input.operationId,
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: input.token || ownerToken,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.deleteOperationInDocumentCollection;
                },
                async updateOperationInCollection(input: {
                  operationId: string;
                  collectionId: string;
                  name: string;
                  query: string;
                  variables?: string;
                  headers?: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: UpdateOperationMutation,
                    variables: {
                      input: {
                        operationId: input.operationId,
                        collectionId: input.collectionId,
                        name: input.name,
                        query: input.query,
                        headers: input.headers,
                        variables: input.variables,
                      },
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: input.token || ownerToken,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.updateOperationInDocumentCollection;
                },
                async getSavedFilter({
                  filterId,
                  token = ownerToken,
                }: {
                  filterId: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: GetSavedFilterQuery,
                    variables: {
                      id: filterId,
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.target?.savedFilter;
                },
                async getSavedFilters({
                  first = 20,
                  after,
                  visibility,
                  search,
                  token = ownerToken,
                }: {
                  first?: number;
                  after?: string;
                  visibility?: GraphQLSchema.SavedFilterVisibilityType;
                  search?: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: GetSavedFiltersQuery,
                    variables: {
                      first,
                      after,
                      visibility,
                      search,
                      selector: {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: target.slug,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return {
                    savedFilters: result.target?.savedFilters,
                    viewerCanCreateSavedFilter: result.target?.viewerCanCreateSavedFilter,
                  };
                },
                async createSavedFilter({
                  name,
                  description,
                  visibility,
                  insightsFilter,
                  token = ownerToken,
                }: {
                  name: string;
                  description?: string;
                  visibility: GraphQLSchema.SavedFilterVisibilityType;
                  insightsFilter?: GraphQLSchema.InsightsFilterConfigurationInput;
                  token?: string;
                }) {
                  const result = await execute({
                    document: CreateSavedFilterMutation,
                    variables: {
                      input: {
                        target: {
                          bySelector: {
                            organizationSlug: organization.slug,
                            projectSlug: project.slug,
                            targetSlug: target.slug,
                          },
                        },
                        name,
                        description,
                        visibility,
                        insightsFilter,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.createSavedFilter;
                },
                async updateSavedFilter({
                  filterId,
                  name,
                  description,
                  visibility,
                  insightsFilter,
                  token = ownerToken,
                }: {
                  filterId: string;
                  name?: string;
                  description?: string;
                  visibility?: GraphQLSchema.SavedFilterVisibilityType;
                  insightsFilter?: GraphQLSchema.InsightsFilterConfigurationInput;
                  token?: string;
                }) {
                  const result = await execute({
                    document: UpdateSavedFilterMutation,
                    variables: {
                      input: {
                        target: {
                          bySelector: {
                            organizationSlug: organization.slug,
                            projectSlug: project.slug,
                            targetSlug: target.slug,
                          },
                        },
                        id: filterId,
                        name,
                        description,
                        visibility,
                        insightsFilter,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.updateSavedFilter;
                },
                async deleteSavedFilter({
                  filterId,
                  token = ownerToken,
                }: {
                  filterId: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: DeleteSavedFilterMutation,
                    variables: {
                      input: {
                        target: {
                          bySelector: {
                            organizationSlug: organization.slug,
                            projectSlug: project.slug,
                            targetSlug: target.slug,
                          },
                        },
                        id: filterId,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.deleteSavedFilter;
                },
                async trackSavedFilterView({
                  filterId,
                  token = ownerToken,
                }: {
                  filterId: string;
                  token?: string;
                }) {
                  const result = await execute({
                    document: TrackSavedFilterViewMutation,
                    variables: {
                      input: {
                        target: {
                          bySelector: {
                            organizationSlug: organization.slug,
                            projectSlug: project.slug,
                            targetSlug: target.slug,
                          },
                        },
                        id: filterId,
                      },
                    },
                    authToken: token,
                  }).then(r => r.expectNoGraphQLErrors());

                  return result.trackSavedFilterView;
                },
                async addAlert(
                  input: {
                    token?: string;
                  } & Parameters<typeof addAlert>[0],
                ) {
                  const result = await addAlert(input, input.token || ownerToken).then(r =>
                    r.expectNoGraphQLErrors(),
                  );
                  return result.addAlert;
                },
                async addAlertChannel(
                  input: { token?: string } & Parameters<typeof addAlertChannel>[0],
                ) {
                  const result = await addAlertChannel(input, input.token || ownerToken).then(r =>
                    r.expectNoGraphQLErrors(),
                  );
                  return result.addAlertChannel;
                },
                /**
                 * Create an access token for a given target.
                 * This token can be used for usage reporting and all actions that would be performed by the CLI.
                 */
                async createTargetAccessToken({
                  mode = 'readWrite',
                  target: forTarget = {
                    slug: target.slug,
                    id: target.id,
                  },
                  actorToken = ownerToken,
                }: {
                  mode?: 'readWrite' | 'readOnly' | 'noAccess';
                  target?: {
                    slug: string;
                    id: string;
                  };
                  actorToken?: string;
                }) {
                  const target = forTarget;

                  const tokenResult = await createToken(
                    {
                      name: generateUnique(),
                      organizationSlug: organization.slug,
                      projectSlug: project.slug,
                      targetSlug: target.slug,
                      organizationScopes: [],
                      projectScopes: [],
                      targetScopes:
                        mode === 'noAccess'
                          ? []
                          : mode === 'readWrite'
                            ? [TargetAccessScope.RegistryRead, TargetAccessScope.RegistryWrite]
                            : [TargetAccessScope.RegistryRead],
                    },
                    actorToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  const secret = tokenResult.createToken.ok!.secret;
                  const token = tokenResult.createToken.ok!.createdToken;

                  return {
                    token,
                    secret,
                    async collectLegacyOperations(
                      operations: CollectedOperation[],
                      headerName: 'x-api-token' | 'authorization' = 'authorization',
                    ) {
                      return await legacyCollect({
                        operations,
                        token: secret,
                        authorizationHeader: headerName,
                      });
                    },
                    collectUsage(report: Report) {
                      return collect({
                        report,
                        accessToken: secret,
                      });
                    },
                    async checkSchema(
                      sdl: string,
                      service?: string,
                      meta?: {
                        author: string;
                        commit: string;
                      },
                      contextId?: string,
                      schemaProposalId?: string,
                    ) {
                      return await checkSchema(
                        {
                          sdl,
                          service,
                          meta,
                          contextId,
                          schemaProposalId,
                        },
                        secret,
                      );
                    },
                    async publishSchema(options: {
                      sdl: string;
                      headerName?: 'x-api-token' | 'authorization';
                      author?: string;
                      force?: boolean;
                      experimental_acceptBreakingChanges?: boolean;
                      commit?: string;
                      service?: string;
                      url?: string;
                      metadata?: string;
                      /**
                       * @deprecated
                       */
                      github?: boolean | null;
                    }) {
                      return await publishSchema(
                        {
                          author: options.author || 'Kamil',
                          commit: options.commit || 'test',
                          sdl: options.sdl,
                          service: options.service,
                          url: options.url,
                          force: options.force,
                          metadata: options.metadata,
                          experimental_acceptBreakingChanges:
                            options.experimental_acceptBreakingChanges,
                          github: options.github,
                        },
                        secret,
                        options.headerName || 'authorization',
                      );
                    },
                    async deleteSchema(serviceName: string) {
                      return await deleteSchema(
                        {
                          serviceName,
                          dryRun: false,
                        },
                        secret,
                      );
                    },
                    async latestSchema() {
                      return (await fetchLatestSchema(secret)).expectNoGraphQLErrors();
                    },
                    async fetchLatestValidSchema() {
                      return (await fetchLatestValidSchema(secret)).expectNoGraphQLErrors();
                    },
                    async fetchTokenInfo() {
                      const tokenInfoResult = await readTokenInfo(secret).then(r =>
                        r.expectNoGraphQLErrors(),
                      );

                      return tokenInfoResult.tokenInfo;
                    },
                  };
                },
                async createCdnAccess(ttarget: TargetOverwrite = target) {
                  const result = await createCdnAccess(
                    {
                      organizationSlug: organization.slug,
                      projectSlug: project.slug,
                      targetSlug: ttarget.slug,
                    },
                    ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());
                  expect(result.createCdnAccessToken.ok).not.toBeNull();

                  const data = result.createCdnAccessToken.ok!;

                  return {
                    secretAccessToken: data.secretAccessToken,
                    cdnUrl: data.cdnUrl,
                    fetchSchemaFromCDN() {
                      return fetchSchemaFromCDN(data.cdnUrl, data.secretAccessToken);
                    },
                    fetchMetadataFromCDN() {
                      return fetchMetadataFromCDN(data.cdnUrl, data.secretAccessToken);
                    },
                    fetchSupergraphFromCDN() {
                      return fetchSupergraphFromCDN(data.cdnUrl, data.secretAccessToken);
                    },
                  };
                },
                async toggleTargetValidation(
                  isEnabled: boolean,
                  ttarget: TargetOverwrite = target,
                ) {
                  const result = await updateTargetValidationSettings(
                    {
                      target: {
                        bySelector: {
                          organizationSlug: organization.slug,
                          projectSlug: project.slug,
                          targetSlug: ttarget.slug,
                        },
                      },
                      conditionalBreakingChangeConfiguration: {
                        isEnabled,
                      },
                    },
                    {
                      token: ownerToken,
                    },
                  ).then(r => r.expectNoGraphQLErrors());

                  return result.updateTargetConditionalBreakingChangeConfiguration.ok!.target
                    .conditionalBreakingChangeConfiguration;
                },
                async updateTargetValidationSettings({
                  isEnabled,
                  excludedClients,
                  excludedAppDeployments,
                  percentage,
                  target: ttarget = target,
                  requestCount,
                  breakingChangeFormula,
                }: GraphQLSchema.ConditionalBreakingChangeConfigurationInput & {
                  target?: TargetOverwrite;
                }) {
                  const result = await updateTargetValidationSettings(
                    {
                      target: {
                        bySelector: {
                          organizationSlug: organization.slug,
                          projectSlug: project.slug,
                          targetSlug: ttarget.slug,
                        },
                      },
                      conditionalBreakingChangeConfiguration: {
                        isEnabled,
                        excludedClients,
                        excludedAppDeployments,
                        percentage,
                        requestCount,
                        breakingChangeFormula,
                        period: 2,
                        targetIds: [target.id],
                      },
                    },
                    {
                      token: ownerToken,
                    },
                  ).then(r => r.expectNoGraphQLErrors());

                  return result.updateTargetConditionalBreakingChangeConfiguration;
                },
                async compareToPreviousVersion(version: string, ttarget: TargetOverwrite = target) {
                  return (
                    await compareToPreviousVersion(
                      {
                        organizationSlug: organization.slug,
                        projectSlug: project.slug,
                        targetSlug: ttarget.slug,
                        version,
                      },
                      ownerToken,
                    )
                  ).expectNoGraphQLErrors();
                },
                async readOperationBody(hash: string, ttarget: TargetOverwrite = target) {
                  const operationBodyResult = await readOperationBody(
                    {
                      organizationSlug: organization.slug,
                      projectSlug: project.slug,
                      targetSlug: ttarget.slug,
                      hash,
                    },
                    ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  return operationBodyResult?.target?.operation?.body;
                },
                async waitForOperationsCollected(
                  n: number,
                  _from?: number,
                  _to?: number,
                  ttarget: TargetOverwrite = target,
                ) {
                  const from = formatISO(_from ?? subHours(Date.now(), 1));
                  const to = formatISO(_to ?? Date.now());
                  const check = async () => {
                    const statsResult = await readOperationsStats(
                      {
                        bySelector: {
                          organizationSlug: organization.slug,
                          projectSlug: project.slug,
                          targetSlug: ttarget.slug,
                        },
                      },
                      {
                        from,
                        to,
                      },
                      {},
                      ownerToken,
                    ).then(r => r.expectNoGraphQLErrors());
                    return statsResult.target?.operationsStats.totalOperations == n;
                  };

                  return pollFor(check);
                },
                async waitForRequestsCollected(
                  n: number,
                  opts?: {
                    from?: number;
                    to?: number;
                    target?: TargetOverwrite;
                  },
                ) {
                  const from = formatISO(opts?.from ?? subHours(Date.now(), 1));
                  const to = formatISO(opts?.to ?? Date.now());
                  const check = async () => {
                    const statsResult = await readOperationsStats(
                      {
                        bySelector: {
                          organizationSlug: organization.slug,
                          projectSlug: project.slug,
                          targetSlug: (opts?.target ?? target).slug,
                        },
                      },
                      {
                        from,
                        to,
                      },
                      {},
                      ownerToken,
                    ).then(r => r.expectNoGraphQLErrors());
                    const totalRequests =
                      statsResult.target?.operationsStats.operations.edges.reduce(
                        (total, edge) => total + edge.node.count,
                        0,
                      );
                    return totalRequests == n;
                  };

                  return pollFor(check);
                },
                async readOperationsStats(
                  from: string,
                  to: string,
                  ttarget: TargetOverwrite = target,
                  filter: GraphQLSchema.OperationStatsFilterInput = {},
                ) {
                  const statsResult = await readOperationsStats(
                    { byId: ttarget.id },
                    {
                      from,
                      to,
                    },
                    filter,
                    ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  return statsResult.target?.operationsStats!;
                },
                async readClientStats(params: { clientName: string; from: string; to: string }) {
                  const statsResult = await readClientStats(
                    {
                      byId: target.id,
                    },
                    {
                      from: params.from,
                      to: params.to,
                    },
                    params.clientName,
                    ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  return statsResult.target?.clientStats!;
                },
                async updateBaseSchema(newBase: string, ttarget: TargetOverwrite = target) {
                  const result = await updateBaseSchema(
                    {
                      newBase,
                      organizationSlug: organization.slug,
                      projectSlug: project.slug,
                      targetSlug: ttarget.slug,
                    },
                    ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  return result.updateBaseSchema;
                },
                async fetchVersions(count: number, ttarget: TargetOverwrite = target) {
                  const result = await fetchVersions(
                    {
                      organizationSlug: organization.slug,
                      projectSlug: project.slug,
                      targetSlug: ttarget.slug,
                    },
                    count,
                    ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  if (!result.target) {
                    throw new Error('Could not find target');
                  }

                  return result.target?.schemaVersions.edges.map(edge => edge.node);
                },
                async createTarget(args?: { slug?: string; accessToken?: string }) {
                  return createTarget(
                    {
                      project: {
                        bySelector: {
                          organizationSlug: orgSlug,
                          projectSlug: project.slug,
                        },
                      },
                      slug: args?.slug ?? generateUnique(),
                    },
                    args?.accessToken ?? ownerToken,
                  );
                },
              };
            },
            async inviteAndJoinMember(options?: {
              inviteToken?: string;
              memberRoleId?: string | undefined;
              oidcIntegrationId?: string | undefined;
              resources?: GraphQLSchema.ResourceAssignmentInput | undefined;
            }) {
              const { inviteToken, memberRoleId, oidcIntegrationId, resources } = Object.assign(
                options ?? {},
                {
                  inviteToken: ownerToken,
                },
              );
              const memberEmail = userEmail(generateUnique());
              const memberToken = await doAuthenticate(memberEmail, {
                oidcIntegrationId,
                verifyEmail: true,
              }).then(r => r.access_token);

              if (!oidcIntegrationId) {
                const invitationResult = await inviteToOrganization(
                  {
                    organization: {
                      bySelector: {
                        organizationSlug: organization.slug,
                      },
                    },
                    email: memberEmail,
                    memberRoleId,
                    resources,
                  },
                  inviteToken,
                ).then(r => r.expectNoGraphQLErrors());
                const code =
                  invitationResult.inviteToOrganizationByEmail.ok?.createdOrganizationInvitation
                    .code;

                if (!code) {
                  throw new Error(
                    `Could not create invitation for ${memberEmail} to join org ${organization.slug}`,
                  );
                }

                const joinResult = await joinOrganization(code, memberToken).then(r =>
                  r.expectNoGraphQLErrors(),
                );

                if (joinResult.joinOrganization.__typename !== 'OrganizationPayload') {
                  throw new Error(
                    `Member ${memberEmail} could not join organization ${organization.slug}`,
                  );
                }
              }

              const orgAfterJoin = await getOrganization(organization.slug, memberToken).then(r =>
                r.expectNoGraphQLErrors(),
              );
              const member = orgAfterJoin.organization?.me;

              if (!member) {
                throw new Error(
                  `Could not retrieve membership for ${memberEmail} in ${organization.slug} after joining`,
                );
              }

              return {
                member,
                memberEmail,
                memberToken,
                async assignMemberRole(
                  input: {
                    roleId: string;
                    userId: string;
                    resources?: GraphQLSchema.ResourceAssignmentInput;
                  },
                  options: { useMemberToken?: boolean } = {
                    useMemberToken: false,
                  },
                ) {
                  const memberRoleAssignmentResult = await assignMemberRole(
                    {
                      organization: {
                        bySelector: {
                          organizationSlug: organization.slug,
                        },
                      },
                      member: {
                        byId: input.userId,
                      },
                      memberRole: {
                        byId: input.roleId,
                      },
                      resources: input.resources ?? {
                        mode: GraphQLSchema.ResourceAssignmentModeType.All,
                        projects: [],
                      },
                    },
                    options.useMemberToken ? memberToken : ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  if (memberRoleAssignmentResult.assignMemberRole.error) {
                    throw new Error(memberRoleAssignmentResult.assignMemberRole.error.message);
                  }

                  return memberRoleAssignmentResult.assignMemberRole.ok?.updatedMember;
                },
                async deleteMemberRole(
                  memberRoleId: string,
                  options: { useMemberToken?: boolean } = {
                    useMemberToken: false,
                  },
                ) {
                  const memberRoleDeletionResult = await deleteMemberRole(
                    {
                      memberRole: {
                        byId: memberRoleId,
                      },
                    },
                    options.useMemberToken ? memberToken : ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  if (memberRoleDeletionResult.deleteMemberRole.error) {
                    throw new Error(memberRoleDeletionResult.deleteMemberRole.error.message);
                  }

                  return memberRoleDeletionResult.deleteMemberRole.ok?.updatedOrganization;
                },
                async createMemberRole(
                  permissions: Array<string>,
                  options: { useMemberToken?: boolean } = {
                    useMemberToken: false,
                  },
                ) {
                  const name = humanId({
                    separator: '',
                    adjectiveCount: 1,
                    addAdverb: true,
                    capitalize: true,
                  });
                  const memberRoleCreationResult = await createMemberRole(
                    {
                      organization: {
                        bySelector: {
                          organizationSlug: organization.slug,
                        },
                      },
                      name,
                      description: 'some description',
                      selectedPermissions: permissions,
                    },
                    options.useMemberToken ? memberToken : ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  if (memberRoleCreationResult.createMemberRole.error) {
                    if (memberRoleCreationResult.createMemberRole.error.inputErrors?.name) {
                      throw new Error(
                        memberRoleCreationResult.createMemberRole.error.inputErrors.name,
                      );
                    }
                    if (memberRoleCreationResult.createMemberRole.error.inputErrors?.description) {
                      throw new Error(
                        memberRoleCreationResult.createMemberRole.error.inputErrors.description,
                      );
                    }

                    throw new Error(memberRoleCreationResult.createMemberRole.error.message);
                  }

                  const createdRole =
                    memberRoleCreationResult.createMemberRole.ok?.updatedOrganization.memberRoles?.edges.find(
                      e => e.node.name === name,
                    )?.node;

                  if (!createdRole) {
                    throw new Error(
                      `Could not find created member role for org ${organization.slug}`,
                    );
                  }

                  return createdRole;
                },
                async updateMemberRole(
                  role: {
                    id: string;
                    name: string;
                    description: string;
                  },
                  permissions: Array<string>,
                  options: { useMemberToken?: boolean } = {
                    useMemberToken: false,
                  },
                ) {
                  const memberRoleUpdateResult = await updateMemberRole(
                    {
                      memberRole: {
                        byId: role.id,
                      },
                      name: role.name,
                      description: role.description,
                      selectedPermissions: permissions,
                    },
                    options.useMemberToken ? memberToken : ownerToken,
                  ).then(r => r.expectNoGraphQLErrors());

                  if (memberRoleUpdateResult.updateMemberRole.error) {
                    if (memberRoleUpdateResult.updateMemberRole.error.inputErrors?.name) {
                      throw new Error(
                        memberRoleUpdateResult.updateMemberRole.error.inputErrors.name,
                      );
                    }
                    if (memberRoleUpdateResult.updateMemberRole.error.inputErrors?.description) {
                      throw new Error(
                        memberRoleUpdateResult.updateMemberRole.error.inputErrors.description,
                      );
                    }

                    throw new Error(memberRoleUpdateResult.updateMemberRole.error.message);
                  }

                  const updatedRole = memberRoleUpdateResult.updateMemberRole.ok?.updatedRole;

                  if (!updatedRole) {
                    throw new Error(
                      `Could not find the updated member role for org ${organization.slug}`,
                    );
                  }

                  return updatedRole;
                },
              };
            },
            createOIDCIntegration() {
              return createOIDCIntegration({
                organizationId: organization.id,
                accessToken: ownerToken,
                getPool: getPool,
              });
            },
          };
        },
      };
    },
  };
}

type TargetOverwrite = {
  __typename?: 'Target';
  id: string;
  slug: string;
};
