import { parse } from 'graphql';
import { composeServices } from '@theguild/federation-composition';
import { extractSuperGraphInformation } from '../federation-super-graph.js';

const federation2SDL = parse(/* GraphQL */ `
  schema
    @link(url: "https://specs.apollo.dev/link/v1.0")
    @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
    @link(url: "https://specs.apollo.dev/tag/v0.3")
    @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY)
    @link(
      url: "https://myspecs.dev/myDirective/v1.0"
      import: ["@myDirective", { name: "@anotherDirective", as: "@hello" }]
    ) {
    query: Query
  }

  directive @hello on FIELD_DEFINITION

  directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

  directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

  directive @join__field(
    graph: join__Graph
    requires: join__FieldSet
    provides: join__FieldSet
    type: String
    external: Boolean
    override: String
    usedOverridden: Boolean
  ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

  directive @join__graph(name: String!, url: String!) on ENUM_VALUE

  directive @join__implements(
    graph: join__Graph!
    interface: String!
  ) repeatable on OBJECT | INTERFACE

  directive @join__type(
    graph: join__Graph!
    key: join__FieldSet
    extension: Boolean! = false
    resolvable: Boolean! = true
    isInterfaceObject: Boolean! = false
  ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

  directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

  directive @link(
    url: String
    as: String
    for: link__Purpose
    import: [link__Import]
  ) repeatable on SCHEMA

  directive @myDirective(a: String!) on FIELD_DEFINITION

  directive @tag(
    name: String!
  ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA

  type DeliveryEstimates @join__type(graph: INVENTORY) {
    estimatedDelivery: String
    fastestDelivery: String
  }

  scalar join__FieldSet

  enum join__Graph {
    INVENTORY @join__graph(name: "inventory", url: "")
    PANDAS @join__graph(name: "pandas", url: "")
    PRODUCTS @join__graph(name: "products", url: "")
    REVIEWS @join__graph(name: "reviews", url: "")
    USERS @join__graph(name: "users", url: "")
  }

  scalar link__Import

  enum link__Purpose {
    SECURITY
    EXECUTION
  }

  type Panda @join__type(graph: PANDAS) {
    name: ID!
    favoriteFood: String @tag(name: "nom-nom-nom")
  }

  type Product implements ProductItf & SkuItf
    @join__implements(graph: INVENTORY, interface: "ProductItf")
    @join__implements(graph: PRODUCTS, interface: "ProductItf")
    @join__implements(graph: PRODUCTS, interface: "SkuItf")
    @join__implements(graph: REVIEWS, interface: "ProductItf")
    @join__type(graph: INVENTORY, key: "id")
    @join__type(graph: PRODUCTS, key: "id")
    @join__type(graph: PRODUCTS, key: "sku package")
    @join__type(graph: PRODUCTS, key: "sku variation { id }")
    @join__type(graph: REVIEWS, key: "id") {
    id: ID! @tag(name: "hi-from-products")
    dimensions: ProductDimension
      @join__field(graph: INVENTORY, external: true)
      @join__field(graph: PRODUCTS)
    delivery(zip: String): DeliveryEstimates
      @join__field(graph: INVENTORY, requires: "dimensions { size weight }")
    sku: String @join__field(graph: PRODUCTS)
    name: String @join__field(graph: PRODUCTS) @hello
    package: String @join__field(graph: PRODUCTS)
    variation: ProductVariation @join__field(graph: PRODUCTS)
    createdBy: User @join__field(graph: PRODUCTS)
    hidden: String @join__field(graph: PRODUCTS)
    reviewsScore: Float! @join__field(graph: REVIEWS, override: "products")
    oldField: String @join__field(graph: PRODUCTS)
    reviewsCount: Int! @join__field(graph: REVIEWS)
    reviews: [Review!]! @join__field(graph: REVIEWS)
  }

  type ProductDimension @join__type(graph: INVENTORY) @join__type(graph: PRODUCTS) {
    size: String
    weight: Float
  }

  interface ProductItf implements SkuItf
    @join__implements(graph: PRODUCTS, interface: "SkuItf")
    @join__type(graph: INVENTORY)
    @join__type(graph: PRODUCTS)
    @join__type(graph: REVIEWS) {
    id: ID!
    dimensions: ProductDimension @join__field(graph: INVENTORY) @join__field(graph: PRODUCTS)
    delivery(zip: String): DeliveryEstimates @join__field(graph: INVENTORY)
    sku: String @join__field(graph: PRODUCTS)
    name: String @join__field(graph: PRODUCTS)
    package: String @join__field(graph: PRODUCTS)
    variation: ProductVariation @join__field(graph: PRODUCTS)
    createdBy: User @join__field(graph: PRODUCTS)
    hidden: String @inaccessible @join__field(graph: PRODUCTS)
    oldField: String @join__field(graph: PRODUCTS) @deprecated(reason: "refactored out")
    reviewsCount: Int! @join__field(graph: REVIEWS)
    reviewsScore: Float! @join__field(graph: REVIEWS)
    reviews: [Review!]! @join__field(graph: REVIEWS)
  }

  type ProductVariation @join__type(graph: PRODUCTS) {
    id: ID!
    name: String
  }

  type Query
    @join__type(graph: INVENTORY)
    @join__type(graph: PANDAS)
    @join__type(graph: PRODUCTS)
    @join__type(graph: REVIEWS)
    @join__type(graph: USERS) {
    allPandas: [Panda] @join__field(graph: PANDAS)
    panda(name: ID!): Panda @join__field(graph: PANDAS)
    allProducts: [ProductItf] @join__field(graph: PRODUCTS)
    product(id: ID!): ProductItf @join__field(graph: PRODUCTS)
    review(id: Int!): Review @join__field(graph: REVIEWS)
  }

  type Review @join__type(graph: REVIEWS) {
    id: Int!
    body: String!
  }

  enum ShippingClass @join__type(graph: INVENTORY) @join__type(graph: PRODUCTS) {
    STANDARD @join__enumValue(graph: INVENTORY) @join__enumValue(graph: PRODUCTS)
    EXPRESS @join__enumValue(graph: INVENTORY) @join__enumValue(graph: PRODUCTS)
    OVERNIGHT @join__enumValue(graph: INVENTORY)
  }

  interface SkuItf @join__type(graph: PRODUCTS) {
    sku: String
  }

  type User @join__type(graph: PRODUCTS, key: "email") @join__type(graph: USERS, key: "email") {
    email: ID! @tag(name: "test-from-users")
    totalProductsCreated: Int
    name: String @join__field(graph: USERS)
  }

  union PandaOrUser
    @join__type(graph: PRODUCTS)
    @join__type(graph: PANDAS)
    @join__unionMember(graph: PRODUCTS, member: "User")
    @join__unionMember(graph: B, member: "Panda") =
    | Movie
    | Book

  input UserInput @join__type(graph: PRODUCTS) @join__type(graph: PANDAS) {
    name: String!
  }
`);

