import { ModelTransformer } from '@aws-amplify/graphql-model-transformer';
import { testTransform } from '@aws-amplify/graphql-transformer-test-utils';
import { ResourceConstants } from 'graphql-transformer-common';
import { AppSyncAuthConfiguration } from '@aws-amplify/graphql-transformer-interfaces';
import { DocumentNode, FieldDefinitionNode, Kind, ObjectTypeDefinitionNode, parse } from 'graphql';
import { FunctionTransformer } from '@aws-amplify/graphql-function-transformer';
import { AuthTransformer } from '../graphql-auth-transformer';

describe('@auth directive without @model', () => {
  const authConfig: AppSyncAuthConfiguration = {
    defaultAuthentication: {
      authenticationType: 'AMAZON_COGNITO_USER_POOLS',
    },
    additionalAuthenticationProviders: [{ authenticationType: 'AWS_IAM' }],
  };

  const getObjectType = (doc: DocumentNode, type: string): ObjectTypeDefinitionNode | undefined => {
    return doc.definitions.find((def) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === type) as
      | ObjectTypeDefinitionNode
      | undefined;
  };

  const expectNoDirectives = (fieldOrType: ObjectTypeDefinitionNode | FieldDefinitionNode | undefined): void => {
    expect(fieldOrType?.directives?.length).toEqual(0);
  };

  const expectOneDirective = (fieldOrType: ObjectTypeDefinitionNode | FieldDefinitionNode | undefined, directiveName: string): void => {
    expect(fieldOrType?.directives?.length).toEqual(1);
    expect(fieldOrType?.directives?.find((d) => d.name.value === directiveName)).toBeDefined();
  };

  const getField = (type: ObjectTypeDefinitionNode | undefined, name: string): FieldDefinitionNode | undefined =>
    type?.fields?.find((f) => f.name.value === name);

  const getObjectWithKeyPrefix = <T>(keyPrefix: string, obj: Record<string, T>): T | undefined => {
    const matchingKey = Object.keys(obj ?? {}).find((key) => key.startsWith(keyPrefix));
    if (!matchingKey) {
      return undefined;
    }
    return obj[matchingKey];
  };

  it('supports @auth with no operations on a field in a type with no @model', () => {
    const validSchema = /* GraphQL */ `
      type Todo {
        id: ID! @auth(rules: [{ allow: owner }])
      }
    `;

    const testTransformParams = {
      schema: validSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer()],
    };
    const out = testTransform(testTransformParams);
    expect(out).toBeDefined();
    const resources = out.rootStack.Resources;
    expect(resources).toBeDefined();
    expect(resources![ResourceConstants.RESOURCES.GraphQLAPILogicalID].Properties.AuthenticationType).toEqual('AMAZON_COGNITO_USER_POOLS');
  });

  it('fails if @auth present on a root type', () => {
    const invalidSchema = /* GraphQL */ `
      type Todo @auth(rules: [{ allow: owner }]) {
        id: ID!
        title: String!
      }
    `;

    const testTransformParams = {
      schema: invalidSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer()],
    };
    expect(() => testTransform(testTransformParams)).toThrowError('Types annotated with @auth must also be annotated with @model.');
  });

  it('fails if @auth with operations is present on a field in a root type', () => {
    const invalidSchema = /* GraphQL */ `
      type Todo {
        id: ID! @auth(rules: [{ allow: owner, operations: [read] }])
      }
    `;

    const testTransformParams = {
      schema: invalidSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer()],
    };
    expect(() => testTransform(testTransformParams)).toThrowError(
      "@auth rules on fields within types that does not have @model directive cannot specify 'operations' argument as there are operations will be generated by the CLI.",
    );
  });

  it('@auth on Query field fails if operations specified', () => {
    const authConfig: AppSyncAuthConfiguration = {
      defaultAuthentication: {
        authenticationType: 'AMAZON_COGNITO_USER_POOLS',
      },
      additionalAuthenticationProviders: [],
    };
    const invalidSchema = /* GraphQL */ `
      type Query {
        getSecret: String! @auth(rules: [{ allow: owner, operations: [read] }])
      }
    `;

    const testTransformParams = {
      schema: invalidSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer()],
    };
    expect(() => testTransform(testTransformParams)).toThrowError(
      "@auth rules on fields within Query, Mutation, Subscription cannot specify 'operations' argument as these rules are already on an operation already.",
    );
  });

  it('@auth directive on Query field does not add @auth directive to Query type', () => {
    const validSchema = /* GraphQL */ `
      type Query {
        getSecret: String! @auth(rules: [{ allow: owner }])
      }
    `;

    const testTransformParams = {
      schema: validSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer()],
    };
    const out = testTransform(testTransformParams);
    expect(out).toBeDefined();

    const parsedSchema = parse(out.schema);

    const queryType = getObjectType(parsedSchema, 'Query');
    expect(queryType).toBeDefined();
    expectNoDirectives(queryType!);
  });

  it('if @auth directive on Query field matches the default, it does not generate an AppSync directive', () => {
    const validSchema = /* GraphQL */ `
      type Query {
        getSecret: String! @auth(rules: [{ allow: private }])
      }
    `;

    const testTransformParams = {
      schema: validSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer()],
    };
    const out = testTransform(testTransformParams);
    expect(out).toBeDefined();

    const parsedSchema = parse(out.schema);

    const queryType = getObjectType(parsedSchema, 'Query');
    expect(queryType).toBeDefined();
    expectNoDirectives(queryType!);
    expectNoDirectives(getField(queryType, 'getSecret'));
  });

  it('if @auth directive on Query field does not match the default, it generates the correct AppSync directive', () => {
    const validSchema = /* GraphQL */ `
      type Query {
        getSecret: String! @auth(rules: [{ allow: private, provider: iam }])
      }
    `;

    const testTransformParams = {
      schema: validSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer()],
    };
    const out = testTransform(testTransformParams);
    expect(out).toBeDefined();

    const parsedSchema = parse(out.schema);

    const queryType = getObjectType(parsedSchema, 'Query');
    expect(queryType).toBeDefined();
    expectNoDirectives(queryType!);
    expectOneDirective(getField(queryType, 'getSecret'), 'aws_iam');
  });

  test('generates the right IAM policy for AuthRole using private auth', () => {
    const validSchema = `
      type Query {
          getSecret: String!
          @auth (
            rules: [
              { allow: private, provider: iam },
            ])
          @function (name: "getSecret-\${env}")
      }
      `;

    const testTransformParams = {
      schema: validSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer(), new FunctionTransformer()],
    };

    const out = testTransform(testTransformParams);
    expect(out).toBeDefined();
    const resources = out.rootStack.Resources;
    expect(resources).toBeDefined();

    const authRolePolicy = getObjectWithKeyPrefix('AuthRolePolicy', resources!);
    const unauthRolePolicy = getObjectWithKeyPrefix('UnauthRolePolicy', resources!);
    expect(authRolePolicy).toBeDefined();
    expect(unauthRolePolicy).toBeUndefined();
    expect(authRolePolicy?.Properties.PolicyDocument.Statement[0].Resource).toBeDefined();
    expect(authRolePolicy?.Properties.PolicyDocument.Statement[0].Resource).not.toEqual({});
  });

  test('generates the right IAM policies for AuthRole and UnauthRole using public auth', () => {
    const validSchema = `
      type Query {
          getSecret: String!
          @auth (
            rules: [
              { allow: public, provider: iam },
            ])
          @function (name: "getSecret-\${env}")
      }
      `;

    const testTransformParams = {
      schema: validSchema,
      authConfig,
      transformers: [new ModelTransformer(), new AuthTransformer(), new FunctionTransformer()],
    };

    const out = testTransform(testTransformParams);
    expect(out).toBeDefined();
    const resources = out.rootStack.Resources;
    expect(resources).toBeDefined();

    const authRolePolicy = getObjectWithKeyPrefix('AuthRolePolicy', resources!);
    const unauthRolePolicy = getObjectWithKeyPrefix('UnauthRolePolicy', resources!);
    expect(authRolePolicy).toBeDefined();
    expect(unauthRolePolicy).toBeDefined();
    expect(authRolePolicy?.Properties.PolicyDocument.Statement[0].Resource).toBeDefined();
    expect(authRolePolicy?.Properties.PolicyDocument.Statement[0].Resource).not.toEqual({});
    expect(unauthRolePolicy?.Properties.PolicyDocument.Statement[0].Resource).toBeDefined();
    expect(unauthRolePolicy?.Properties.PolicyDocument.Statement[0].Resource).not.toEqual({});
  });
});
