import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import type { Point, Language, Tree, TreeCursor } from '../src';
import { Parser, Edit } from '../src';
import helper from './helper';

let JavaScript: Language;

interface CursorState {
  nodeType: string;
  nodeIsNamed: boolean;
  startPosition: Point;
  endPosition: Point;
  startIndex: number;
  endIndex: number;
}

describe('Tree', () => {
  let parser: Parser;
  let tree: Tree;

  beforeAll(async () => {
    ({ JavaScript } = await helper);
  });

  beforeEach(() => {
    parser = new Parser();
    parser.setLanguage(JavaScript);
  });

  afterEach(() => {
    parser.delete();
    tree.delete();
  });

  describe('.edit', () => {
    let input: string;
    let edit: Edit;

    it('updates the positions of nodes', () => {
      input = 'abc + cde';
      tree = parser.parse(input)!;
      expect(tree.rootNode.toString()).toBe(
        '(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
      );

      let sumNode = tree.rootNode.firstChild!.firstChild;
      let variableNode1 = sumNode!.firstChild;
      let variableNode2 = sumNode!.lastChild;
      expect(variableNode1!.startIndex).toBe(0);
      expect(variableNode1!.endIndex).toBe(3);
      expect(variableNode2!.startIndex).toBe(6);
      expect(variableNode2!.endIndex).toBe(9);

      [input, edit] = spliceInput(input, input.indexOf('bc'), 0, ' * ');
      expect(input).toBe('a * bc + cde');
      tree.edit(edit);

      sumNode = tree.rootNode.firstChild!.firstChild;
      variableNode1 = sumNode!.firstChild;
      variableNode2 = sumNode!.lastChild;
      expect(variableNode1!.startIndex).toBe(0);
      expect(variableNode1!.endIndex).toBe(6);
      expect(variableNode2!.startIndex).toBe(9);
      expect(variableNode2!.endIndex).toBe(12);

      tree = parser.parse(input, tree)!;
      expect(tree.rootNode.toString()).toBe(
        '(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
      );
    });

    it('handles non-ascii characters', () => {
      input = 'αβδ + cde';

      tree = parser.parse(input)!;
      expect(tree.rootNode.toString()).toBe(
        '(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
      );

      let variableNode = tree.rootNode.firstChild!.firstChild!.lastChild;

      [input, edit] = spliceInput(input, input.indexOf('δ'), 0, '👍 * ');
      expect(input).toBe('αβ👍 * δ + cde');
      tree.edit(edit);

      variableNode = tree.rootNode.firstChild!.firstChild!.lastChild;
      expect(variableNode!.startIndex).toBe(input.indexOf('cde'));

      tree = parser.parse(input, tree)!;
      expect(tree.rootNode.toString()).toBe(
        '(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
      );
    });
  });

  describe('.getChangedRanges(previous)', () => {
    it('reports the ranges of text whose syntactic meaning has changed', () => {
      let sourceCode = 'abcdefg + hij';
      tree = parser.parse(sourceCode)!;

      expect(tree.rootNode.toString()).toBe(
        '(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
      );

      sourceCode = 'abc + defg + hij';
      tree.edit(new Edit({
        startIndex: 2,
        oldEndIndex: 2,
        newEndIndex: 5,
        startPosition: { row: 0, column: 2 },
        oldEndPosition: { row: 0, column: 2 },
        newEndPosition: { row: 0, column: 5 },
      }));

      const tree2 = parser.parse(sourceCode, tree)!;
      expect(tree2.rootNode.toString()).toBe(
        '(program (expression_statement (binary_expression left: (binary_expression left: (identifier) right: (identifier)) right: (identifier))))'
      );

      const ranges = tree.getChangedRanges(tree2);
      expect(ranges).toEqual([
        {
          startIndex: 0,
          endIndex: 'abc + defg'.length,
          startPosition: { row: 0, column: 0 },
          endPosition: { row: 0, column: 'abc + defg'.length },
        },
      ]);

      tree2.delete();
    });

    it('throws an exception if the argument is not a tree', () => {
      tree = parser.parse('abcdefg + hij')!;

      expect(() => {
        tree.getChangedRanges({} as Tree);
      }).toThrow(/Argument must be a Tree/);
    });
  });

  describe('.walk()', () => {
    let cursor: TreeCursor;

    afterEach(() => {
      cursor.delete();
    });

    it('returns a cursor that can be used to walk the tree', () => {
      tree = parser.parse('a * b + c / d')!;
      cursor = tree.walk();

      assertCursorState(cursor, {
        nodeType: 'program',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 13 },
        startIndex: 0,
        endIndex: 13,
      });

      expect(cursor.gotoFirstChild()).toBe(true);
      assertCursorState(cursor, {
        nodeType: 'expression_statement',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 13 },
        startIndex: 0,
        endIndex: 13,
      });

      expect(cursor.gotoFirstChild()).toBe(true);
      assertCursorState(cursor, {
        nodeType: 'binary_expression',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 13 },
        startIndex: 0,
        endIndex: 13,
      });

      expect(cursor.gotoFirstChild()).toBe(true);
      assertCursorState(cursor, {
        nodeType: 'binary_expression',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 5 },
        startIndex: 0,
        endIndex: 5,
      });

      expect(cursor.gotoFirstChild()).toBe(true);
      expect(cursor.nodeText).toBe('a');
      assertCursorState(cursor, {
        nodeType: 'identifier',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 1 },
        startIndex: 0,
        endIndex: 1,
      });

      expect(cursor.gotoFirstChild()).toBe(false);
      expect(cursor.gotoNextSibling()).toBe(true);
      expect(cursor.nodeText).toBe('*');
      assertCursorState(cursor, {
        nodeType: '*',
        nodeIsNamed: false,
        startPosition: { row: 0, column: 2 },
        endPosition: { row: 0, column: 3 },
        startIndex: 2,
        endIndex: 3,
      });

      expect(cursor.gotoNextSibling()).toBe(true);
      expect(cursor.nodeText).toBe('b');
      assertCursorState(cursor, {
        nodeType: 'identifier',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 4 },
        endPosition: { row: 0, column: 5 },
        startIndex: 4,
        endIndex: 5,
      });

      expect(cursor.gotoNextSibling()).toBe(false);
      expect(cursor.gotoParent()).toBe(true);
      assertCursorState(cursor, {
        nodeType: 'binary_expression',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 5 },
        startIndex: 0,
        endIndex: 5,
      });

      expect(cursor.gotoNextSibling()).toBe(true);
      assertCursorState(cursor, {
        nodeType: '+',
        nodeIsNamed: false,
        startPosition: { row: 0, column: 6 },
        endPosition: { row: 0, column: 7 },
        startIndex: 6,
        endIndex: 7,
      });

      expect(cursor.gotoNextSibling()).toBe(true);
      assertCursorState(cursor, {
        nodeType: 'binary_expression',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 8 },
        endPosition: { row: 0, column: 13 },
        startIndex: 8,
        endIndex: 13,
      });

      const copy = tree.walk();
      copy.resetTo(cursor);

      expect(copy.gotoPreviousSibling()).toBe(true);
      assertCursorState(copy, {
        nodeType: '+',
        nodeIsNamed: false,
        startPosition: { row: 0, column: 6 },
        endPosition: { row: 0, column: 7 },
        startIndex: 6,
        endIndex: 7,
      });

      expect(copy.gotoPreviousSibling()).toBe(true);
      assertCursorState(copy, {
        nodeType: 'binary_expression',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 5 },
        startIndex: 0,
        endIndex: 5,
      });

      expect(copy.gotoLastChild()).toBe(true);
      assertCursorState(copy, {
        nodeType: 'identifier',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 4 },
        endPosition: { row: 0, column: 5 },
        startIndex: 4,
        endIndex: 5,
      });

      expect(copy.gotoParent()).toBe(true);
      expect(copy.gotoParent()).toBe(true);
      expect(copy.nodeType).toBe('binary_expression');
      expect(copy.gotoParent()).toBe(true);
      expect(copy.nodeType).toBe('expression_statement');
      expect(copy.gotoParent()).toBe(true);
      expect(copy.nodeType).toBe('program');
      expect(copy.gotoParent()).toBe(false);
      copy.delete();

      expect(cursor.gotoParent()).toBe(true);
      expect(cursor.nodeType).toBe('binary_expression');
      expect(cursor.gotoParent()).toBe(true);
      expect(cursor.nodeType).toBe('expression_statement');
      expect(cursor.gotoParent()).toBe(true);
      expect(cursor.nodeType).toBe('program');
    });

    it('keeps track of the field name associated with each node', () => {
      tree = parser.parse('a.b();')!;
      cursor = tree.walk();
      cursor.gotoFirstChild();
      cursor.gotoFirstChild();

      expect(cursor.currentNode.type).toBe('call_expression');
      expect(cursor.currentFieldName).toBeNull();

      cursor.gotoFirstChild();
      expect(cursor.currentNode.type).toBe('member_expression');
      expect(cursor.currentFieldName).toBe('function');

      cursor.gotoFirstChild();
      expect(cursor.currentNode.type).toBe('identifier');
      expect(cursor.currentFieldName).toBe('object');

      cursor.gotoNextSibling();
      cursor.gotoNextSibling();
      expect(cursor.currentNode.type).toBe('property_identifier');
      expect(cursor.currentFieldName).toBe('property');

      cursor.gotoParent();
      cursor.gotoNextSibling();
      expect(cursor.currentNode.type).toBe('arguments');
      expect(cursor.currentFieldName).toBe('arguments');
    });

    it('returns a cursor that can be reset anywhere in the tree', () => {
      tree = parser.parse('a * b + c / d')!;
      cursor = tree.walk();
      const root = tree.rootNode.firstChild;

      cursor.reset(root!.firstChild!.firstChild!);
      assertCursorState(cursor, {
        nodeType: 'binary_expression',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 5 },
        startIndex: 0,
        endIndex: 5,
      });

      cursor.gotoFirstChild();
      assertCursorState(cursor, {
        nodeType: 'identifier',
        nodeIsNamed: true,
        startPosition: { row: 0, column: 0 },
        endPosition: { row: 0, column: 1 },
        startIndex: 0,
        endIndex: 1,
      });

      expect(cursor.gotoParent()).toBe(true);
      expect(cursor.gotoParent()).toBe(false);
    });
  });

  describe('.copy', () => {
    let input: string;
    let edit: Edit;

    it('creates another tree that remains stable if the original tree is edited', () => {
      input = 'abc + cde';
      tree = parser.parse(input)!;
      expect(tree.rootNode.toString()).toBe(
        '(program (expression_statement (binary_expression left: (identifier) right: (identifier))))'
      );

      const tree2 = tree.copy();
      [input, edit] = spliceInput(input, 3, 0, '123');
      expect(input).toBe('abc123 + cde');
      tree.edit(edit);

      const leftNode = tree.rootNode.firstChild!.firstChild!.firstChild;
      const leftNode2 = tree2.rootNode.firstChild!.firstChild!.firstChild;
      const rightNode = tree.rootNode.firstChild!.firstChild!.lastChild;
      const rightNode2 = tree2.rootNode.firstChild!.firstChild!.lastChild;
      expect(leftNode!.endIndex).toBe(6);
      expect(leftNode2!.endIndex).toBe(3);
      expect(rightNode!.startIndex).toBe(9);
      expect(rightNode2!.startIndex).toBe(6);

      tree2.delete();
    });
  });
});