const federation1SDL = parse(/* GraphQL */ `
  schema
    @core(feature: "https://specs.apollo.dev/core/v0.2")
    @core(feature: "https://specs.apollo.dev/join/v0.1", for: EXECUTION) {
    query: Query
  }

  directive @core(as: String, feature: String!, for: core__Purpose) repeatable on SCHEMA

  directive @join__field(
    graph: join__Graph
    provides: join__FieldSet
    requires: join__FieldSet
  ) on FIELD_DEFINITION

  directive @join__graph(name: String!, url: String!) on ENUM_VALUE

  directive @join__owner(graph: join__Graph!) on INTERFACE | OBJECT

  directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on INTERFACE | OBJECT

  type NestedQuery {
    test: String
  }

  type Query {
    field(arg: String): String @join__field(graph: FOO)
    nested: NestedQuery! @join__field(graph: FOO)
  }

  enum core__Purpose {
    EXECUTION
    SECURITY
  }

  scalar join__FieldSet

  enum join__Graph {
    FOO @join__graph(name: "foo", url: "https://foo.com")
  }
`);

describe('extractSuperGraphInformation', () => {
  test('extracts definitions (Federation 2)', () => {
    const result = extractSuperGraphInformation(federation2SDL);
    expect(result).toMatchInlineSnapshot(`
      {
        schemaCoordinateServicesMappings: Map {
          DeliveryEstimates.estimatedDelivery => [
            inventory,
          ],
          DeliveryEstimates.fastestDelivery => [
            inventory,
          ],
          DeliveryEstimates => [
            inventory,
          ],
          Panda.name => [
            pandas,
          ],
          Panda.favoriteFood => [
            pandas,
          ],
          Panda => [
            pandas,
          ],
          Product.id => [
            inventory,
            products,
            reviews,
          ],
          Product.dimensions => [
            products,
          ],
          Product.delivery => [
            inventory,
          ],
          Product.sku => [
            products,
          ],
          Product.name => [
            products,
          ],
          Product.package => [
            products,
          ],
          Product.variation => [
            products,
          ],
          Product.createdBy => [
            products,
          ],
          Product.hidden => [
            products,
          ],
          Product.reviewsScore => [
            reviews,
          ],
          Product.oldField => [
            products,
          ],
          Product.reviewsCount => [
            reviews,
          ],
          Product.reviews => [
            reviews,
          ],
          Product => [
            inventory,
            products,
            reviews,
          ],
          ProductDimension.size => [
            inventory,
            products,
          ],
          ProductDimension.weight => [
            inventory,
            products,
          ],
          ProductDimension => [
            inventory,
            products,
          ],
          ProductItf.id => [
            inventory,
            products,
            reviews,
          ],
          ProductItf.dimensions => [
            inventory,
          ],
          ProductItf.delivery => [
            inventory,
          ],
          ProductItf.sku => [
            products,
          ],
          ProductItf.name => [
            products,
          ],
          ProductItf.package => [
            products,
          ],
          ProductItf.variation => [
            products,
          ],
          ProductItf.createdBy => [
            products,
          ],
          ProductItf.hidden => [
            products,
          ],
          ProductItf.oldField => [
            products,
          ],
          ProductItf.reviewsCount => [
            reviews,
          ],
          ProductItf.reviewsScore => [
            reviews,
          ],
          ProductItf.reviews => [
            reviews,
          ],
          ProductItf => [
            inventory,
            products,
            reviews,
          ],
          ProductVariation.id => [
            products,
          ],
          ProductVariation.name => [
            products,
          ],
          ProductVariation => [
            products,
          ],
          Query.allPandas => [
            pandas,
          ],
          Query.panda => [
            pandas,
          ],
          Query.allProducts => [
            products,
          ],
          Query.product => [
            products,
          ],
          Query.review => [
            reviews,
          ],
          Query => [
            inventory,
            pandas,
            products,
            reviews,
            users,
          ],
          Review.id => [
            reviews,
          ],
          Review.body => [
            reviews,
          ],
          Review => [
            reviews,
          ],
          ShippingClass => [
            inventory,
            products,
          ],
          ShippingClass.STANDARD => [
            inventory,
            products,
          ],
          ShippingClass.EXPRESS => [
            inventory,
            products,
          ],
          ShippingClass.OVERNIGHT => [
            inventory,
          ],
          SkuItf.sku => [
            products,
          ],
          SkuItf => [
            products,
          ],
          User.email => [
            products,
            users,
          ],
          User.totalProductsCreated => [
            products,
            users,
          ],
          User.name => [
            users,
          ],
          User => [
            products,
            users,
          ],
          PandaOrUser => [
            products,
            pandas,
          ],
          UserInput => [
            products,
            pandas,
          ],
          UserInput.name => [
            products,
            pandas,
          ],
        },
      }
    `);
  });

  test('extracts definitions (Federation 1)', () => {
    const result = extractSuperGraphInformation(federation1SDL);
    expect(result).toMatchInlineSnapshot(`
      {
        schemaCoordinateServicesMappings: Map {
          Query.field => [
            foo,
          ],
          Query.nested => [
            foo,
          ],
          Query => [
            foo,
          ],
          NestedQuery => [
            foo,
          ],
          NestedQuery.test => [
            foo,
          ],
        },
      }
    `);
  });
});

