import { buildASTSchema, parse } from 'graphql';
import { createLogger } from 'graphql-yoga';
import { waitFor } from 'testkit/flow';
import { initSeed } from 'testkit/seed';
import { getServiceHost } from 'testkit/utils';
import { createHive } from '@graphql-hive/core';
import { graphql } from '../../testkit/gql';
import { execute } from '../../testkit/graphql';

const CreateAppDeployment = graphql(`
  mutation CreateAppDeployment($input: CreateAppDeploymentInput!) {
    createAppDeployment(input: $input) {
      error {
        message
        details {
          appName
          appVersion
        }
      }
      ok {
        createdAppDeployment {
          id
          name
          version
          status
        }
      }
    }
  }
`);

const GetAppDeployment = graphql(`
  query GetAppDeployment(
    $targetSelector: TargetSelectorInput!
    $appDeploymentName: String!
    $appDeploymentVersion: String!
  ) {
    target(selector: $targetSelector) {
      appDeployment(appName: $appDeploymentName, appVersion: $appDeploymentVersion) {
        id
        lastUsed
      }
    }
  }
`);

const AddDocumentsToAppDeployment = graphql(`
  mutation AddDocumentsToAppDeployment($input: AddDocumentsToAppDeploymentInput!) {
    addDocumentsToAppDeployment(input: $input) {
      error {
        message
        details {
          index
          message
        }
      }
      ok {
        appDeployment {
          id
          name
          version
          status
        }
      }
    }
  }
`);

const ActivateAppDeployment = graphql(`
  mutation ActivateAppDeployment($input: ActivateAppDeploymentInput!) {
    activateAppDeployment(input: $input) {
      error {
        message
      }
      ok {
        isSkipped
        activatedAppDeployment {
          id
          name
          version
          status
        }
      }
    }
  }
`);

const RetireAppDeployment = graphql(`
  mutation RetireAppDeployment($input: RetireAppDeploymentInput!) {
    retireAppDeployment(input: $input) {
      error {
        message
      }

      ok {
        retiredAppDeployment {
          id
          name
          version
          status
        }
      }
    }
  }
`);

const GetPaginatedPersistedDocuments = graphql(`
  query GetPaginatedPersistedDocuments(
    $targetSelector: TargetSelectorInput!
    $appDeploymentName: String!
    $appDeploymentVersion: String!
    $first: Int
    $cursor: String
  ) {
    target(selector: $targetSelector) {
      appDeployment(appName: $appDeploymentName, appVersion: $appDeploymentVersion) {
        id
        documents(first: $first, after: $cursor) {
          edges {
            cursor
            node {
              hash
              body
            }
          }
          pageInfo {
            hasNextPage
          }
        }
      }
    }
  }
`);

test('create app deployment, add operations, publish, access via CDN (happy path)', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, createCdnAccess } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const cdnAccess = await createCdnAccess();

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'query { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: null,
    ok: {
      appDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { activateAppDeployment } = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(activateAppDeployment).toEqual({
    error: null,
    ok: {
      activatedAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'active',
      },
      isSkipped: false,
    },
  });

  const persistedOperationUrl = `${cdnAccess.cdnUrl}/apps/my-app/1.0.0/hash`;
  const response = await fetch(persistedOperationUrl, {
    method: 'GET',
    headers: {
      'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
    },
  });
  const txt = await response.text();
  expect(txt).toEqual('query { hello }');
  expect(response.status).toBe(200);
});

test('create app deployment with same name and version succeed if deployment is not active', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  let createAppDeployment = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  })
    .then(res => res.expectNoGraphQLErrors())
    .then(res => res.createAppDeployment);

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  createAppDeployment = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  })
    .then(res => res.expectNoGraphQLErrors())
    .then(res => res.createAppDeployment);

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });
});

test('create app deployment with same name and version does not fail if deployment is active', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  let createAppDeployment = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  })
    .then(res => res.expectNoGraphQLErrors())
    .then(res => res.createAppDeployment);

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { activateAppDeployment } = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(activateAppDeployment).toEqual({
    error: null,
    ok: {
      activatedAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'active',
      },
      isSkipped: false,
    },
  });

  createAppDeployment = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  })
    .then(res => res.expectNoGraphQLErrors())
    .then(res => res.createAppDeployment);

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'active',
      },
    },
  });
});

