import { composeServices } from '../../../compose';
import { providesNotOnEntity as validateProvidesNotOnEntity } from '../';
import {
  gql,
  graphqlErrorSerializer,
} from 'apollo-federation-integration-testsuite';

expect.addSnapshotSerializer(graphqlErrorSerializer);

describe('providesNotOnEntity', () => {
  it('does not warn when @provides used on an entity', () => {
    const serviceA = {
      typeDefs: gql`
        type LineItem @key(fields: "sku") {
          sku: String!
          quantity: Int!
        }
      `,
      name: 'serviceA',
    };

    const serviceB = {
      typeDefs: gql`
        type Product {
          lineItem: LineItem @provides(fields: "quantity")
          lineItemNonNull: LineItem! @provides(fields: "quantity")
        }
      `,
      name: 'serviceB',
    };

    const serviceList = [serviceA, serviceB];
    const { schema } = composeServices(serviceList);
    const warnings = validateProvidesNotOnEntity({ schema, serviceList });
    expect(warnings).toMatchInlineSnapshot(`Array []`);
  });

  it('does not warn when @provides used on a list of entity', () => {
    const serviceA = {
      typeDefs: gql`
        type LineItem @key(fields: "sku") {
          sku: String!
          quantity: Int!
        }
      `,
      name: 'serviceA',
    };

    const serviceB = {
      typeDefs: gql`
        type Product {
          lineItems: [LineItem] @provides(fields: "quantity")
          lineItemsNonNull: [LineItem]! @provides(fields: "quantity")
          nonNullLineItems: [LineItem!] @provides(fields: "quantity")
          nonNullLineItemsNonNull: [LineItem!]! @provides(fields: "quantity")
          deep: [[LineItem!]!]! @provides(fields: "quantity")
        }
      `,
      name: 'serviceB',
    };

    const serviceList = [serviceA, serviceB];
    const { schema } = composeServices(serviceList);
    const warnings = validateProvidesNotOnEntity({ schema, serviceList });
    expect(warnings).toMatchInlineSnapshot(`Array []`);
  });

  it('does not warn when @provides used on an entity of a child type', () => {
    const serviceA = {
      typeDefs: gql`
        type User @key(fields: "id") {
          id: ID!
          username: String
        }
      `,
      name: 'serviceA',
    };

    const serviceB = {
      typeDefs: gql`
        type Review {
          author: User @provides(fields: "username")
        }

        type User {
          username: String @external
        }
      `,
      name: 'serviceB',
    };

    const serviceList = [serviceA, serviceB];
    const { schema } = composeServices(serviceList);
    const warnings = validateProvidesNotOnEntity({ schema, serviceList });
    expect(warnings).toEqual([]);
  });

  it('warns when there is a @provides on a type that is not an entity', () => {
    const serviceA = {
      typeDefs: gql`
        type Product @key(fields: "sku") {
          sku: String!
          upc: String!
          id: ID!
        }

        type LineItem {
          sku: String!
          quantity: Int!
        }
      `,
      name: 'serviceA',
    };

    const serviceB = {
      typeDefs: gql`
        extend type Product @key(fields: "sku") {
          sku: String! @external
          lineItem: LineItem @provides(fields: "quantity")
        }
      `,
      name: 'serviceB',
    };

    const serviceList = [serviceA, serviceB];
    const { schema } = composeServices(serviceList);
    const warnings = validateProvidesNotOnEntity({ schema, serviceList });
    expect(warnings).toMatchInlineSnapshot(`
      Array [
        Object {
          "code": "PROVIDES_NOT_ON_ENTITY",
          "locations": Array [
            Object {
              "column": 22,
              "line": 4,
            },
          ],
          "message": "[serviceB] Product.lineItem -> uses the @provides directive but \`Product.lineItem\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.",
        },
      ]
    `);
  });

  it('warns when there is a @provides on a type that is not a list of entity', () => {
    const serviceA = {
      typeDefs: gql`
        type Product @key(fields: "sku") {
          sku: String!
          upc: String!
          id: ID!
        }

        type LineItem {
          sku: String!
          quantity: Int!
        }
      `,
      name: 'serviceA',
    };

    const serviceB = {
      typeDefs: gql`
        extend type Product @key(fields: "sku") {
          sku: String! @external
          lineItems: [LineItem] @provides(fields: "quantity")
        }
      `,
      name: 'serviceB',
    };

    const serviceList = [serviceA, serviceB];
    const { schema } = composeServices(serviceList);
    const warnings = validateProvidesNotOnEntity({ schema, serviceList });
    expect(warnings).toMatchInlineSnapshot(`
      Array [
        Object {
          "code": "PROVIDES_NOT_ON_ENTITY",
          "locations": Array [
            Object {
              "column": 25,
              "line": 4,
            },
          ],
          "message": "[serviceB] Product.lineItems -> uses the @provides directive but \`Product.lineItems\` does not return a type that has a @key. Try adding a @key to the \`LineItem\` type.",
        },
      ]
    `);
  });

  it('warns when there is a @provides on a non-object type', () => {
    const serviceA = {
      typeDefs: gql`
        type Product @key(fields: "sku") {
          sku: String!
          upc: String!
          id: ID!
        }

        enum Category {
          BOOK
          MOVIE
          SONG
          ALBUM
        }
      `,
      name: 'serviceA',
    };

    const serviceB = {
      typeDefs: gql`
        extend type Product @key(fields: "sku") {
          sku: String! @external
          category: Category @provides(fields: "id")
        }
      `,
      name: 'serviceB',
    };

    const serviceList = [serviceA, serviceB];
    const { schema } = composeServices(serviceList);
    const warnings = validateProvidesNotOnEntity({ schema, serviceList });
    expect(warnings).toMatchInlineSnapshot(`
      Array [
        Object {
          "code": "PROVIDES_NOT_ON_ENTITY",
          "locations": Array [
            Object {
              "column": 22,
              "line": 4,
            },
          ],
          "message": "[serviceB] Product.category -> uses the @provides directive but \`Product.category\` returns \`Category\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.",
        },
      ]
    `);
  });

  it('warns when there is a @provides on a list of non-object type', () => {
    const serviceA = {
      typeDefs: gql`
        type Product @key(fields: "sku") {
          sku: String!
          upc: String!
          id: ID!
        }

        enum Category {
          BOOK
          MOVIE
          SONG
          ALBUM
        }
      `,
      name: 'serviceA',
    };

    const serviceB = {
      typeDefs: gql`
        extend type Product @key(fields: "sku") {
          sku: String! @external
          categories: [Category] @provides(fields: "id")
        }
      `,
      name: 'serviceB',
    };

    const serviceList = [serviceA, serviceB];
    const { schema } = composeServices(serviceList);
    const warnings = validateProvidesNotOnEntity({ schema, serviceList });
    expect(warnings).toMatchInlineSnapshot(`
      Array [
        Object {
          "code": "PROVIDES_NOT_ON_ENTITY",
          "locations": Array [
            Object {
              "column": 26,
              "line": 4,
            },
          ],
          "message": "[serviceB] Product.categories -> uses the @provides directive but \`Product.categories\` returns \`[Category]\`, which is not an Object or List type. @provides can only be used on Object types with at least one @key, or Lists of such Objects.",
        },
      ]
    `);
  });
});
