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

import type { GraphQLInputType } from '../../type/definition';
import {
  GraphQLEnumType,
  GraphQLInputObjectType,
  GraphQLList,
  GraphQLNonNull,
  GraphQLScalarType,
} from '../../type/definition';
import { GraphQLInt } from '../../type/scalars';

import { coerceInputValue } from '../coerceInputValue';

interface CoerceResult {
  value: unknown;
  errors: ReadonlyArray<CoerceError>;
}

interface CoerceError {
  path: ReadonlyArray<string | number>;
  value: unknown;
  error: string;
}

function coerceValue(
  inputValue: unknown,
  type: GraphQLInputType,
): CoerceResult {
  const errors: Array<CoerceError> = [];
  const value = coerceInputValue(
    inputValue,
    type,
    (path, invalidValue, error) => {
      errors.push({ path, value: invalidValue, error: error.message });
    },
  );

  return { errors, value };
}

function expectValue(result: CoerceResult) {
  expect(result.errors).to.deep.equal([]);
  return expect(result.value);
}

function expectErrors(result: CoerceResult) {
  return expect(result.errors);
}

describe('coerceInputValue', () => {
  describe('for GraphQLNonNull', () => {
    const TestNonNull = new GraphQLNonNull(GraphQLInt);

    it('returns no error for non-null value', () => {
      const result = coerceValue(1, TestNonNull);
      expectValue(result).to.equal(1);
    });

    it('returns an error for undefined value', () => {
      const result = coerceValue(undefined, TestNonNull);
      expectErrors(result).to.deep.equal([
        {
          error: 'Expected non-nullable type "Int!" not to be null.',
          path: [],
          value: undefined,
        },
      ]);
    });

    it('returns an error for null value', () => {
      const result = coerceValue(null, TestNonNull);
      expectErrors(result).to.deep.equal([
        {
          error: 'Expected non-nullable type "Int!" not to be null.',
          path: [],
          value: null,
        },
      ]);
    });
  });

  describe('for GraphQLScalar', () => {
    const TestScalar = new GraphQLScalarType({
      name: 'TestScalar',
      parseValue(input: any) {
        if (input.error != null) {
          throw new Error(input.error);
        }
        return input.value;
      },
    });

    it('returns no error for valid input', () => {
      const result = coerceValue({ value: 1 }, TestScalar);
      expectValue(result).to.equal(1);
    });

    it('returns no error for null result', () => {
      const result = coerceValue({ value: null }, TestScalar);
      expectValue(result).to.equal(null);
    });

    it('returns no error for NaN result', () => {
      const result = coerceValue({ value: NaN }, TestScalar);
      expectValue(result).to.satisfy(Number.isNaN);
    });

    it('returns an error for undefined result', () => {
      const result = coerceValue({ value: undefined }, TestScalar);
      expectErrors(result).to.deep.equal([
        {
          error: 'Expected type "TestScalar".',
          path: [],
          value: { value: undefined },
        },
      ]);
    });

    it('returns an error for undefined result', () => {
      const inputValue = { error: 'Some error message' };
      const result = coerceValue(inputValue, TestScalar);
      expectErrors(result).to.deep.equal([
        {
          error: 'Expected type "TestScalar". Some error message',
          path: [],
          value: { error: 'Some error message' },
        },
      ]);
    });
  });

  describe('for GraphQLEnum', () => {
    const TestEnum = new GraphQLEnumType({
      name: 'TestEnum',
      values: {
        FOO: { value: 'InternalFoo' },
        BAR: { value: 123456789 },
      },
    });

    it('returns no error for a known enum name', () => {
      const fooResult = coerceValue('FOO', TestEnum);
      expectValue(fooResult).to.equal('InternalFoo');

      const barResult = coerceValue('BAR', TestEnum);
      expectValue(barResult).to.equal(123456789);
    });

    it('returns an error for misspelled enum value', () => {
      const result = coerceValue('foo', TestEnum);
      expectErrors(result).to.deep.equal([
        {
          error:
            'Value "foo" does not exist in "TestEnum" enum. Did you mean the enum value "FOO"?',
          path: [],
          value: 'foo',
        },
      ]);
    });

    it('returns an error for incorrect value type', () => {
      const result1 = coerceValue(123, TestEnum);
      expectErrors(result1).to.deep.equal([
        {
          error: 'Enum "TestEnum" cannot represent non-string value: 123.',
          path: [],
          value: 123,
        },
      ]);

      const result2 = coerceValue({ field: 'value' }, TestEnum);
      expectErrors(result2).to.deep.equal([
        {
          error:
            'Enum "TestEnum" cannot represent non-string value: { field: "value" }.',
          path: [],
          value: { field: 'value' },
        },
      ]);
    });
  });

  describe('for GraphQLInputObject', () => {
    const TestInputObject = new GraphQLInputObjectType({
      name: 'TestInputObject',
      fields: {
        foo: { type: new GraphQLNonNull(GraphQLInt) },
        bar: { type: GraphQLInt },
      },
    });

    it('returns no error for a valid input', () => {
      const result = coerceValue({ foo: 123 }, TestInputObject);
      expectValue(result).to.deep.equal({ foo: 123 });
    });

    it('returns an error for a non-object type', () => {
      const result = coerceValue(123, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error: 'Expected type "TestInputObject" to be an object.',
          path: [],
          value: 123,
        },
      ]);
    });

    it('returns an error for an invalid field', () => {
      const result = coerceValue({ foo: NaN }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error: 'Int cannot represent non-integer value: NaN',
          path: ['foo'],
          value: NaN,
        },
      ]);
    });

    it('returns multiple errors for multiple invalid fields', () => {
      const result = coerceValue({ foo: 'abc', bar: 'def' }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error: 'Int cannot represent non-integer value: "abc"',
          path: ['foo'],
          value: 'abc',
        },
        {
          error: 'Int cannot represent non-integer value: "def"',
          path: ['bar'],
          value: 'def',
        },
      ]);
    });

    it('returns error for a missing required field', () => {
      const result = coerceValue({ bar: 123 }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error: 'Field "foo" of required type "Int!" was not provided.',
          path: [],
          value: { bar: 123 },
        },
      ]);
    });

    it('returns error for an unknown field', () => {
      const result = coerceValue(
        { foo: 123, unknownField: 123 },
        TestInputObject,
      );
      expectErrors(result).to.deep.equal([
        {
          error:
            'Field "unknownField" is not defined by type "TestInputObject".',
          path: [],
          value: { foo: 123, unknownField: 123 },
        },
      ]);
    });

    it('returns error for a misspelled field', () => {
      const result = coerceValue({ foo: 123, bart: 123 }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error:
            'Field "bart" is not defined by type "TestInputObject". Did you mean "bar"?',
          path: [],
          value: { foo: 123, bart: 123 },
        },
      ]);
    });
  });

  describe('for GraphQLInputObject that isOneOf', () => {
    const TestInputObject = new GraphQLInputObjectType({
      name: 'TestInputObject',
      fields: {
        foo: { type: GraphQLInt },
        bar: { type: GraphQLInt },
      },
      isOneOf: true,
    });

    it('returns no error for a valid input', () => {
      const result = coerceValue({ foo: 123 }, TestInputObject);
      expectValue(result).to.deep.equal({ foo: 123 });
    });

    it('returns an error if more than one field is specified', () => {
      const result = coerceValue({ foo: 123, bar: null }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error:
            'Exactly one key must be specified for OneOf type "TestInputObject".',
          path: [],
          value: { foo: 123, bar: null },
        },
      ]);
    });

    it('returns an error the one field is null', () => {
      const result = coerceValue({ bar: null }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error: 'Field "bar" must be non-null.',
          path: ['bar'],
          value: null,
        },
      ]);
    });

    it('returns an error for an invalid field', () => {
      const result = coerceValue({ foo: NaN }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error: 'Int cannot represent non-integer value: NaN',
          path: ['foo'],
          value: NaN,
        },
      ]);
    });

    it('returns multiple errors for multiple invalid fields', () => {
      const result = coerceValue({ foo: 'abc', bar: 'def' }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error: 'Int cannot represent non-integer value: "abc"',
          path: ['foo'],
          value: 'abc',
        },
        {
          error: 'Int cannot represent non-integer value: "def"',
          path: ['bar'],
          value: 'def',
        },
        {
          error:
            'Exactly one key must be specified for OneOf type "TestInputObject".',
          path: [],
          value: { foo: 'abc', bar: 'def' },
        },
      ]);
    });

    it('returns error for an unknown field', () => {
      const result = coerceValue(
        { foo: 123, unknownField: 123 },
        TestInputObject,
      );
      expectErrors(result).to.deep.equal([
        {
          error:
            'Field "unknownField" is not defined by type "TestInputObject".',
          path: [],
          value: { foo: 123, unknownField: 123 },
        },
      ]);
    });

    it('returns error for a misspelled field', () => {
      const result = coerceValue({ bart: 123 }, TestInputObject);
      expectErrors(result).to.deep.equal([
        {
          error:
            'Field "bart" is not defined by type "TestInputObject". Did you mean "bar"?',
          path: [],
          value: { bart: 123 },
        },
        {
          error:
            'Exactly one key must be specified for OneOf type "TestInputObject".',
          path: [],
          value: { bart: 123 },
        },
      ]);
    });
  });

  describe('for GraphQLInputObject with default value', () => {
    const makeTestInputObject = (defaultValue: any) =>
      new GraphQLInputObjectType({
        name: 'TestInputObject',
        fields: {
          foo: {
            type: new GraphQLScalarType({ name: 'TestScalar' }),
            defaultValue,
          },
        },
      });

    it('returns no errors for valid input value', () => {
      const result = coerceValue({ foo: 5 }, makeTestInputObject(7));
      expectValue(result).to.deep.equal({ foo: 5 });
    });

    it('returns object with default value', () => {
      const result = coerceValue({}, makeTestInputObject(7));
      expectValue(result).to.deep.equal({ foo: 7 });
    });

    it('returns null as value', () => {
      const result = coerceValue({}, makeTestInputObject(null));
      expectValue(result).to.deep.equal({ foo: null });
    });

    it('returns NaN as value', () => {
      const result = coerceValue({}, makeTestInputObject(NaN));
      expectValue(result).to.have.property('foo').that.satisfy(Number.isNaN);
    });
  });

  describe('for GraphQLList', () => {
    const TestList = new GraphQLList(GraphQLInt);

    it('returns no error for a valid input', () => {
      const result = coerceValue([1, 2, 3], TestList);
      expectValue(result).to.deep.equal([1, 2, 3]);
    });

    it('returns no error for a valid iterable input', () => {
      function* listGenerator() {
        yield 1;
        yield 2;
        yield 3;
      }

      const result = coerceValue(listGenerator(), TestList);
      expectValue(result).to.deep.equal([1, 2, 3]);
    });

    it('returns an error for an invalid input', () => {
      const result = coerceValue([1, 'b', true, 4], TestList);
      expectErrors(result).to.deep.equal([
        {
          error: 'Int cannot represent non-integer value: "b"',
          path: [1],
          value: 'b',
        },
        {
          error: 'Int cannot represent non-integer value: true',
          path: [2],
          value: true,
        },
      ]);
    });

    it('returns a list for a non-list value', () => {
      const result = coerceValue(42, TestList);
      expectValue(result).to.deep.equal([42]);
    });

    it('returns a list for a non-list object value', () => {
      const TestListOfObjects = new GraphQLList(
        new GraphQLInputObjectType({
          name: 'TestObject',
          fields: {
            length: { type: GraphQLInt },
          },
        }),
      );

      const result = coerceValue({ length: 100500 }, TestListOfObjects);
      expectValue(result).to.deep.equal([{ length: 100500 }]);
    });

    it('returns an error for a non-list invalid value', () => {
      const result = coerceValue('INVALID', TestList);
      expectErrors(result).to.deep.equal([
        {
          error: 'Int cannot represent non-integer value: "INVALID"',
          path: [],
          value: 'INVALID',
        },
      ]);
    });

    it('returns null for a null value', () => {
      const result = coerceValue(null, TestList);
      expectValue(result).to.deep.equal(null);
    });
  });

  describe('for nested GraphQLList', () => {
    const TestNestedList = new GraphQLList(new GraphQLList(GraphQLInt));

    it('returns no error for a valid input', () => {
      const result = coerceValue([[1], [2, 3]], TestNestedList);
      expectValue(result).to.deep.equal([[1], [2, 3]]);
    });

    it('returns a list for a non-list value', () => {
      const result = coerceValue(42, TestNestedList);
      expectValue(result).to.deep.equal([[42]]);
    });

    it('returns null for a null value', () => {
      const result = coerceValue(null, TestNestedList);
      expectValue(result).to.deep.equal(null);
    });

    it('returns nested lists for nested non-list values', () => {
      const result = coerceValue([1, 2, 3], TestNestedList);
      expectValue(result).to.deep.equal([[1], [2], [3]]);
    });

    it('returns nested null for nested null values', () => {
      const result = coerceValue([42, [null], null], TestNestedList);
      expectValue(result).to.deep.equal([[42], [null], null]);
    });
  });

  describe('with default onError', () => {
    it('throw error without path', () => {
      expect(() =>
        coerceInputValue(null, new GraphQLNonNull(GraphQLInt)),
      ).to.throw(
        'Invalid value null: Expected non-nullable type "Int!" not to be null.',
      );
    });

    it('throw error with path', () => {
      expect(() =>
        coerceInputValue(
          [null],
          new GraphQLList(new GraphQLNonNull(GraphQLInt)),
        ),
      ).to.throw(
        'Invalid value null at "value[0]": Expected non-nullable type "Int!" not to be null.',
      );
    });
  });
});