import "reflect-metadata";
import { createPubSub } from "@graphql-yoga/subscription";
import {
  type GraphQLInputObjectType,
  type GraphQLInterfaceType,
  type GraphQLObjectType,
  type GraphQLSchema,
  OperationTypeNode,
} from "graphql";
import {
  Arg,
  Args,
  ArgsType,
  Directive,
  Field,
  InputType,
  InterfaceType,
  Mutation,
  ObjectType,
  Query,
  Resolver,
  Subscription,
  buildSchema,
} from "type-graphql";
import { InvalidDirectiveError } from "@/errors/InvalidDirectiveError";
import { getMetadataStorage } from "@/metadata/getMetadataStorage";
import { assertValidDirective } from "../helpers/directives/assertValidDirective";
import { testDirective, testDirectiveTransformer } from "../helpers/directives/TestDirective";
import { expectToThrow } from "../helpers/expectToThrow";

describe("Directives", () => {
  describe("Schema", () => {
    beforeEach(async () => {
      getMetadataStorage().clear();
    });

    describe("on ObjectType", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @Directive("@test")
        @ObjectType()
        class SampleObject {
          @Field()
          sampleField!: string;
        }
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(): SampleObject {
            return { sampleField: "sampleField" };
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleObjectTypeInfo = schema.getType("SampleObject") as GraphQLObjectType;

        expect(() => {
          assertValidDirective(sampleObjectTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleObjectTypeInfo = schema.getType("SampleObject") as GraphQLObjectType;

        expect(sampleObjectTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on ObjectType field", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @ObjectType()
        class SampleObject {
          @Field()
          @Directive("@test")
          sampleField!: string;
        }
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(): SampleObject {
            return { sampleField: "sampleField" };
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleFieldTypeInfo = (
          schema.getType("SampleObject") as GraphQLObjectType
        ).getFields().sampleField;

        expect(() => {
          assertValidDirective(sampleFieldTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleFieldTypeInfo = (
          schema.getType("SampleObject") as GraphQLObjectType
        ).getFields().sampleField;

        expect(sampleFieldTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on ObjectType field argument", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @ArgsType()
        class SampleArgs {
          @Directive("@test")
          @Field()
          sampleArgument!: string;
        }
        @ObjectType()
        class SampleObject {
          @Field()
          sampleField(@Args() { sampleArgument }: SampleArgs): string {
            return sampleArgument;
          }
        }
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(): SampleObject {
            return new SampleObject();
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleFieldArgTypeInfo = (
          schema.getType("SampleObject") as GraphQLObjectType
        ).getFields().sampleField.args[0];

        expect(() => {
          assertValidDirective(sampleFieldArgTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleFieldArgTypeInfo = (
          schema.getType("SampleObject") as GraphQLObjectType
        ).getFields().sampleField.args[0];

        expect(sampleFieldArgTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on InterfaceType", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @Directive("@test")
        @InterfaceType()
        class SampleInterface {
          @Field()
          sampleField!: string;
        }
        @ObjectType({ implements: [SampleInterface] })
        class SampleObject extends SampleInterface {}
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(): SampleInterface {
            const sampleObject = new SampleObject();
            sampleObject.sampleField = "sampleField";
            return sampleObject;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          orphanedTypes: [SampleObject],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleInterfaceTypeInfo = schema.getType("SampleInterface") as GraphQLInterfaceType;

        expect(() => {
          assertValidDirective(sampleInterfaceTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleInterfaceTypeInfo = schema.getType("SampleInterface") as GraphQLInterfaceType;

        expect(sampleInterfaceTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on InterfaceType field", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @InterfaceType()
        class SampleInterface {
          @Directive("@test")
          @Field()
          sampleField!: string;
        }
        @ObjectType({ implements: [SampleInterface] })
        class SampleObject extends SampleInterface {}
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(): SampleInterface {
            const sampleObject = new SampleObject();
            sampleObject.sampleField = "sampleField";
            return sampleObject;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          orphanedTypes: [SampleObject],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleFieldTypeInfo = (
          schema.getType("SampleInterface") as GraphQLInterfaceType
        ).getFields().sampleField;

        expect(() => {
          assertValidDirective(sampleFieldTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleFieldTypeInfo = (
          schema.getType("SampleInterface") as GraphQLInterfaceType
        ).getFields().sampleField;

        expect(sampleFieldTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on InputType", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @Directive("@test")
        @InputType()
        class SampleInput {
          @Field()
          sampleField!: string;
        }
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(@Arg("input") _input: SampleInput): boolean {
            return true;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleInputTypeInfo = schema.getType("SampleInput") as GraphQLInputObjectType;

        expect(() => {
          assertValidDirective(sampleInputTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleInputTypeInfo = schema.getType("SampleInput") as GraphQLInputObjectType;

        expect(sampleInputTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on InputType field", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @InputType()
        class SampleInput {
          @Field()
          @Directive("@test")
          sampleField!: string;
        }
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(@Arg("input") _input: SampleInput): boolean {
            return true;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleFieldTypeInfo = (
          schema.getType("SampleInput") as GraphQLInputObjectType
        ).getFields().sampleField;

        expect(() => {
          assertValidDirective(sampleFieldTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleFieldTypeInfo = (
          schema.getType("SampleInput") as GraphQLInputObjectType
        ).getFields().sampleField;

        expect(sampleFieldTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on Query", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @Resolver()
        class SampleResolver {
          @Directive("@test")
          @Query()
          sampleQuery(): boolean {
            return true;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleQueryInfo = schema
          .getRootType(OperationTypeNode.QUERY)!
          .getFields().sampleQuery;

        expect(() => {
          assertValidDirective(sampleQueryInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleQueryInfo = schema
          .getRootType(OperationTypeNode.QUERY)!
          .getFields().sampleQuery;

        expect(sampleQueryInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on Query field argument using @Args", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @ArgsType()
        class SampleArgs {
          @Directive("@test")
          @Field()
          sampleArgument!: string;
        }
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(@Args() { sampleArgument }: SampleArgs): string {
            return sampleArgument;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields()
          .sampleQuery.args[0];

        expect(() => {
          assertValidDirective(sampleQueryArgTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields()
          .sampleQuery.args[0];

        expect(sampleQueryArgTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on Query field argument using @Arg", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(
            @Arg("sampleArgument")
            @Directive("@test")
            sampleArgument: string,
          ): string {
            return sampleArgument;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields()
          .sampleQuery.args[0];

        expect(() => {
          assertValidDirective(sampleQueryArgTypeInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleQueryArgTypeInfo = (schema.getType("Query") as GraphQLObjectType).getFields()
          .sampleQuery.args[0];

        expect(sampleQueryArgTypeInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on Mutation", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(): boolean {
            return true;
          }

          @Directive("@test")
          @Mutation()
          sampleMutation(): boolean {
            return true;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleMutationInfo = schema
          .getRootType(OperationTypeNode.MUTATION)!
          .getFields().sampleMutation;

        expect(() => {
          assertValidDirective(sampleMutationInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleMutationInfo = schema
          .getRootType(OperationTypeNode.MUTATION)!
          .getFields().sampleMutation;

        expect(sampleMutationInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });

    describe("on Subscription", () => {
      let schema: GraphQLSchema;
      beforeAll(async () => {
        @Resolver()
        class SampleResolver {
          @Query()
          sampleQuery(): boolean {
            return true;
          }

          @Directive("@test")
          @Subscription({ topics: "sample" })
          sampleSubscription(): boolean {
            return true;
          }
        }

        schema = await buildSchema({
          resolvers: [SampleResolver],
          directives: [testDirective],
          validate: false,
          pubSub: createPubSub(),
        });
        schema = testDirectiveTransformer(schema);
      });

      it("should properly emit directive in AST", () => {
        const sampleSubscriptionInfo = schema
          .getRootType(OperationTypeNode.SUBSCRIPTION)!
          .getFields().sampleSubscription;

        expect(() => {
          assertValidDirective(sampleSubscriptionInfo.astNode, "test");
        }).not.toThrow();
      });

      it("should properly apply directive mapper", async () => {
        const sampleSubscriptionInfo = schema
          .getRootType(OperationTypeNode.SUBSCRIPTION)!
          .getFields().sampleSubscription;

        expect(sampleSubscriptionInfo.extensions).toMatchObject({
          TypeGraphQL: { isMappedByDirective: true },
        });
      });
    });
  });

  describe("multiline and leading white spaces", () => {
    let schema: GraphQLSchema;
    beforeAll(async () => {
      @Resolver()
      class SampleResolver {
        @Directive("\n@test")
        @Query()
        multiline(): boolean {
          return true;
        }

        @Directive(" @test")
        @Query()
        leadingWhiteSpaces(): boolean {
          return true;
        }

        @Directive("\n @test")
        @Query()
        multilineAndLeadingWhiteSpaces(): boolean {
          return true;
        }

        @Directive(`
          @test(
            argNonNullDefault: "argNonNullDefault",
            argNullDefault: "argNullDefault",
            argNull: "argNull"
          )
        `)
        @Query()
        rawMultilineAndLeadingWhiteSpaces(): boolean {
          return true;
        }
      }

      schema = await buildSchema({
        resolvers: [SampleResolver],
        directives: [testDirective],
        validate: false,
      });
      schema = testDirectiveTransformer(schema);
    });

    it("should properly emit directive in AST", () => {
      const multilineInfo = schema.getRootType(OperationTypeNode.QUERY)!.getFields().multiline;
      const leadingWhiteSpacesInfo = schema
        .getRootType(OperationTypeNode.QUERY)!
        .getFields().leadingWhiteSpaces;
      const multilineAndLeadingWhiteSpacesInfo = schema
        .getRootType(OperationTypeNode.QUERY)!
        .getFields().multilineAndLeadingWhiteSpaces;
      const rawMultilineAndLeadingWhiteSpacesInfo = schema
        .getRootType(OperationTypeNode.QUERY)!
        .getFields().rawMultilineAndLeadingWhiteSpaces;

      expect(() => {
        assertValidDirective(multilineInfo.astNode, "test");
        assertValidDirective(leadingWhiteSpacesInfo.astNode, "test");
        assertValidDirective(multilineAndLeadingWhiteSpacesInfo.astNode, "test");
        assertValidDirective(rawMultilineAndLeadingWhiteSpacesInfo.astNode, "test", {
          argNonNullDefault: `"argNonNullDefault"`,
          argNullDefault: `"argNullDefault"`,
          argNull: `"argNull"`,
        });
      }).not.toThrow();
    });

    it("should properly apply directive mapper", async () => {
      const multilineInfo = schema.getRootType(OperationTypeNode.QUERY)!.getFields().multiline;
      const leadingWhiteSpacesInfo = schema
        .getRootType(OperationTypeNode.QUERY)!
        .getFields().leadingWhiteSpaces;
      const multilineAndLeadingWhiteSpacesInfo = schema
        .getRootType(OperationTypeNode.QUERY)!
        .getFields().multilineAndLeadingWhiteSpaces;
      const rawMultilineAndLeadingWhiteSpacesInfo = schema
        .getRootType(OperationTypeNode.QUERY)!
        .getFields().rawMultilineAndLeadingWhiteSpaces;

      expect(multilineInfo.extensions).toMatchObject({
        TypeGraphQL: { isMappedByDirective: true },
      });
      expect(leadingWhiteSpacesInfo.extensions).toMatchObject({
        TypeGraphQL: { isMappedByDirective: true },
      });
      expect(multilineAndLeadingWhiteSpacesInfo.extensions).toMatchObject({
        TypeGraphQL: { isMappedByDirective: true },
      });
      expect(rawMultilineAndLeadingWhiteSpacesInfo.extensions).toMatchObject({
        TypeGraphQL: { isMappedByDirective: true },
      });
    });
  });

  describe("errors", () => {
    beforeEach(async () => {
      getMetadataStorage().clear();
    });

    it("throws error on multiple directive definitions", async () => {
      @Resolver()
      class InvalidQuery {
        @Query()
        @Directive("@upper @append")
        invalid(): string {
          return "invalid";
        }
      }

      const error = await expectToThrow(() => buildSchema({ resolvers: [InvalidQuery] }));

      expect(error).toBeInstanceOf(InvalidDirectiveError);
      expect(error.message).toContain(
        'Please pass only one directive name or definition at a time to the @Directive decorator "@upper @append"',
      );
    });

    it("throws error when parsing invalid directives", async () => {
      @Resolver()
      class InvalidQuery {
        @Query()
        @Directive("@invalid(@directive)")
        invalid(): string {
          return "invalid";
        }
      }

      const error = await expectToThrow(() => buildSchema({ resolvers: [InvalidQuery] }));

      expect(error).toBeInstanceOf(InvalidDirectiveError);
      expect(error.message).toContain('Error parsing directive definition "@invalid(@directive)"');
    });

    it("throws error when no directives are defined", async () => {
      @Resolver()
      class InvalidQuery {
        @Query()
        @Directive("")
        invalid(): string {
          return "invalid";
        }
      }

      const error = await expectToThrow(() => buildSchema({ resolvers: [InvalidQuery] }));

      expect(error).toBeInstanceOf(InvalidDirectiveError);
      expect(error.message).toContain(
        "Please pass at-least one directive name or definition to the @Directive decorator",
      );
    });
  });
});
