import { describe, it } from 'node:test';

import { expect } from 'chai';

import { invariant } from '../../jsutils/invariant.ts';
import type { ReadOnlyObjMap } from '../../jsutils/ObjMap.ts';

import { GraphQLError } from '../../error/GraphQLError.ts';

import type {
  FragmentArgumentNode,
  FragmentSpreadNode,
  ValueNode,
  VariableDefinitionNode,
} from '../../language/ast.ts';
import { Kind } from '../../language/kinds.ts';
import { Parser, parseValue as _parseValue } from '../../language/parser.ts';
import { TokenKind } from '../../language/tokenKind.ts';

import { GraphQLInt } from '../../type/scalars.ts';
import { GraphQLSchema } from '../../type/schema.ts';

import { getVariableSignature } from '../../execution/getVariableSignature.ts';
import {
  getFragmentVariableValues,
  getVariableValues,
} from '../../execution/values.ts';

import { replaceVariables } from '../replaceVariables.ts';

function parseValue(ast: string): ValueNode {
  return _parseValue(ast, { noLocation: true });
}

function testVariables(variableDefs: string, inputs: ReadOnlyObjMap<unknown>) {
  const variableValuesOrErrors = getVariableValues(
    new GraphQLSchema({ types: [GraphQLInt] }),
    parseVariableDefinitions(variableDefs),
    inputs,
  );
  invariant(variableValuesOrErrors.variableValues !== undefined);
  return variableValuesOrErrors.variableValues;
}

function parseVariableDefinitions(
  variableDefs: string,
): ReadonlyArray<VariableDefinitionNode> {
  const parser = new Parser(variableDefs, { noLocation: true });
  parser.expectToken(TokenKind.SOF);
  return parser.parseVariableDefinitions() ?? [];
}

function testFragmentVariables(variableDefs: string, fragmentArgs: string) {
  const schema = new GraphQLSchema({ types: [GraphQLInt] });
  const fragmentSignatures = Object.create(null);
  for (const varDef of parseVariableDefinitions(variableDefs)) {
    const signature = getVariableSignature(schema, varDef);
    invariant(!(signature instanceof GraphQLError));
    fragmentSignatures[signature.name] = signature;
  }
  const spread: FragmentSpreadNode = {
    kind: Kind.FRAGMENT_SPREAD,
    name: { kind: Kind.NAME, value: 'TestFragment' },
    arguments: parseFragmentArguments(fragmentArgs),
  };
  return getFragmentVariableValues(
    spread,
    fragmentSignatures,
    Object.create(null),
    undefined,
  );
}

function parseFragmentArguments(
  fragmentArguments: string,
): ReadonlyArray<FragmentArgumentNode> {
  const parser = new Parser(fragmentArguments, { noLocation: true });
  parser.expectToken(TokenKind.SOF);
  return parser.parseFragmentArguments() ?? [];
}

