import { expect } from 'chai';
import { describe, it } from 'mocha';

import { graphqlSync } from '../../graphql';
import { introspectionFromSchema } from '../../utilities/introspectionFromSchema';

import { GraphQLSchema } from '../schema';
import { GraphQLEnumType, GraphQLObjectType } from '../definition';
import { GraphQLInt, GraphQLString, GraphQLBoolean } from '../scalars';

const ColorType = new GraphQLEnumType({
  name: 'Color',
  values: {
    RED: { value: 0 },
    GREEN: { value: 1 },
    BLUE: { value: 2 },
  },
});

const Complex1 = { someRandomObject: new Date() };
const Complex2 = { someRandomValue: 123 };

const ComplexEnum = new GraphQLEnumType({
  name: 'Complex',
  values: {
    ONE: { value: Complex1 },
    TWO: { value: Complex2 },
  },
});

const QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    colorEnum: {
      type: ColorType,
      args: {
        fromEnum: { type: ColorType },
        fromInt: { type: GraphQLInt },
        fromString: { type: GraphQLString },
      },
      resolve(_source, { fromEnum, fromInt, fromString }) {
        return fromInt !== undefined
          ? fromInt
          : fromString !== undefined
          ? fromString
          : fromEnum;
      },
    },
    colorInt: {
      type: GraphQLInt,
      args: {
        fromEnum: { type: ColorType },
      },
      resolve(_source, { fromEnum }) {
        return fromEnum;
      },
    },
    complexEnum: {
      type: ComplexEnum,
      args: {
        fromEnum: {
          type: ComplexEnum,
          // Note: defaultValue is provided an *internal* representation for
          // Enums, rather than the string name.
          defaultValue: Complex1,
        },
        provideGoodValue: { type: GraphQLBoolean },
        provideBadValue: { type: GraphQLBoolean },
      },
      resolve(_source, { fromEnum, provideGoodValue, provideBadValue }) {
        if (provideGoodValue) {
          // Note: this is one of the references of the internal values which
          // ComplexEnum allows.
          return Complex2;
        }
        if (provideBadValue) {
          // Note: similar shape, but not the same *reference*
          // as Complex2 above. Enum internal values require === equality.
          return { someRandomValue: 123 };
        }
        return fromEnum;
      },
    },
  },
});

const MutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    favoriteEnum: {
      type: ColorType,
      args: { color: { type: ColorType } },
      resolve: (_source, { color }) => color,
    },
  },
});

const SubscriptionType = new GraphQLObjectType({
  name: 'Subscription',
  fields: {
    subscribeToEnum: {
      type: ColorType,
      args: { color: { type: ColorType } },
      resolve: (_source, { color }) => color,
    },
  },
});

const schema = new GraphQLSchema({
  query: QueryType,
  mutation: MutationType,
  subscription: SubscriptionType,
});

function executeQuery(
  source: string,
  variableValues?: { +[variable: string]: mixed, ... },
) {
  return graphqlSync({ schema, source, variableValues });
}