test('create app deployment fails if app name is empty', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: '',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: {
      details: {
        appName: 'Must be at least 1 character long',
        appVersion: null,
      },
      message: 'Invalid input',
    },
    ok: null,
  });
});

test('create app deployment fails if app name exceeds length of 256 characters', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: new Array(257).fill('a').join(''),
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: {
      details: {
        appName: 'Must be at most 64 characters long',
        appVersion: null,
      },
      message: 'Invalid input',
    },
    ok: null,
  });
});

test('create app deployment fails if app version is empty', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'myapp',
        appVersion: '',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: {
      details: {
        appName: null,
        appVersion: 'Must be at least 1 character long',
      },
      message: 'Invalid input',
    },
    ok: null,
  });
});

test('create app deployment fails if app version exceeds length of 256 characters', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: new Array(257).fill('a').join(''),
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: {
      details: {
        appName: null,
        appVersion: 'Must be at most 64 characters long',
      },
      message: 'Invalid input',
    },
    ok: null,
  });
});

test('create app deployment fails without feature flag enabled for organization', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject } = await createOrg();
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: {
      details: null,
      message:
        'This organization has no access to app deployments. Please contact the Hive team for early access.',
    },
    ok: null,
  });
});

test('add documents to app deployment fails if there is no initial schema published', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'query { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: null,
      message: 'No schema has been published yet',
    },
    ok: null,
  });
});

test('add documents to app deployment fails if document hash is less than 1 character', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: '',
            body: 'query { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: {
        index: 0,
        message: 'Hash must be at least 1 characters long',
      },
      message: 'Invalid input, please check the operations.',
    },
    ok: null,
  });
});

test('add documents to app deployment fails if document hash is longer than 256 characters', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: new Array(129).fill('a').join(''),
            body: 'query { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: {
        index: 0,
        message: 'Hash must be at most 128 characters long',
      },
      message: 'Invalid input, please check the operations.',
    },
    ok: null,
  });
});

test('add documents to app deployment fails if document is not parse-able', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'qugu',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: {
        index: 0,
        message: 'Syntax Error: Unexpected Name "qugu".',
      },
      message: 'Failed to parse a GraphQL operation.',
    },
    ok: null,
  });
});

test('add documents to app deployment fails if document does not pass validation against the target schema', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'query { hi }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: {
        index: 0,
        message: 'Cannot query field "hi" on type "Query".',
      },
      message: 'The GraphQL operation is not valid against the latest schema version.',
    },
    ok: null,
  });
});

test('add documents to app deployment fails if document contains multiple executable operation definitions', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(createAppDeployment).toEqual({
    error: null,
    ok: {
      createdAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        version: '1.0.0',
        status: 'pending',
      },
    },
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'query a { hello } query b { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: {
        index: 0,
        message:
          'Multiple operation definitions found. Only one executable operation definition is allowed per document.',
      },
      message: 'Only one executable operation definition is allowed per document.',
    },
    ok: null,
  });
});

test('add documents to app deployment fails if app deployment does not exist', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'query { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: null,
      message: 'App deployment not found',
    },
    ok: null,
  });
});

test('add documents to app deployment fails without feature flag enabled for organization', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject } = await createOrg();
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'query { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(addDocumentsToAppDeployment).toEqual({
    error: {
      details: null,
      message:
        'This organization has no access to app deployments. Please contact the Hive team for early access.',
    },
    ok: null,
  });
});

test('activate app deployment fails if app deployment does not exist', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken } = await createProject();
  const token = await createTargetAccessToken({});

  const { activateAppDeployment } = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(activateAppDeployment).toEqual({
    error: {
      message: 'App deployment not found',
    },
    ok: null,
  });
});

