import gql from 'graphql-tag';
import { fetch } from '__mocks__/apollo-server-env';
import { createTestClient } from 'apollo-server-testing';
import { ApolloServerBase as ApolloServer } from 'apollo-server-core';

import { RemoteGraphQLDataSource } from '../../datasources/RemoteGraphQLDataSource';
import { ApolloGateway, SERVICE_DEFINITION_QUERY } from '../../';
import { fixtures } from 'apollo-federation-integration-testsuite';

beforeEach(() => {
  fetch.mockReset();
});

it('calls buildService only once per service', async () => {
  fetch.mockJSONResponseOnce({
    data: { _service: { sdl: `extend type Query { thing: String }` } },
  });

  const buildServiceSpy = jest.fn(() => {
    return new RemoteGraphQLDataSource({
      url: 'https://api.example.com/foo',
    });
  });

  const gateway = new ApolloGateway({
    serviceList: [{ name: 'foo', url: 'https://api.example.com/foo' }],
    buildService: buildServiceSpy
  });

  await gateway.load();

  expect(buildServiceSpy).toHaveBeenCalledTimes(1);
});

it('correctly passes the context from ApolloServer to datasources', async () => {
  const gateway = new ApolloGateway({
    localServiceList: fixtures,
    buildService: _service => {
      return new RemoteGraphQLDataSource({
        url: 'https://api.example.com/foo',
        willSendRequest: ({ request, context }) => {
          request.http?.headers.set('x-user-id', context.userId);
        },
      });
    },
  });

  const { schema, executor } = await gateway.load();

  const server = new ApolloServer({
    schema,
    executor,
    context: () => ({
      userId: '1234',
    }),
  });

  const call = createTestClient(server);

  const query = gql`
    {
      me {
        username
      }
    }
  `;

  fetch.mockJSONResponseOnce({ data: { me: { username: '@jbaxleyiii' } } });

  const result = await call.query({
    query,
  });

  expect(result.errors).toBeUndefined();
  expect(result.data).toEqual({
    me: { username: '@jbaxleyiii' },
  });

  expect(fetch).toBeCalledTimes(1);
  expect(fetch).toHaveFetched('https://api.example.com/foo', {
    body: {
      query: `{me{username}}`,
      variables: {},
    },
    headers: {
      'x-user-id': '1234',
    },
  });
});

function createSdlData(sdl: string): object {
  return {
    data: {
      _service: {
        sdl: sdl,
      },
    },
  };
}

it('makes enhanced introspection request using datasource', async () => {
  fetch.mockJSONResponseOnce(
    createSdlData('extend type Query { one: String }'),
  );

  const gateway = new ApolloGateway({
    serviceList: [
      {
        name: 'one',
        url: 'https://api.example.com/one',
      },
    ],
    buildService: _service => {
      return new RemoteGraphQLDataSource({
        url: 'https://api.example.com/override',
        willSendRequest: ({ request }) => {
          request.http?.headers.set('custom-header', 'some-custom-value');
        },
      });
    },
  });

  await gateway.load();

  expect(fetch).toBeCalledTimes(1);

  expect(fetch).toHaveFetched('https://api.example.com/override', {
    body: {
      query: SERVICE_DEFINITION_QUERY,
    },
    headers: {
      'custom-header': 'some-custom-value',
    },
  });
});

it('customizes request on a per-service basis', async () => {
  fetch
    .mockJSONResponseOnce(createSdlData('extend type Query { one: String }'))
    .mockJSONResponseOnce(createSdlData('extend type Query { two: String }'))
    .mockJSONResponseOnce(createSdlData('extend type Query { three: String }'));

  const gateway = new ApolloGateway({
    serviceList: [
      {
        name: 'one',
        url: 'https://api.example.com/one',
      },
      {
        name: 'two',
        url: 'https://api.example.com/two',
      },
      {
        name: 'three',
        url: 'https://api.example.com/three',
      },
    ],
    buildService: service => {
      return new RemoteGraphQLDataSource({
        url: service.url,
        willSendRequest: ({ request }) => {
          request.http?.headers.set('service-name', service.name);
        },
      });
    },
  });

  await gateway.load();

  expect(fetch).toBeCalledTimes(3);

  expect(fetch).toHaveFetched('https://api.example.com/one', {
    body: {
      query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`,
    },
    headers: {
      'service-name': 'one',
    },
  });

  expect(fetch).toHaveFetched('https://api.example.com/two', {
    body: {
      query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`,
    },
    headers: {
      'service-name': 'two',
    },
  });

  expect(fetch).toHaveFetched('https://api.example.com/three', {
    body: {
      query: `query __ApolloGetServiceDefinition__ { _service { sdl } }`,
    },
    headers: {
      'service-name': 'three',
    },
  });
});

it('does not share service definition cache between gateways', async () => {
  let updates = 0;
  const updateObserver: any = (..._args: any[]) => {
    updates += 1;
  };

  // Initialize first gateway
  {
    fetch.mockJSONResponseOnce(
      createSdlData('extend type Query { repeat: String }'),
    );

    const gateway = new ApolloGateway({
      serviceList: [
        {
          name: 'repeat',
          url: 'https://api.example.com/repeat',
        },
      ],
      experimental_didUpdateComposition: updateObserver,
    });

    await gateway.load();
  }

  // Initialize second gateway
  {
    fetch.mockJSONResponseOnce(
      createSdlData('extend type Query { repeat: String }'),
    );

    const gateway = new ApolloGateway({
      serviceList: [
        {
          name: 'repeat',
          url: 'https://api.example.com/repeat',
        },
      ],
      experimental_didUpdateComposition: updateObserver,
    });

    await gateway.load();
  }

  expect(updates).toEqual(2);
});
