// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT

import { describe, expect, it } from "vitest";

import { Lexer } from "../../src/expression-evaluation/lexer";

import type { Token } from "../../src/expression-evaluation/grammar";

describe("Lexer", () => {
  const singleOperatorTestCases: { input: string; expected: Token[] }[] = [
    {
      input: "+",
      expected: [
        { type: "+", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "-",
      expected: [
        { type: "-", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "*",
      expected: [
        { type: "*", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "**",
      expected: [
        { type: "**", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "/",
      expected: [
        { type: "/", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "%",
      expected: [
        { type: "%", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "===",
      expected: [
        { type: "===", pos: 0 },
        { type: "eof", pos: 3 },
      ],
    },
    {
      input: "!==",
      expected: [
        { type: "!==", pos: 0 },
        { type: "eof", pos: 3 },
      ],
    },
    {
      input: "==",
      expected: [
        { type: "==", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "<",
      expected: [
        { type: "<", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "<=",
      expected: [
        { type: "<=", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: ">",
      expected: [
        { type: ">", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: ">=",
      expected: [
        { type: ">=", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "&&",
      expected: [
        { type: "&&", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "||",
      expected: [
        { type: "||", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "&",
      expected: [
        { type: "&", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "|",
      expected: [
        { type: "|", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "<<",
      expected: [
        { type: "<<", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: ">>>",
      expected: [
        { type: ">>>", pos: 0 },
        { type: "eof", pos: 3 },
      ],
    },
    {
      input: ".",
      expected: [
        { type: ".", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "?.",
      expected: [
        { type: "?.", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "??",
      expected: [
        { type: "??", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "?",
      expected: [
        { type: "?", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: ":",
      expected: [
        { type: ":", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "(",
      expected: [
        { type: "(", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: ")",
      expected: [
        { type: ")", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "[",
      expected: [
        { type: "[", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "]",
      expected: [
        { type: "]", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: ",",
      expected: [
        { type: ",", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "{",
      expected: [
        { type: "{", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "}",
      expected: [
        { type: "}", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
  ];

  const singleNumberTestCases: { input: string; expected: Token[] }[] = [
    {
      input: "1",
      expected: [
        { type: "number", symbol: "1", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "42",
      expected: [
        { type: "number", symbol: "42", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "42.9",
      expected: [
        { type: "number", symbol: "42.9", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: ".9",
      expected: [
        { type: "number", symbol: ".9", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "1_1",
      expected: [
        { type: "number", symbol: "1_1", pos: 0 },
        { type: "eof", pos: 3 },
      ],
    },
    {
      input: "10_1",
      expected: [
        { type: "number", symbol: "10_1", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "1_1_1",
      expected: [
        { type: "number", symbol: "1_1_1", pos: 0 },
        { type: "eof", pos: 5 },
      ],
    },
    {
      input: ".1_1",
      expected: [
        { type: "number", symbol: ".1_1", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
  ];

  const invalidNumberTestCases: { input: string; expected: Token[] }[] = [
    { input: "1.1.1", expected: [{ type: "invalid", pos: 0 }] },
    { input: "1__1", expected: [{ type: "invalid", pos: 0 }] },
    { input: "1_", expected: [{ type: "invalid", pos: 0 }] },
    { input: "1._1", expected: [{ type: "invalid", pos: 0 }] },
    { input: "0_1", expected: [{ type: "invalid", pos: 0 }] },
    { input: "00_1", expected: [{ type: "invalid", pos: 0 }] },
  ];

  const singleIdentifierTestCases: { input: string; expected: Token[] }[] = [
    {
      input: "foo",
      expected: [
        { type: "iden", symbol: "foo", pos: 0 },
        { type: "eof", pos: 3 },
      ],
    },
    {
      input: "_foo",
      expected: [
        { type: "iden", symbol: "_foo", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "$foo",
      expected: [
        { type: "iden", symbol: "$foo", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "foo1",
      expected: [
        { type: "iden", symbol: "foo1", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "_foo1_",
      expected: [
        { type: "iden", symbol: "_foo1_", pos: 0 },
        { type: "eof", pos: 6 },
      ],
    },
    {
      input: "μ",
      expected: [
        { type: "iden", symbol: "μ", pos: 0 },
        { type: "eof", pos: 1 },
      ],
    },
    {
      input: "._1",
      expected: [
        { type: ".", pos: 0 },
        { type: "iden", symbol: "_1", pos: 1 },
        { type: "eof", pos: 3 },
      ],
    },
    {
      input: "true",
      expected: [
        { type: "iden", symbol: "true", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "false",
      expected: [
        { type: "iden", symbol: "false", pos: 0 },
        { type: "eof", pos: 5 },
      ],
    },
    {
      input: "null",
      expected: [
        { type: "iden", symbol: "null", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "undefined",
      expected: [
        { type: "iden", symbol: "undefined", pos: 0 },
        { type: "eof", pos: 9 },
      ],
    },
  ];

  const invalidIdentifierTestCases: { input: string; expected: Token[] }[] = [
    {
      input: "1foo",
      expected: [
        { type: "number", symbol: "1", pos: 0 },
        { type: "iden", symbol: "foo", pos: 1 },
        { type: "eof", pos: 4 },
      ],
    },
    { input: "↓", expected: [{ type: "invalid", pos: 0 }] },
    { input: '"', expected: [{ type: "invalid", pos: 0 }] },
  ];

  const singleStringTestCases: { input: string; expected: Token[] }[] = [
    {
      input: '""',
      expected: [
        { type: "string", symbol: '""', pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: '"foo"',
      expected: [
        { type: "string", symbol: '"foo"', pos: 0 },
        { type: "eof", pos: 5 },
      ],
    },
    {
      input: '"\\""',
      expected: [
        { type: "string", symbol: '"\\""', pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: '"\\\'"',
      expected: [
        { type: "string", symbol: '"\\\'"', pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "''",
      expected: [
        { type: "string", symbol: "''", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "'foo'",
      expected: [
        { type: "string", symbol: "'foo'", pos: 0 },
        { type: "eof", pos: 5 },
      ],
    },
    {
      input: "'\\''",
      expected: [
        { type: "string", symbol: "'\\''", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "'\\\"'",
      expected: [
        { type: "string", symbol: "'\\\"'", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: "``",
      expected: [
        { type: "string", symbol: "``", pos: 0 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "`foo`",
      expected: [
        { type: "string", symbol: "`foo`", pos: 0 },
        { type: "eof", pos: 5 },
      ],
    },
    {
      input: "`\\``",
      expected: [
        { type: "string", symbol: "`\\``", pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
    {
      input: '`\\"`',
      expected: [
        { type: "string", symbol: '`\\"`', pos: 0 },
        { type: "eof", pos: 4 },
      ],
    },
  ];

  const invalidStringTestCases: { input: string; expected: Token[] }[] = [
    { input: '"', expected: [{ type: "invalid", pos: 0 }] },
    { input: '"\\"', expected: [{ type: "invalid", pos: 0 }] },
    { input: "'", expected: [{ type: "invalid", pos: 0 }] },
    { input: "'\\'", expected: [{ type: "invalid", pos: 0 }] },
  ];

  const whiteSpaceTestCases: { input: string; expected: Token[] }[] = [
    { input: " ", expected: [{ type: "eof", pos: 1 }] },
    { input: "   ", expected: [{ type: "eof", pos: 3 }] },
    { input: "\n", expected: [{ type: "eof", pos: 1 }] },
    { input: " \n", expected: [{ type: "eof", pos: 2 }] },
    { input: "	", expected: [{ type: "eof", pos: 1 }] },
  ];

  const complicatedTermTestCases: { input: string; expected: Token[] }[] = [
    {
      input: "5x",
      expected: [
        { type: "number", symbol: "5", pos: 0 },
        { type: "iden", symbol: "x", pos: 1 },
        { type: "eof", pos: 2 },
      ],
    },
    {
      input: "5*x",
      expected: [
        { type: "number", symbol: "5", pos: 0 },
        { type: "*", pos: 1 },
        { type: "iden", symbol: "x", pos: 2 },
        { type: "eof", pos: 3 },
      ],
    },
    {
      input: "5 * x",
      expected: [
        { type: "number", symbol: "5", pos: 0 },
        { type: "*", pos: 2 },
        { type: "iden", symbol: "x", pos: 4 },
        { type: "eof", pos: 5 },
      ],
    },
    {
      input: "(5 * 5 + 2) / 1.2 === 'foo'",
      expected: [
        { type: "(", pos: 0 },
        { type: "number", symbol: "5", pos: 1 },
        { type: "*", pos: 3 },
        { type: "number", symbol: "5", pos: 5 },
        { type: "+", pos: 7 },
        { type: "number", symbol: "2", pos: 9 },
        { type: ")", pos: 10 },
        { type: "/", pos: 12 },
        { type: "number", symbol: "1.2", pos: 14 },
        { type: "===", pos: 18 },
        { type: "string", symbol: "'foo'", pos: 22 },
        { type: "eof", pos: 27 },
      ],
    },
    {
      input: "(() => {console.log('foo'); return 1;})()",
      expected: [
        { type: "(", pos: 0 },
        { type: "(", pos: 1 },
        { type: ")", pos: 2 },
        { type: "invalid", pos: 4 },
      ],
    },
    {
      input: "(function() {console.log('foo'); return 1;})()",
      expected: [
        { type: "(", pos: 0 },
        { type: "iden", symbol: "function", pos: 1 },
        { type: "(", pos: 9 },
        { type: ")", pos: 10 },
        { type: "{", pos: 12 },
        { type: "iden", symbol: "console", pos: 13 },
        { type: ".", pos: 20 },
        { type: "iden", symbol: "log", pos: 21 },
        { type: "(", pos: 24 },
        { type: "string", symbol: "'foo'", pos: 25 },
        { type: ")", pos: 30 },
        { type: "invalid", pos: 31 },
      ],
    },
    {
      input: "'ranged' === 'ranged'",
      expected: [
        { type: "string", symbol: "'ranged'", pos: 0 },
        { type: "===", pos: 9 },
        { type: "string", symbol: "'ranged'", pos: 13 },
        { type: "eof", pos: 21 },
      ],
    },
  ];

  it.each([
    ...singleOperatorTestCases,
    ...singleNumberTestCases,
    ...invalidNumberTestCases,
    ...singleIdentifierTestCases,
    ...invalidIdentifierTestCases,
    ...singleStringTestCases,
    ...invalidStringTestCases,
    ...whiteSpaceTestCases,
    ...complicatedTermTestCases,
  ])("lexes $input correctly", ({ input, expected }) => {
    // when
    const result = consume(new Lexer(input));

    // then
    expect(result).toEqual(expected);
  });
});

function consume<T>(iterable: Iterable<T>): T[] {
  const result: T[] = [];
  for (const value of iterable) {
    result.push(value);
  }
  return result;
}