describe('Type System: Enum Values', () => {
  it('accepts enum literals as input', () => {
    const result = executeQuery('{ colorInt(fromEnum: GREEN) }');

    expect(result).to.deep.equal({
      data: { colorInt: 1 },
    });
  });

  it('enum may be output type', () => {
    const result = executeQuery('{ colorEnum(fromInt: 1) }');

    expect(result).to.deep.equal({
      data: { colorEnum: 'GREEN' },
    });
  });

  it('enum may be both input and output type', () => {
    const result = executeQuery('{ colorEnum(fromEnum: GREEN) }');

    expect(result).to.deep.equal({
      data: { colorEnum: 'GREEN' },
    });
  });

  it('does not accept string literals', () => {
    const result = executeQuery('{ colorEnum(fromEnum: "GREEN") }');

    expect(result).to.deep.equal({
      errors: [
        {
          message:
            'Enum "Color" cannot represent non-enum value: "GREEN". Did you mean the enum value "GREEN"?',
          locations: [{ line: 1, column: 23 }],
        },
      ],
    });
  });

  it('does not accept values not in the enum', () => {
    const result = executeQuery('{ colorEnum(fromEnum: GREENISH) }');

    expect(result).to.deep.equal({
      errors: [
        {
          message:
            'Value "GREENISH" does not exist in "Color" enum. Did you mean the enum value "GREEN"?',
          locations: [{ line: 1, column: 23 }],
        },
      ],
    });
  });

  it('does not accept values with incorrect casing', () => {
    const result = executeQuery('{ colorEnum(fromEnum: green) }');

    expect(result).to.deep.equal({
      errors: [
        {
          message:
            'Value "green" does not exist in "Color" enum. Did you mean the enum value "GREEN" or "RED"?',
          locations: [{ line: 1, column: 23 }],
        },
      ],
    });
  });

  it('does not accept incorrect internal value', () => {
    const result = executeQuery('{ colorEnum(fromString: "GREEN") }');

    expect(result).to.deep.equal({
      data: { colorEnum: null },
      errors: [
        {
          message: 'Enum "Color" cannot represent value: "GREEN"',
          locations: [{ line: 1, column: 3 }],
          path: ['colorEnum'],
        },
      ],
    });
  });

  it('does not accept internal value in place of enum literal', () => {
    const result = executeQuery('{ colorEnum(fromEnum: 1) }');

    expect(result).to.deep.equal({
      errors: [
        {
          message: 'Enum "Color" cannot represent non-enum value: 1.',
          locations: [{ line: 1, column: 23 }],
        },
      ],
    });
  });

  it('does not accept enum literal in place of int', () => {
    const result = executeQuery('{ colorEnum(fromInt: GREEN) }');

    expect(result).to.deep.equal({
      errors: [
        {
          message: 'Int cannot represent non-integer value: GREEN',
          locations: [{ line: 1, column: 22 }],
        },
      ],
    });
  });

  it('accepts JSON string as enum variable', () => {
    const doc = 'query ($color: Color!) { colorEnum(fromEnum: $color) }';
    const result = executeQuery(doc, { color: 'BLUE' });

    expect(result).to.deep.equal({
      data: { colorEnum: 'BLUE' },
    });
  });

  it('accepts enum literals as input arguments to mutations', () => {
    const doc = 'mutation ($color: Color!) { favoriteEnum(color: $color) }';
    const result = executeQuery(doc, { color: 'GREEN' });

    expect(result).to.deep.equal({
      data: { favoriteEnum: 'GREEN' },
    });
  });

  it('accepts enum literals as input arguments to subscriptions', () => {
    const doc =
      'subscription ($color: Color!) { subscribeToEnum(color: $color) }';
    const result = executeQuery(doc, { color: 'GREEN' });

    expect(result).to.deep.equal({
      data: { subscribeToEnum: 'GREEN' },
    });
  });

  it('does not accept internal value as enum variable', () => {
    const doc = 'query ($color: Color!) { colorEnum(fromEnum: $color) }';
    const result = executeQuery(doc, { color: 2 });

    expect(result).to.deep.equal({
      errors: [
        {
          message:
            'Variable "$color" got invalid value 2; Enum "Color" cannot represent non-string value: 2.',
          locations: [{ line: 1, column: 8 }],
        },
      ],
    });
  });

  it('does not accept string variables as enum input', () => {
    const doc = 'query ($color: String!) { colorEnum(fromEnum: $color) }';
    const result = executeQuery(doc, { color: 'BLUE' });

    expect(result).to.deep.equal({
      errors: [
        {
          message:
            'Variable "$color" of type "String!" used in position expecting type "Color".',
          locations: [
            { line: 1, column: 8 },
            { line: 1, column: 47 },
          ],
        },
      ],
    });
  });

  it('does not accept internal value variable as enum input', () => {
    const doc = 'query ($color: Int!) { colorEnum(fromEnum: $color) }';
    const result = executeQuery(doc, { color: 2 });

    expect(result).to.deep.equal({
      errors: [
        {
          message:
            'Variable "$color" of type "Int!" used in position expecting type "Color".',
          locations: [
            { line: 1, column: 8 },
            { line: 1, column: 44 },
          ],
        },
      ],
    });
  });

  it('enum value may have an internal value of 0', () => {
    const result = executeQuery(`
      {
        colorEnum(fromEnum: RED)
        colorInt(fromEnum: RED)
      }
    `);

    expect(result).to.deep.equal({
      data: {
        colorEnum: 'RED',
        colorInt: 0,
      },
    });
  });

  it('enum inputs may be nullable', () => {
    const result = executeQuery(`
      {
        colorEnum
        colorInt
      }
    `);

    expect(result).to.deep.equal({
      data: {
        colorEnum: null,
        colorInt: null,
      },
    });
  });

  it('presents a getValues() API for complex enums', () => {
    const values = ComplexEnum.getValues();
    expect(values).to.have.deep.ordered.members([
      {
        name: 'ONE',
        description: undefined,
        value: Complex1,
        isDeprecated: false,
        deprecationReason: undefined,
        extensions: undefined,
        astNode: undefined,
      },
      {
        name: 'TWO',
        description: undefined,
        value: Complex2,
        isDeprecated: false,
        deprecationReason: undefined,
        extensions: undefined,
        astNode: undefined,
      },
    ]);
  });

  it('presents a getValue() API for complex enums', () => {
    const oneValue = ComplexEnum.getValue('ONE');
    expect(oneValue).to.include({ name: 'ONE', value: Complex1 });

    // $FlowExpectedError[incompatible-call]
    const badUsage = ComplexEnum.getValue(Complex1);
    expect(badUsage).to.equal(undefined);
  });

  it('may be internally represented with complex values', () => {
    const result = executeQuery(`
      {
        first: complexEnum
        second: complexEnum(fromEnum: TWO)
        good: complexEnum(provideGoodValue: true)
        bad: complexEnum(provideBadValue: true)
      }
    `);

    expect(result).to.deep.equal({
      data: {
        first: 'ONE',
        second: 'TWO',
        good: 'TWO',
        bad: null,
      },
      errors: [
        {
          message:
            'Enum "Complex" cannot represent value: { someRandomValue: 123 }',
          locations: [{ line: 6, column: 9 }],
          path: ['bad'],
        },
      ],
    });
  });

  it('can be introspected without error', () => {
    expect(() => introspectionFromSchema(schema)).to.not.throw();
  });
});