test('subgraph ownership for external fields is correct', () => {
  const a = parse(/* GraphQL */ `
    extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])

    type Product @key(fields: "id") {
      id: ID!
      weight: ProductWeight!
    }

    type ProductWeight {
      id: ID!
      weight: Int!
    }

    type Query {
      a: Product!
      g: ProductWeight!
    }
  `);

  const b = parse(/* GraphQL */ `
    extend schema
      @link(
        url: "https://specs.apollo.dev/federation/v2.3"
        import: ["@key", "@external", "@requires"]
      )

    type Product @key(fields: "id") {
      id: ID!
      weight: ProductWeight! @external
      shippingCosts: Int! @requires(fields: "weight { id }")
    }

    type ProductWeight @external {
      id: ID!
    }

    type Query {
      c: Product!
    }
  `);

  const result = composeServices([
    {
      name: 'a',
      typeDefs: a,
    },
    {
      name: 'b',
      typeDefs: b,
    },
  ]);

  const supergraphInfo = extractSuperGraphInformation(parse(result.supergraphSdl!));
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('Product.weight')).toEqual(['a']);
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('ProductWeight.id')).toEqual(['a']);
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('ProductWeight')).toEqual(['a', 'b']);
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('Product.shippingCosts')).toEqual([
    'b',
  ]);
});

