describe('@jsdoc/parse/lib/ast-node', () => {
  const astNode = require('../../../lib/ast-node');
  const babelParser = require('@babel/parser');
  const { parserOptions } = require('../../../lib/ast-builder');
  const { Syntax } = require('../../../lib/syntax');

  function parse(str) {
    return babelParser.parse(str, parserOptions).program.body[0];
  }

  // create the AST nodes we'll be testing
  const arrayExpression = parse('[,]').expression;
  const arrowFunctionExpression = parse('var foo = () => {};').declarations[0].init;
  const assignmentExpression = parse('foo = 1;').expression;
  const binaryExpression = parse('foo & foo;').expression;
  const experimentalObjectRestSpread = parse('var one = {...two, three: 4};').declarations[0].init;
  const functionDeclaration1 = parse('function foo() {}');
  const functionDeclaration2 = parse('function foo(bar) {}');
  const functionDeclaration3 = parse('function foo(bar, baz, qux) {}');
  const functionDeclaration4 = parse('function foo(...bar) {}');
  const functionExpression1 = parse('var foo = function() {};').declarations[0].init;
  const functionExpression2 = parse('var foo = function(bar) {};').declarations[0].init;
  const identifier = parse('foo;').expression;
  const literal = parse('1;').expression;
  const memberExpression = parse('foo.bar;').expression;
  const memberExpressionComputed1 = parse('foo["bar"];').expression;
  const memberExpressionComputed2 = parse("foo['bar'];").expression;
  const methodDefinition1 = parse('class Foo { bar() {} }').body.body[0];
  const methodDefinition2 = parse('var foo = () => class { bar() {} };').declarations[0].init.body
    .body[0];
  const propertyGet = parse('var foo = { get bar() {} };').declarations[0].init.properties[0];
  const propertyInit = parse('var foo = { bar: {} };').declarations[0].init.properties[0];
  const propertySet = parse('var foo = { set bar(a) {} };').declarations[0].init.properties[0];
  const thisExpression = parse('this;').expression;
  const unaryExpression1 = parse('+1;').expression;
  const unaryExpression2 = parse('+foo;').expression;
  const variableDeclarator1 = parse('var foo = 1;').declarations[0];
  const variableDeclarator2 = parse('var foo;').declarations[0];

  it('should exist', () => {
    expect(astNode).toBeObject();
  });

  it('should export an addNodeProperties method', () => {
    expect(astNode.addNodeProperties).toBeFunction();
  });

  it('should export a getInfo method', () => {
    expect(astNode.getInfo).toBeFunction();
  });

  it('should export a getParamNames method', () => {
    expect(astNode.getParamNames).toBeFunction();
  });

  it('should export an isAccessor method', () => {
    expect(astNode.isAccessor).toBeFunction();
  });

  it('should export an isAssignment method', () => {
    expect(astNode.isAssignment).toBeFunction();
  });

  it('should export an isFunction method', () => {
    expect(astNode.isFunction).toBeFunction();
  });

  it('should export an isScope method', () => {
    expect(astNode.isScope).toBeFunction();
  });

  it('should export a nodeToString method', () => {
    expect(astNode.nodeToString).toBeFunction();
  });

  it('should export a nodeToValue method', () => {
    expect(astNode.nodeToValue).toBeFunction();
  });

  describe('addNodeProperties', () => {
    it('should return null for undefined input', () => {
      expect(astNode.addNodeProperties()).toBe(null);
    });

    it('should return null if the input is not an object', () => {
      expect(astNode.addNodeProperties('foo')).toBe(null);
    });

    it('should preserve existing properties that are not "node properties"', () => {
      const node = astNode.addNodeProperties({ foo: 1 });

      expect(node).toBeObject();
      expect(node.foo).toBe(1);
    });

    it('should add a nodeId if necessary', () => {
      const node = astNode.addNodeProperties({});
      const descriptor = Object.getOwnPropertyDescriptor(node, 'nodeId');

      expect(descriptor).toBeObject();
      expect(descriptor.value).toBeString();
      expect(descriptor.enumerable).toBeTrue();
    });

    it('should not overwrite an existing nodeId', () => {
      const nodeId = 'foo';
      const node = astNode.addNodeProperties({ nodeId: nodeId });

      expect(node.nodeId).toBe(nodeId);
    });

    it('should add a non-enumerable, writable parent if necessary', () => {
      const node = astNode.addNodeProperties({});
      const descriptor = Object.getOwnPropertyDescriptor(node, 'parent');

      expect(descriptor).toBeDefined();
      expect(descriptor.value).toBeUndefined();
      expect(descriptor.enumerable).toBeFalse();
      expect(descriptor.writable).toBeTrue();
    });

    it('should not overwrite an existing parent', () => {
      const parent = {};
      const node = astNode.addNodeProperties({ parent: parent });

      expect(node.parent).toBe(parent);
    });

    it('should not overwrite a null parent', () => {
      const node = astNode.addNodeProperties({ parent: null });

      expect(node.parent).toBeNull();
    });

    it('should add an enumerable parentId', () => {
      const node = astNode.addNodeProperties({});
      const descriptor = Object.getOwnPropertyDescriptor(node, 'parentId');

      expect(descriptor).toBeObject();
      expect(descriptor.enumerable).toBeTrue();
    });

    it('should provide a null parentId for nodes with no parent', () => {
      const node = astNode.addNodeProperties({});

      expect(node.parentId).toBeNull();
    });

    it('should provide a non-null parentId for nodes with a parent', () => {
      const node = astNode.addNodeProperties({});
      const parent = astNode.addNodeProperties({});

      node.parent = parent;

      expect(node.parentId).toBe(parent.nodeId);
    });

    it('should add a non-enumerable, writable enclosingScope if necessary', () => {
      const node = astNode.addNodeProperties({});
      const descriptor = Object.getOwnPropertyDescriptor(node, 'enclosingScope');

      expect(descriptor).toBeObject();
      expect(descriptor.value).toBeUndefined();
      expect(descriptor.enumerable).toBeFalse();
      expect(descriptor.writable).toBeTrue();
    });

    it('should not overwrite an existing enclosingScope', () => {
      const enclosingScope = {};
      const node = astNode.addNodeProperties({ enclosingScope: enclosingScope });

      expect(node.enclosingScope).toBe(enclosingScope);
    });

    it('should not overwrite a null enclosingScope', () => {
      const node = astNode.addNodeProperties({ enclosingScope: null });

      expect(node.enclosingScope).toBeNull();
    });

    it('should add an enumerable enclosingScopeId', () => {
      const node = astNode.addNodeProperties({});
      const descriptor = Object.getOwnPropertyDescriptor(node, 'enclosingScopeId');

      expect(descriptor).toBeObject();
      expect(descriptor.enumerable).toBeTrue();
    });

    it('should provide a null enclosingScopeId for nodes with no enclosing scope', () => {
      const node = astNode.addNodeProperties({});

      expect(node.enclosingScopeId).toBeNull();
    });

    it('should provide a non-null enclosingScopeId for nodes with an enclosing scope', () => {
      const node = astNode.addNodeProperties({});
      const enclosingScope = astNode.addNodeProperties({});

      node.enclosingScope = enclosingScope;

      expect(node.enclosingScopeId).toBe(enclosingScope.nodeId);
    });
  });

  describe('getInfo', () => {
    it('should throw an error for undefined input', () => {
      function noNode() {
        astNode.getInfo();
      }

      expect(noNode).toThrow();
    });

    it('should return the correct info for an AssignmentExpression', () => {
      const info = astNode.getInfo(assignmentExpression);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.Literal);
      expect(info.node.value).toBe(1);

      expect(info.name).toBe('foo');
      expect(info.type).toBe(Syntax.Literal);
      expect(info.value).toBe(1);
    });

    it('should return the correct info for a FunctionDeclaration', () => {
      const info = astNode.getInfo(functionDeclaration2);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.FunctionDeclaration);

      expect(info.name).toBe('foo');
      expect(info.type).toBe(Syntax.FunctionDeclaration);
      expect(info.value).toBeUndefined();

      expect(info.paramnames).toBeArrayOfSize(1);
      expect(info.paramnames[0]).toBe('bar');
    });

    it('should return the correct info for a FunctionExpression', () => {
      const info = astNode.getInfo(functionExpression2);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.FunctionExpression);

      expect(info.name).toBe('');
      expect(info.type).toBe(Syntax.FunctionExpression);
      expect(info.value).toBeUndefined();

      expect(info.paramnames).toBeArrayOfSize(1);
      expect(info.paramnames[0]).toBe('bar');
    });

    it('should return the correct info for a MemberExpression', () => {
      const info = astNode.getInfo(memberExpression);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.MemberExpression);

      expect(info.name).toBe('foo.bar');
      expect(info.type).toBe(Syntax.MemberExpression);
    });

    it('should return the correct info for a computed MemberExpression', () => {
      const info = astNode.getInfo(memberExpressionComputed1);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.MemberExpression);

      expect(info.name).toBe('foo["bar"]');
      expect(info.type).toBe(Syntax.MemberExpression);
    });

    it('should return the correct info for a Property initializer', () => {
      const info = astNode.getInfo(propertyInit);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.ObjectExpression);

      expect(info.name).toBe('bar');
      expect(info.type).toBe(Syntax.ObjectExpression);
    });

    it('should return the correct info for a Property setter', () => {
      const info = astNode.getInfo(propertySet);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.FunctionExpression);

      expect(info.name).toBe('bar');
      expect(info.type).toBeUndefined();
      expect(info.value).toBeUndefined();

      expect(info.paramnames).toBeArrayOfSize(1);
      expect(info.paramnames[0]).toBe('a');
    });

    it('should return the correct info for a VariableDeclarator with a value', () => {
      const info = astNode.getInfo(variableDeclarator1);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.Literal);

      expect(info.name).toBe('foo');
      expect(info.type).toBe(Syntax.Literal);
      expect(info.value).toBe(1);
    });

    it('should return the correct info for a VariableDeclarator with no value', () => {
      const info = astNode.getInfo(variableDeclarator2);

      expect(info).toBeObject();

      expect(info.node).toBeObject();
      expect(info.node.type).toBe(Syntax.Identifier);

      expect(info.name).toBe('foo');
      expect(info.type).toBeUndefined();
      expect(info.value).toBeUndefined();
    });

    it('should return the correct info for other node types', () => {
      const info = astNode.getInfo(binaryExpression);

      expect(info).toBeObject();

      expect(info.node).toBe(binaryExpression);
      expect(info.type).toBe(Syntax.BinaryExpression);
    });
  });

  describe('getParamNames', () => {
    it('should return an empty array for undefined input', () => {
      const params = astNode.getParamNames();

      expect(params).toBeEmptyArray();
    });

    it('should return an empty array if the input has no params property', () => {
      const params = astNode.getParamNames({});

      expect(params).toBeEmptyArray();
    });

    it('should return an empty array if the input has no params', () => {
      const params = astNode.getParamNames(functionDeclaration1);

      expect(params).toBeEmptyArray();
    });

    it('should return a single-item array if the input has a single param', () => {
      const params = astNode.getParamNames(functionDeclaration2);

      expect(params).toEqual(['bar']);
    });

    it('should return a multi-item array if the input has multiple params', () => {
      const params = astNode.getParamNames(functionDeclaration3);

      expect(params).toEqual(['bar', 'baz', 'qux']);
    });

    it('should include rest parameters', () => {
      const params = astNode.getParamNames(functionDeclaration4);

      expect(params).toEqual(['bar']);
    });
  });

  describe('isAccessor', () => {
    it('should return false for undefined values', () => {
      expect(astNode.isAccessor()).toBeFalse();
    });

    it('should return false if the parameter is not an object', () => {
      expect(astNode.isAccessor('foo')).toBeFalse();
    });

    it('should return false for non-Property nodes', () => {
      expect(astNode.isAccessor(binaryExpression)).toBeFalse();
    });

    it('should return false for Property nodes whose kind is "init"', () => {
      expect(astNode.isAccessor(propertyInit)).toBeFalse();
    });

    it('should return true for Property nodes whose kind is "get"', () => {
      expect(astNode.isAccessor(propertyGet)).toBeTrue();
    });

    it('should return true for Property nodes whose kind is "set"', () => {
      expect(astNode.isAccessor(propertySet)).toBeTrue();
    });
  });

  describe('isAssignment', () => {
    it('should return false for undefined values', () => {
      expect(astNode.isAssignment()).toBeFalse();
    });

    it('should return false if the parameter is not an object', () => {
      expect(astNode.isAssignment('foo')).toBeFalse();
    });

    it('should return false for nodes that are not assignments', () => {
      expect(astNode.isAssignment(binaryExpression)).toBeFalse();
    });

    it('should return true for AssignmentExpression nodes', () => {
      expect(astNode.isAssignment(assignmentExpression)).toBeTrue();
    });

    it('should return true for VariableDeclarator nodes', () => {
      expect(astNode.isAssignment(variableDeclarator1)).toBeTrue();
    });
  });

  describe('isFunction', () => {
    it('should recognize function declarations as functions', () => {
      expect(astNode.isFunction(functionDeclaration1)).toBeTrue();
    });

    it('should recognize function expressions as functions', () => {
      expect(astNode.isFunction(functionExpression1)).toBeTrue();
    });

    it('should recognize method definitions as functions', () => {
      expect(astNode.isFunction(methodDefinition1)).toBeTrue();
    });

    it('should recognize arrow function expressions as functions', () => {
      expect(astNode.isFunction(arrowFunctionExpression)).toBeTrue();
    });

    it('should recognize non-functions', () => {
      expect(astNode.isFunction(arrayExpression)).toBeFalse();
    });
  });

  describe('isScope', () => {
    it('should return false for undefined values', () => {
      expect(astNode.isScope()).toBeFalse();
    });

    it('should return false if the parameter is not an object', () => {
      expect(astNode.isScope('foo')).toBeFalse();
    });

    it('should return true for CatchClause nodes', () => {
      expect(astNode.isScope({ type: Syntax.CatchClause })).toBeTrue();
    });

    it('should return true for FunctionDeclaration nodes', () => {
      expect(astNode.isScope({ type: Syntax.FunctionDeclaration })).toBeTrue();
    });

    it('should return true for FunctionExpression nodes', () => {
      expect(astNode.isScope({ type: Syntax.FunctionExpression })).toBeTrue();
    });

    it('should return false for other nodes', () => {
      expect(astNode.isScope({ type: Syntax.NameExpression })).toBeFalse();
    });
  });

  describe('nodeToString', () => {
    it('should be an alias to nodeToValue', () => {
      expect(astNode.nodeToString).toBe(astNode.nodeToValue);
    });
  });

  describe('nodeToValue', () => {
    it('should return `[null]` for the sparse array `[,]`', () => {
      expect(astNode.nodeToValue(arrayExpression)).toBe('[null]');
    });

    it('should return the variable name for assignment expressions', () => {
      expect(astNode.nodeToValue(assignmentExpression)).toBe('foo');
    });

    it('should return the function name for function declarations', () => {
      expect(astNode.nodeToValue(functionDeclaration1)).toBe('foo');
    });

    it('should return undefined for anonymous function expressions', () => {
      expect(astNode.nodeToValue(functionExpression1)).toBeUndefined();
    });

    it('should return the identifier name for identifiers', () => {
      expect(astNode.nodeToValue(identifier)).toBe('foo');
    });

    it('should return the literal value for literals', () => {
      expect(astNode.nodeToValue(literal)).toBe(1);
    });

    it('should return the object and property for noncomputed member expressions', () => {
      expect(astNode.nodeToValue(memberExpression)).toBe('foo.bar');
    });

    it(
      'should return the object and property, with a computed property that uses the same ' +
        'quote character as the original source, for computed member expressions',
      () => {
        expect(astNode.nodeToValue(memberExpressionComputed1)).toBe('foo["bar"]');
        expect(astNode.nodeToValue(memberExpressionComputed2)).toBe("foo['bar']");
      }
    );

    // TODO: we can't test this here because JSDoc, not Babylon, adds the `parent` property to
    // nodes. also, we currently return an empty string instead of `<anonymous>` in this case;
    // see `module:@jsdoc/parse.astNode.nodeToValue` and the comment on
    // `Syntax.MethodDefinition` for details
    xit(
      'should return `<anonymous>` for method definitions inside classes that were ' +
        'returned by an arrow function expression',
      () => {
        expect(astNode.nodeToValue(methodDefinition2)).toBe('<anonymous>');
      }
    );

    it('should return "this" for this expressions', () => {
      expect(astNode.nodeToValue(thisExpression)).toBe('this');
    });

    it('should return the operator and nodeToValue value for prefix unary expressions', () => {
      expect(astNode.nodeToValue(unaryExpression1)).toBe('+1');
      expect(astNode.nodeToValue(unaryExpression2)).toBe('+foo');
    });

    it('should throw an error for postfix unary expressions', () => {
      function postfixNodeToValue() {
        // there's no valid source representation for this one, so we fake it
        const unaryExpressionPostfix = (() => {
          const node = parse('+1;').body[0].expression;

          node.prefix = false;

          return node;
        })();

        return astNode.nodeToValue(unaryExpressionPostfix);
      }

      expect(postfixNodeToValue).toThrow();
    });

    it('should return the variable name for variable declarators', () => {
      expect(astNode.nodeToValue(variableDeclarator1)).toBe('foo');
    });

    it('should return an empty string for all other nodes', () => {
      expect(astNode.nodeToValue(binaryExpression)).toBe('');
    });

    it('should understand and ignore ExperimentalSpreadProperty', () => {
      expect(astNode.nodeToValue(experimentalObjectRestSpread)).toBe('{"three":4}');
    });
  });
});