describe('replaceVariables', () => {
  describe('Operation Variables', () => {
    it('does not change simple AST', () => {
      const ast = parseValue('null');
      expect(replaceVariables(ast, undefined)).to.equal(ast);
    });

    it('replaces simple Variables', () => {
      const ast = parseValue('$var');
      const vars = testVariables('($var: Int)', { var: 123 });
      expect(replaceVariables(ast, vars)).to.deep.equal(parseValue('123'));
    });

    it('replaces Variables with default values', () => {
      const ast = parseValue('$var');
      const vars = testVariables('($var: Int = 123)', {});
      expect(replaceVariables(ast, vars)).to.deep.equal(parseValue('123'));
    });

    it('replaces nested Variables', () => {
      const ast = parseValue('{ foo: [ $var ], bar: $var }');
      const vars = testVariables('($var: Int)', { var: 123 });
      expect(replaceVariables(ast, vars)).to.deep.equal(
        parseValue('{ foo: [ 123 ], bar: 123 }'),
      );
    });

    it('replaces missing Variables with null', () => {
      const ast = parseValue('$var');
      expect(replaceVariables(ast, undefined)).to.deep.equal(
        parseValue('null'),
      );
    });

    it('replaces missing variable declaration with null', () => {
      const ast = parseValue('$var');
      const vars = testVariables('', {});
      expect(replaceVariables(ast, vars)).to.deep.equal(parseValue('null'));
    });

    it('replaces misspelled variable declaration with null', () => {
      const ast = parseValue('$var1');
      const vars = testVariables('($var2: Int)', { var2: 123 });
      expect(replaceVariables(ast, vars)).to.deep.equal(parseValue('null'));
    });

    it('replaces missing Variables in lists with null', () => {
      const ast = parseValue('[1, $var]');
      expect(replaceVariables(ast, undefined)).to.deep.equal(
        parseValue('[1, null]'),
      );
    });

    it('omits missing Variables from objects', () => {
      const ast = parseValue('{ foo: 1, bar: $var }');
      const vars = testVariables('($wrongVar: Int)', { var: 123 });
      expect(replaceVariables(ast, vars)).to.deep.equal(
        parseValue('{ foo: 1 }'),
      );
    });
  });

  describe('Fragment Variables', () => {
    it('replaces simple Fragment Variables', () => {
      const ast = parseValue('$var');
      const fragmentVars = testFragmentVariables('($var: Int)', `(var: 123)`);
      expect(replaceVariables(ast, undefined, fragmentVars)).to.deep.equal(
        parseValue('123'),
      );
    });

    it('replaces simple Fragment Variables even when overlapping with Operation Variables', () => {
      const ast = parseValue('$var');
      const operationVars = testVariables('($var: Int)', { var: 123 });
      const fragmentVars = testFragmentVariables('($var: Int)', '(var: 456)');
      expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal(
        parseValue('456'),
      );
    });

    it('replaces Fragment Variables with default values', () => {
      const ast = parseValue('$var');
      const fragmentVars = testFragmentVariables('($var: Int = 123)', '');
      expect(replaceVariables(ast, undefined, fragmentVars)).to.deep.equal(
        parseValue('123'),
      );
    });

    it('replaces Fragment Variables with default values even when overlapping with Operation Variables', () => {
      const ast = parseValue('$var');
      const operationVars = testVariables('($var: Int = 123)', {});
      const fragmentVars = testFragmentVariables('($var: Int = 456)', '');
      expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal(
        parseValue('456'),
      );
    });

    it('replaces nested Fragment Variables', () => {
      const ast = parseValue('{ foo: [ $var ], bar: $var }');
      const fragmentVars = testFragmentVariables('($var: Int)', '(var: 123)');
      expect(replaceVariables(ast, undefined, fragmentVars)).to.deep.equal(
        parseValue('{ foo: [ 123 ], bar: 123 }'),
      );
    });

    it('replaces nested Fragment Variables even when overlapping with Operation Variables', () => {
      const ast = parseValue('{ foo: [ $var ], bar: $var }');
      const operationVars = testVariables('($var: Int)', { var: 123 });
      const fragmentVars = testFragmentVariables('($var: Int)', '(var: 456)');
      expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal(
        parseValue('{ foo: [ 456 ], bar: 456 }'),
      );
    });

    it('replaces missing Fragment Variables with null', () => {
      const ast = parseValue('$var');
      expect(replaceVariables(ast, undefined, undefined)).to.deep.equal(
        parseValue('null'),
      );
    });

    it('replaces missing Fragment Variables with null even when overlapping with Operation Variables', () => {
      const ast = parseValue('$var');
      const operationVars = testVariables('($var: Int)', { var: 123 });
      const fragmentVars = testFragmentVariables('($var: Int)', '');
      expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal(
        parseValue('null'),
      );
    });

    it('replaces missing Fragment Variables in lists with null', () => {
      const ast = parseValue('[1, $var]');
      expect(replaceVariables(ast, undefined, undefined)).to.deep.equal(
        parseValue('[1, null]'),
      );
    });

    it('replaces missing Fragment Variables in lists with null even when overlapping with Operation Variables', () => {
      const ast = parseValue('[1, $var]');
      const operationVars = testVariables('($var: Int)', { var: 123 });
      const fragmentVars = testFragmentVariables('($var: Int)', '');
      expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal(
        parseValue('[1, null]'),
      );
    });

    it('omits missing Fragment Variables from objects', () => {
      const ast = parseValue('{ foo: 1, bar: $var }');
      expect(replaceVariables(ast, undefined, undefined)).to.deep.equal(
        parseValue('{ foo: 1 }'),
      );
    });

    it('omits missing Fragment Variables from objects even when overlapping with Operation Variables', () => {
      const ast = parseValue('{ foo: 1, bar: $var }');
      const operationVars = testVariables('($var: Int)', { var: 123 });
      const fragmentVars = testFragmentVariables('($var: Int)', '');
      expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal(
        parseValue('{ foo: 1 }'),
      );
    });
  });
});