function spliceInput(input: string, startIndex: number, lengthRemoved: number, newText: string): [string, Edit] {
  const oldEndIndex = startIndex + lengthRemoved;
  const newEndIndex = startIndex + newText.length;
  const startPosition = getExtent(input.slice(0, startIndex));
  const oldEndPosition = getExtent(input.slice(0, oldEndIndex));
  input = input.slice(0, startIndex) + newText + input.slice(oldEndIndex);
  const newEndPosition = getExtent(input.slice(0, newEndIndex));
  return [
    input,
    new Edit({
      startIndex,
      startPosition,
      oldEndIndex,
      oldEndPosition,
      newEndIndex,
      newEndPosition,
    }),
  ];
}

// Gets the extent of the text in terms of zero-based row and column numbers.
function getExtent(text: string): Point {
  let row = 0;
  let index = -1;
  let lastIndex = 0;
  while ((index = text.indexOf('\n', index + 1)) !== -1) {
    row++;
    lastIndex = index + 1;
  }
  return { row, column: text.length - lastIndex };
}

function assertCursorState(cursor: TreeCursor, params: CursorState): void {
  expect(cursor.nodeType).toBe(params.nodeType);
  expect(cursor.nodeIsNamed).toBe(params.nodeIsNamed);
  expect(cursor.startPosition).toEqual(params.startPosition);
  expect(cursor.endPosition).toEqual(params.endPosition);
  expect(cursor.startIndex).toEqual(params.startIndex);
  expect(cursor.endIndex).toEqual(params.endIndex);

  const node = cursor.currentNode;
  expect(node.type).toBe(params.nodeType);
  expect(node.isNamed).toBe(params.nodeIsNamed);
  expect(node.startPosition).toEqual(params.startPosition);
  expect(node.endPosition).toEqual(params.endPosition);
  expect(node.startIndex).toEqual(params.startIndex);
  expect(node.endIndex).toEqual(params.endIndex);
}