test('activate app deployment succeeds if app deployment is already active', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, target } = await createProject();
  const token = await createTargetAccessToken({});

  await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  let activateResult = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(activateResult).toEqual({
    activateAppDeployment: {
      error: null,
      ok: {
        activatedAppDeployment: {
          id: expect.any(String),
          name: 'my-app',
          status: 'active',
          version: '1.0.0',
        },
        isSkipped: false,
      },
    },
  });

  activateResult = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(activateResult).toEqual({
    activateAppDeployment: {
      error: null,
      ok: {
        activatedAppDeployment: {
          id: expect.any(String),
          name: 'my-app',
          status: 'active',
          version: '1.0.0',
        },
        isSkipped: true,
      },
    },
  });
});

test('activate app deployment fails if app deployment is retired', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, target } = await createProject();
  const token = await createTargetAccessToken({});

  await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  let activateResult = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(activateResult).toEqual({
    activateAppDeployment: {
      error: null,
      ok: {
        activatedAppDeployment: {
          id: expect.any(String),
          name: 'my-app',
          status: 'active',
          version: '1.0.0',
        },
        isSkipped: false,
      },
    },
  });

  const retireResult = await execute({
    document: RetireAppDeployment,
    variables: {
      input: {
        targetId: target.id,
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(retireResult).toEqual({
    retireAppDeployment: {
      error: null,
      ok: {
        retiredAppDeployment: {
          id: expect.any(String),
          name: 'my-app',
          status: 'active',
          version: '1.0.0',
        },
      },
    },
  });

  activateResult = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(activateResult).toEqual({
    activateAppDeployment: {
      error: {
        message: 'App deployment is retired',
      },
      ok: null,
    },
  });
});

test('retire app deployment fails if app deployment does not exist', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, target } = await createProject();
  const token = await createTargetAccessToken({});

  const { retireAppDeployment } = await execute({
    document: RetireAppDeployment,
    variables: {
      input: {
        targetId: target.id,
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(retireAppDeployment).toEqual({
    error: {
      message: 'App deployment not found',
    },
    ok: null,
  });
});

test('retire app deployment fails if app deployment is pending (not active)', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, target } = await createProject();
  const token = await createTargetAccessToken({});

  await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  const { retireAppDeployment } = await execute({
    document: RetireAppDeployment,
    variables: {
      input: {
        targetId: target.id,
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(retireAppDeployment).toEqual({
    error: {
      message: 'App deployment is not active',
    },
    ok: null,
  });
});

test('retire app deployment succeeds if app deployment is active', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, target } = await createProject();
  const token = await createTargetAccessToken({});

  await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  const { retireAppDeployment } = await execute({
    document: RetireAppDeployment,
    variables: {
      input: {
        targetId: target.id,
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(retireAppDeployment).toEqual({
    error: null,
    ok: {
      retiredAppDeployment: {
        id: expect.any(String),
        name: 'my-app',
        status: 'active',
        version: '1.0.0',
      },
    },
  });
});

test('retire app deployments makes the persisted operations unavailable via CDN', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject, setFeatureFlag } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, createCdnAccess, target } = await createProject();
  const token = await createTargetAccessToken({});

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        hello: String
      }
    `,
  });

  const cdnAccess = await createCdnAccess();

  await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
        documents: [
          {
            hash: 'hash',
            body: 'query { hello }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  const persistedOperationUrl = `${cdnAccess.cdnUrl}/apps/my-app/1.0.0/hash`;
  let response = await fetch(persistedOperationUrl, {
    method: 'GET',
    headers: {
      'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
    },
  });

  expect(response.status).toBe(200);

  await execute({
    document: RetireAppDeployment,
    variables: {
      input: {
        targetId: target.id,
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  response = await fetch(persistedOperationUrl, {
    method: 'GET',
    headers: {
      'X-Hive-CDN-Key': cdnAccess.secretAccessToken,
    },
  });

  expect(response.status).toBe(404);
});

test('retire app deployments fails without feature flag enabled for organization', async () => {
  const { createOrg } = await initSeed().createOwner();
  const { createProject } = await createOrg();
  const { createTargetAccessToken, target } = await createProject();
  const token = await createTargetAccessToken({});

  const { retireAppDeployment } = await execute({
    document: RetireAppDeployment,
    variables: {
      input: {
        targetId: target.id,
        appName: 'my-app',
        appVersion: '1.0.0',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());

  expect(retireAppDeployment).toEqual({
    error: {
      message:
        'This organization has no access to app deployments. Please contact the Hive team for early access.',
    },
    ok: null,
  });
});

test('get app deployment documents via GraphQL API', async () => {
  const { createOrg, ownerToken } = await initSeed().createOwner();
  const { createProject, setFeatureFlag, organization } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, project, target } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: 'app-version',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(createAppDeployment.error).toBeNull();

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        a: String
        b: String
        c: String
        d: String
      }
    `,
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: 'app-version',
        documents: [
          {
            hash: 'aaa',
            body: 'query { a }',
          },
          {
            hash: 'bbb',
            body: 'query { b }',
          },
          {
            hash: 'ccc',
            body: 'query { c }',
          },
          {
            hash: 'ddd',
            body: 'query { d }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(addDocumentsToAppDeployment.error).toBeNull();

  const result = await execute({
    document: GetPaginatedPersistedDocuments,
    variables: {
      targetSelector: {
        organizationSlug: organization.slug,
        projectSlug: project.slug,
        targetSlug: target.slug,
      },
      appDeploymentName: 'app-name',
      appDeploymentVersion: 'app-version',
    },
    authToken: ownerToken,
  }).then(res => res.expectNoGraphQLErrors());
  expect(result.target).toMatchObject({
    appDeployment: {
      documents: {
        edges: [
          {
            cursor: 'YWFh',
            node: {
              body: 'query { a }',
              hash: 'aaa',
            },
          },
          {
            cursor: 'YmJi',
            node: {
              body: 'query { b }',
              hash: 'bbb',
            },
          },
          {
            cursor: 'Y2Nj',
            node: {
              body: 'query { c }',
              hash: 'ccc',
            },
          },
          {
            cursor: 'ZGRk',
            node: {
              body: 'query { d }',
              hash: 'ddd',
            },
          },
        ],
        pageInfo: {
          hasNextPage: false,
        },
      },
      id: expect.any(String),
    },
  });
});

test('paginate app deployment documents via GraphQL API', async () => {
  const { createOrg, ownerToken } = await initSeed().createOwner();
  const { createProject, setFeatureFlag, organization } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, project, target } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: 'app-version',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(createAppDeployment.error).toBeNull();

  await token.publishSchema({
    sdl: /* GraphQL */ `
      type Query {
        a: String
        b: String
        c: String
        d: String
      }
    `,
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: 'app-version',
        documents: [
          {
            hash: 'aaa',
            body: 'query { a }',
          },
          {
            hash: 'bbb',
            body: 'query { b }',
          },
          {
            hash: 'ccc',
            body: 'query { c }',
          },
          {
            hash: 'ddd',
            body: 'query { d }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(addDocumentsToAppDeployment.error).toBeNull();

  let result = await execute({
    document: GetPaginatedPersistedDocuments,
    variables: {
      targetSelector: {
        organizationSlug: organization.slug,
        projectSlug: project.slug,
        targetSlug: target.slug,
      },
      appDeploymentName: 'app-name',
      appDeploymentVersion: 'app-version',
      first: 1,
    },
    authToken: ownerToken,
  }).then(res => res.expectNoGraphQLErrors());
  expect(result.target).toMatchObject({
    appDeployment: {
      documents: {
        edges: [
          {
            cursor: 'YWFh',
            node: {
              body: 'query { a }',
              hash: 'aaa',
            },
          },
        ],
        pageInfo: {
          hasNextPage: true,
        },
      },
      id: expect.any(String),
    },
  });
  result = await execute({
    document: GetPaginatedPersistedDocuments,
    variables: {
      targetSelector: {
        organizationSlug: organization.slug,
        projectSlug: project.slug,
        targetSlug: target.slug,
      },
      appDeploymentName: 'app-name',
      appDeploymentVersion: 'app-version',
      first: 1,
      cursor: 'YWFh',
    },
    authToken: ownerToken,
  }).then(res => res.expectNoGraphQLErrors());
  expect(result.target).toMatchObject({
    appDeployment: {
      documents: {
        edges: [
          {
            cursor: 'YmJi',
            node: {
              body: 'query { b }',
              hash: 'bbb',
            },
          },
        ],
        pageInfo: {
          hasNextPage: true,
        },
      },
      id: expect.any(String),
    },
  });
  result = await execute({
    document: GetPaginatedPersistedDocuments,
    variables: {
      targetSelector: {
        organizationSlug: organization.slug,
        projectSlug: project.slug,
        targetSlug: target.slug,
      },
      appDeploymentName: 'app-name',
      appDeploymentVersion: 'app-version',
      cursor: 'YmJi',
    },
    authToken: ownerToken,
  }).then(res => res.expectNoGraphQLErrors());
  expect(result.target).toMatchObject({
    appDeployment: {
      documents: {
        edges: [
          {
            cursor: 'Y2Nj',
            node: {
              body: 'query { c }',
              hash: 'ccc',
            },
          },
          {
            cursor: 'ZGRk',
            node: {
              body: 'query { d }',
              hash: 'ddd',
            },
          },
        ],
        pageInfo: {
          hasNextPage: false,
        },
      },
      id: expect.any(String),
    },
  });
});

test('app deployment usage reporting', async () => {
  const { createOrg, ownerToken } = await initSeed().createOwner();
  const { createProject, setFeatureFlag, organization } = await createOrg();
  await setFeatureFlag('appDeployments', true);
  const { createTargetAccessToken, project, target } = await createProject();
  const token = await createTargetAccessToken({});

  const { createAppDeployment } = await execute({
    document: CreateAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: 'app-version',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(createAppDeployment.error).toBeNull();

  const sdl = /* GraphQL */ `
    type Query {
      a: String
      b: String
      c: String
      d: String
    }
  `;

  await token.publishSchema({
    sdl,
  });

  const { addDocumentsToAppDeployment } = await execute({
    document: AddDocumentsToAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: 'app-version',
        documents: [
          {
            hash: 'aaa',
            body: 'query { a }',
          },
        ],
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(addDocumentsToAppDeployment.error).toBeNull();

  const { activateAppDeployment } = await execute({
    document: ActivateAppDeployment,
    variables: {
      input: {
        appName: 'app-name',
        appVersion: 'app-version',
      },
    },
    authToken: token.secret,
  }).then(res => res.expectNoGraphQLErrors());
  expect(activateAppDeployment.error).toEqual(null);

  let data = await execute({
    document: GetAppDeployment,
    variables: {
      targetSelector: {
        organizationSlug: organization.slug,
        projectSlug: project.slug,
        targetSlug: target.slug,
      },
      appDeploymentName: 'app-name',
      appDeploymentVersion: 'app-version',
    },
    authToken: ownerToken,
  }).then(res => res.expectNoGraphQLErrors());
  expect(data.target?.appDeployment?.lastUsed).toEqual(null);

  const usageAddress = await getServiceHost('usage', 8081);

  const client = createHive({
    enabled: true,
    token: token.secret,
    usage: true,
    debug: false,
    agent: {
      logger: createLogger('debug'),
      maxSize: 1,
    },
    selfHosting: {
      usageEndpoint: 'http://' + usageAddress,
      graphqlEndpoint: 'http://noop/',
      applicationUrl: 'http://noop/',
    },
  });

  const request = new Request('http://localhost:4000/graphql', {
    method: 'POST',
    headers: {
      'x-graphql-client-name': 'app-name',
      'x-graphql-client-version': 'app-version',
    },
  });

  await client.collectUsage()(
    {
      document: parse(`query { a }`),
      schema: buildASTSchema(parse(sdl)),
      contextValue: { request },
    },
    {},
    'app-name~app-version~aaa',
  );

  await waitFor(5000);

  data = await execute({
    document: GetAppDeployment,
    variables: {
      targetSelector: {
        organizationSlug: organization.slug,
        projectSlug: project.slug,
        targetSlug: target.slug,
      },
      appDeploymentName: 'app-name',
      appDeploymentVersion: 'app-version',
    },
    authToken: ownerToken,
  }).then(res => res.expectNoGraphQLErrors());
  expect(data.target?.appDeployment?.lastUsed).toEqual(expect.any(String));
});