test('subgraph ownership picks first non external "join__field" annotation', () => {
  // See Product.weight within the schema
  // B is picked as the owner even though it is external
  const supergraphSdl = /* GraphQL */ `
    schema
      @link(url: "https://specs.apollo.dev/link/v1.0")
      @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
      query: Query
    }

    directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

    directive @join__graph(name: String!, url: String!) on ENUM_VALUE

    directive @join__field(
      graph: join__Graph
      requires: join__FieldSet
      provides: join__FieldSet
      type: String
      external: Boolean
      override: String
      usedOverridden: Boolean
    ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

    directive @join__implements(
      graph: join__Graph!
      interface: String!
    ) repeatable on OBJECT | INTERFACE

    directive @join__type(
      graph: join__Graph!
      key: join__FieldSet
      extension: Boolean! = false
      resolvable: Boolean! = true
      isInterfaceObject: Boolean! = false
    ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

    directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

    scalar join__FieldSet

    directive @link(
      url: String
      as: String
      for: link__Purpose
      import: [link__Import]
    ) repeatable on SCHEMA

    scalar link__Import

    enum link__Purpose {
      SECURITY
      EXECUTION
    }

    enum join__Graph {
      A @join__graph(name: "a", url: "")
      B @join__graph(name: "b", url: "")
    }

    type Product @join__type(graph: A, key: "id") @join__type(graph: B, key: "id") {
      id: ID!
      # THIS IS THE RELEVANT FIELD FOR THIS TEST
      weight: ProductWeight! @join__field(graph: B, external: true) @join__field(graph: A)
      shippingCosts: Int! @join__field(graph: B, requires: "weight{id}")
    }

    type ProductWeight @join__type(graph: A) @join__type(graph: B) {
      id: ID! @join__field(graph: A) @join__field(graph: B, external: true)
      weight: Int! @join__field(graph: A)
    }

    type Query @join__type(graph: A) @join__type(graph: B) {
      a: Product! @join__field(graph: A)
      g: ProductWeight! @join__field(graph: A)
      c: Product! @join__field(graph: B)
    }
  `;

  const supergraphInfo = extractSuperGraphInformation(parse(supergraphSdl!));
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('Product.weight')).toEqual(['a']);
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('ProductWeight.id')).toEqual(['a']);
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('ProductWeight')).toEqual(['a', 'b']);
  expect(supergraphInfo.schemaCoordinateServicesMappings.get('Product.shippingCosts')).toEqual([
    'b',
  ]);
});
