/**********************************************************************
 * Copyright (C) 2023 Red Hat, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 ***********************************************************************/
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
// based on https://github.com/microsoft/vscode/blob/76415ef0b1f60e0479bdfee173c1a4f97e785b52/src/vs/platform/contextkey/test/common/scanner.test.ts
/* eslint-disable no-useless-escape */

import * as assert from 'node:assert';

import { test } from 'vitest';

import { Scanner, type Token, TokenType } from './scanner';

function tokenTypeToStr(token: Token): string {
  switch (token.type) {
    case TokenType.LParen:
      return '(';
    case TokenType.RParen:
      return ')';
    case TokenType.Neg:
      return '!';
    case TokenType.Eq:
      return token.isTripleEq ? '===' : '==';
    case TokenType.NotEq:
      return token.isTripleEq ? '!==' : '!=';
    case TokenType.Lt:
      return '<';
    case TokenType.LtEq:
      return '<=';
    case TokenType.Gt:
      return '>';
    case TokenType.GtEq:
      return '>=';
    case TokenType.RegexOp:
      return '=~';
    case TokenType.RegexStr:
      return 'RegexStr';
    case TokenType.True:
      return 'true';
    case TokenType.False:
      return 'false';
    case TokenType.In:
      return 'in';
    case TokenType.Not:
      return 'not';
    case TokenType.And:
      return '&&';
    case TokenType.Or:
      return '||';
    case TokenType.Str:
      return 'Str';
    case TokenType.QuotedStr:
      return 'QuotedStr';
    case TokenType.Error:
      return 'ErrorToken';
    case TokenType.EOF:
      return 'EOF';
  }
}

function scan(input: string): { type: string; offset: number; lexeme?: string }[] {
  return new Scanner()
    .reset(input)
    .scan()
    .map((token: Token) => {
      return 'lexeme' in token
        ? {
            type: tokenTypeToStr(token),
            offset: token.offset,
            lexeme: token.lexeme,
          }
        : {
            type: tokenTypeToStr(token),
            offset: token.offset,
          };
    });
}

test('foo.bar<C-shift+2>', () => {
  const input = 'foo.bar<C-shift+2>';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo.bar<C-shift+2>', offset: 0 },
    { type: 'EOF', offset: 18 },
  ]);
});

test('!foo', () => {
  const input = '!foo';
  assert.deepStrictEqual(scan(input), [
    { type: '!', offset: 0 },
    { type: 'Str', lexeme: 'foo', offset: 1 },
    { type: 'EOF', offset: 4 },
  ]);
});

test('foo === bar', () => {
  const input = 'foo === bar';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'foo' },
    { type: '===', offset: 4 },
    { type: 'Str', offset: 8, lexeme: 'bar' },
    { type: 'EOF', offset: 11 },
  ]);
});

test('foo  !== bar', () => {
  const input = 'foo  !== bar';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'foo' },
    { type: '!==', offset: 5 },
    { type: 'Str', offset: 9, lexeme: 'bar' },
    { type: 'EOF', offset: 12 },
  ]);
});

test('!(foo && bar)', () => {
  const input = '!(foo && bar)';
  assert.deepStrictEqual(scan(input), [
    { type: '!', offset: 0 },
    { type: '(', offset: 1 },
    { type: 'Str', lexeme: 'foo', offset: 2 },
    { type: '&&', offset: 6 },
    { type: 'Str', lexeme: 'bar', offset: 9 },
    { type: ')', offset: 12 },
    { type: 'EOF', offset: 13 },
  ]);
});

test('=~ ', () => {
  const input = '=~ ';
  assert.deepStrictEqual(scan(input), [
    { type: '=~', offset: 0 },
    { type: 'EOF', offset: 3 },
  ]);
});

test('foo =~ /bar/', () => {
  const input = 'foo =~ /bar/';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo', offset: 0 },
    { type: '=~', offset: 4 },
    { type: 'RegexStr', lexeme: '/bar/', offset: 7 },
    { type: 'EOF', offset: 12 },
  ]);
});

test('foo =~ /zee/i', () => {
  const input = 'foo =~ /zee/i';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo', offset: 0 },
    { type: '=~', offset: 4 },
    { type: 'RegexStr', lexeme: '/zee/i', offset: 7 },
    { type: 'EOF', offset: 13 },
  ]);
});

test('foo =~ /zee/gm', () => {
  const input = 'foo =~ /zee/gm';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo', offset: 0 },
    { type: '=~', offset: 4 },
    { type: 'RegexStr', lexeme: '/zee/gm', offset: 7 },
    { type: 'EOF', offset: 14 },
  ]);
});

test('foo in barrr  ', () => {
  const input = 'foo in barrr  ';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo', offset: 0 },
    { type: 'in', offset: 4 },
    { type: 'Str', lexeme: 'barrr', offset: 7 },
    { type: 'EOF', offset: 14 },
  ]);
});

test('resource =~ //FileCabinet/(SuiteScripts|Templates/(E-mail%20Templates|Marketing%20Templates)|Web%20Site%20Hosting%20Files)(/.*)*$/', () => {
  const input =
    'resource =~ //FileCabinet/(SuiteScripts|Templates/(E-mail%20Templates|Marketing%20Templates)|Web%20Site%20Hosting%20Files)(/.*)*$/';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'resource' },
    { type: '=~', offset: 9 },
    { type: 'RegexStr', offset: 12, lexeme: '//' },
    { type: 'Str', offset: 14, lexeme: 'FileCabinet/' },
    { type: '(', offset: 26 },
    { type: 'Str', offset: 27, lexeme: 'SuiteScripts' },
    { type: 'ErrorToken', offset: 39, lexeme: '|' },
    { type: 'Str', offset: 40, lexeme: 'Templates/' },
    { type: '(', offset: 50 },
    { type: 'Str', offset: 51, lexeme: 'E-mail%20Templates' },
    { type: 'ErrorToken', offset: 69, lexeme: '|' },
    { type: 'Str', offset: 70, lexeme: 'Marketing%20Templates' },
    { type: ')', offset: 91 },
    { type: 'ErrorToken', offset: 92, lexeme: '|' },
    { type: 'Str', offset: 93, lexeme: 'Web%20Site%20Hosting%20Files' },
    { type: ')', offset: 121 },
    { type: '(', offset: 122 },
    { type: 'RegexStr', offset: 123, lexeme: '/.*)*$/' },
    { type: 'EOF', offset: 130 },
  ]);
});

test('editorLangId in testely.supportedLangIds && resourceFilename =~ /^.+(.test.(w+))$/gm', () => {
  const input = 'editorLangId in testely.supportedLangIds && resourceFilename =~ /^.+(.test.(w+))$/gm';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'editorLangId', offset: 0 },
    { type: 'in', offset: 13 },
    { type: 'Str', lexeme: 'testely.supportedLangIds', offset: 16 },
    { type: '&&', offset: 41 },
    { type: 'Str', lexeme: 'resourceFilename', offset: 44 },
    { type: '=~', offset: 61 },
    { type: 'RegexStr', lexeme: '/^.+(.test.(w+))$/gm', offset: 64 },
    { type: 'EOF', offset: 84 },
  ]);
});

test('!(foo && bar) && baz', () => {
  const input = '!(foo && bar) && baz';
  assert.deepStrictEqual(scan(input), [
    { type: '!', offset: 0 },
    { type: '(', offset: 1 },
    { type: 'Str', lexeme: 'foo', offset: 2 },
    { type: '&&', offset: 6 },
    { type: 'Str', lexeme: 'bar', offset: 9 },
    { type: ')', offset: 12 },
    { type: '&&', offset: 14 },
    { type: 'Str', lexeme: 'baz', offset: 17 },
    { type: 'EOF', offset: 20 },
  ]);
});

test('foo.bar:zed==completed - equality with no space', () => {
  const input = 'foo.bar:zed==completed';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo.bar:zed', offset: 0 },
    { type: '==', offset: 11 },
    { type: 'Str', lexeme: 'completed', offset: 13 },
    { type: 'EOF', offset: 22 },
  ]);
});

test('a && b || c', () => {
  const input = 'a && b || c';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'a', offset: 0 },
    { type: '&&', offset: 2 },
    { type: 'Str', lexeme: 'b', offset: 5 },
    { type: '||', offset: 7 },
    { type: 'Str', lexeme: 'c', offset: 10 },
    { type: 'EOF', offset: 11 },
  ]);
});

test('fooBar && baz.jar && fee.bee<K-loo+1>', () => {
  const input = 'fooBar && baz.jar && fee.bee<K-loo+1>';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'fooBar', offset: 0 },
    { type: '&&', offset: 7 },
    { type: 'Str', lexeme: 'baz.jar', offset: 10 },
    { type: '&&', offset: 18 },
    { type: 'Str', lexeme: 'fee.bee<K-loo+1>', offset: 21 },
    { type: 'EOF', offset: 37 },
  ]);
});

test('foo.barBaz<C-r> < 2', () => {
  const input = 'foo.barBaz<C-r> < 2';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo.barBaz<C-r>', offset: 0 },
    { type: '<', offset: 16 },
    { type: 'Str', lexeme: '2', offset: 18 },
    { type: 'EOF', offset: 19 },
  ]);
});

test('foo.bar >= -1', () => {
  const input = 'foo.bar >= -1';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo.bar', offset: 0 },
    { type: '>=', offset: 8 },
    { type: 'Str', lexeme: '-1', offset: 11 },
    { type: 'EOF', offset: 13 },
  ]);
});

test('foo.bar <= -1', () => {
  const input = 'foo.bar <= -1';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo.bar', offset: 0 },
    { type: '<=', offset: 8 },
    { type: 'Str', lexeme: '-1', offset: 11 },
    { type: 'EOF', offset: 13 },
  ]);
});

test('resource =~ /\\/Objects\\/.+\\.xml$/', () => {
  const input = 'resource =~ /\\/Objects\\/.+\\.xml$/';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'resource', offset: 0 },
    { type: '=~', offset: 9 },
    { type: 'RegexStr', lexeme: '/\\/Objects\\/.+\\.xml$/', offset: 12 },
    { type: 'EOF', offset: 33 },
  ]);
});

test('view == vsc-packages-activitybar-folders && vsc-packages-folders-loaded', () => {
  const input = 'view == vsc-packages-activitybar-folders && vsc-packages-folders-loaded';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'view', offset: 0 },
    { type: '==', offset: 5 },
    { type: 'Str', lexeme: 'vsc-packages-activitybar-folders', offset: 8 },
    { type: '&&', offset: 41 },
    { type: 'Str', lexeme: 'vsc-packages-folders-loaded', offset: 44 },
    { type: 'EOF', offset: 71 },
  ]);
});

test('sfdx:project_opened && resource =~ /.*\\/functions\\/.*\\/[^\\/]+(\\/[^\\/]+.(ts|js|java|json|toml))?$/ && resourceFilename != package.json && resourceFilename != package-lock.json && resourceFilename != tsconfig.json', () => {
  const input =
    'sfdx:project_opened && resource =~ /.*\\/functions\\/.*\\/[^\\/]+(\\/[^\\/]+.(ts|js|java|json|toml))?$/ && resourceFilename != package.json && resourceFilename != package-lock.json && resourceFilename != tsconfig.json';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'sfdx:project_opened', offset: 0 },
    { type: '&&', offset: 20 },
    { type: 'Str', lexeme: 'resource', offset: 23 },
    { type: '=~', offset: 32 },
    { type: 'RegexStr', lexeme: '/.*\\/functions\\/.*\\/[^\\/]+(\\/[^\\/]+.(ts|js|java|json|toml))?$/', offset: 35 },
    { type: '&&', offset: 98 },
    { type: 'Str', lexeme: 'resourceFilename', offset: 101 },
    { type: '!=', offset: 118 },
    { type: 'Str', lexeme: 'package.json', offset: 121 },
    { type: '&&', offset: 134 },
    { type: 'Str', lexeme: 'resourceFilename', offset: 137 },
    { type: '!=', offset: 154 },
    { type: 'Str', lexeme: 'package-lock.json', offset: 157 },
    { type: '&&', offset: 175 },
    { type: 'Str', lexeme: 'resourceFilename', offset: 178 },
    { type: '!=', offset: 195 },
    { type: 'Str', lexeme: 'tsconfig.json', offset: 198 },
    { type: 'EOF', offset: 211 },
  ]);
});

test(`view =~ '/(servers)/' && viewItem =~ '/^(Starting|Started|Debugging|Stopping|Stopped)/'`, () => {
  const input = `view =~ '/(servers)/' && viewItem =~ '/^(Starting|Started|Debugging|Stopping|Stopped)/'`;
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'view', offset: 0 },
    { type: '=~', offset: 5 },
    { type: 'QuotedStr', lexeme: '/(servers)/', offset: 9 },
    { type: '&&', offset: 22 },
    { type: 'Str', lexeme: 'viewItem', offset: 25 },
    { type: '=~', offset: 34 },
    { type: 'QuotedStr', lexeme: '/^(Starting|Started|Debugging|Stopping|Stopped)/', offset: 38 },
    { type: 'EOF', offset: 87 },
  ]);
});

test('resourcePath =~ /.md(.yml|.txt)*$/gim', () => {
  const input = 'resourcePath =~ /.md(.yml|.txt)*$/gim';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'resourcePath' },
    { type: '=~', offset: 13 },
    { type: 'RegexStr', offset: 16, lexeme: '/.md(.yml|.txt)*$/gim' },
    { type: 'EOF', offset: 37 },
  ]);
});

test(`foo === bar'`, () => {
  const input = `foo === bar'`;
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'foo' },
    { type: '===', offset: 4 },
    { type: 'Str', offset: 8, lexeme: 'bar' },
    { type: 'ErrorToken', offset: 11, lexeme: `'` },
    { type: 'EOF', offset: 12 },
  ]);
});

test(`foo === '`, () => {
  const input = `foo === '`;
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'foo' },
    { type: '===', offset: 4 },
    { type: 'ErrorToken', offset: 8, lexeme: `'` },
    { type: 'EOF', offset: 9 },
  ]);
});

test(`foo && 'bar - unterminated single quote`, () => {
  const input = `foo && 'bar`;
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'foo', offset: 0 },
    { type: '&&', offset: 4 },
    { type: 'ErrorToken', offset: 7, lexeme: `'bar` },
    { type: 'EOF', offset: 11 },
  ]);
});

test('vim<c-r> == 1 && vim<2 <= 3', () => {
  const input = 'vim<c-r> == 1 && vim<2 <= 3';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', lexeme: 'vim<c-r>', offset: 0 },
    { type: '==', offset: 9 },
    { type: 'Str', lexeme: '1', offset: 12 },
    { type: '&&', offset: 14 },
    { type: 'Str', lexeme: 'vim<2', offset: 17 },
    { type: '<=', offset: 23 },
    { type: 'Str', lexeme: '3', offset: 26 },
    { type: 'EOF', offset: 27 },
  ]);
});

test('vim<c-r>==1 && vim<2<=3', () => {
  const input = 'vim<c-r>==1 && vim<2<=3';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'vim<c-r>' },
    { type: '==', offset: 8 },
    { type: 'Str', offset: 10, lexeme: '1' },
    { type: '&&', offset: 12 },
    { type: 'Str', offset: 15, lexeme: 'vim<2<' },
    { type: 'ErrorToken', offset: 21, lexeme: '=' },
    { type: 'Str', offset: 22, lexeme: '3' },
    { type: 'EOF', offset: 23 },
  ]);
});

test('foo|bar', () => {
  const input = 'foo|bar';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'foo' },
    { type: 'ErrorToken', offset: 3, lexeme: '|' },
    { type: 'Str', offset: 4, lexeme: 'bar' },
    { type: 'EOF', offset: 7 },
  ]);
});

test('resource =~ //foo/(barr|door/(Foo-Bar%20Templates|Soo%20Looo)|Web%20Site%Jjj%20Llll)(/.*)*$/', () => {
  const input = 'resource =~ //foo/(barr|door/(Foo-Bar%20Templates|Soo%20Looo)|Web%20Site%Jjj%20Llll)(/.*)*$/';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'resource' },
    { type: '=~', offset: 9 },
    { type: 'RegexStr', offset: 12, lexeme: '//' },
    { type: 'Str', offset: 14, lexeme: 'foo/' },
    { type: '(', offset: 18 },
    { type: 'Str', offset: 19, lexeme: 'barr' },
    { type: 'ErrorToken', offset: 23, lexeme: '|' },
    { type: 'Str', offset: 24, lexeme: 'door/' },
    { type: '(', offset: 29 },
    { type: 'Str', offset: 30, lexeme: 'Foo-Bar%20Templates' },
    { type: 'ErrorToken', offset: 49, lexeme: '|' },
    { type: 'Str', offset: 50, lexeme: 'Soo%20Looo' },
    { type: ')', offset: 60 },
    { type: 'ErrorToken', offset: 61, lexeme: '|' },
    { type: 'Str', offset: 62, lexeme: 'Web%20Site%Jjj%20Llll' },
    { type: ')', offset: 83 },
    { type: '(', offset: 84 },
    { type: 'RegexStr', offset: 85, lexeme: '/.*)*$/' },
    { type: 'EOF', offset: 92 },
  ]);
});

test('/((/foo/(?!bar)(.*)/)|((/src/).*/)).*$/', () => {
  const input = '/((/foo/(?!bar)(.*)/)|((/src/).*/)).*$/';
  assert.deepStrictEqual(scan(input), [
    { type: 'RegexStr', offset: 0, lexeme: '/((/' },
    { type: 'Str', offset: 4, lexeme: 'foo/' },
    { type: '(', offset: 8 },
    { type: 'Str', offset: 9, lexeme: '?' },
    { type: '!', offset: 10 },
    { type: 'Str', offset: 11, lexeme: 'bar' },
    { type: ')', offset: 14 },
    { type: '(', offset: 15 },
    { type: 'Str', offset: 16, lexeme: '.*' },
    { type: ')', offset: 18 },
    { type: 'RegexStr', offset: 19, lexeme: '/)|((/s' },
    { type: 'Str', offset: 26, lexeme: 'rc/' },
    { type: ')', offset: 29 },
    { type: 'Str', offset: 30, lexeme: '.*/' },
    { type: ')', offset: 33 },
    { type: ')', offset: 34 },
    { type: 'Str', offset: 35, lexeme: '.*$/' },
    { type: 'EOF', offset: 39 },
  ]);
});

test('resourcePath =~ //foo/barr// || resourcePath =~ //view/(jarrr|doooor|bees)/(web|templates)// && resourceExtname in foo.Bar', () => {
  const input =
    'resourcePath =~ //foo/barr// || resourcePath =~ //view/(jarrr|doooor|bees)/(web|templates)// && resourceExtname in foo.Bar';
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'resourcePath' },
    { type: '=~', offset: 13 },
    { type: 'RegexStr', offset: 16, lexeme: '//' },
    { type: 'Str', offset: 18, lexeme: 'foo/barr//' },
    { type: '||', offset: 29 },
    { type: 'Str', offset: 32, lexeme: 'resourcePath' },
    { type: '=~', offset: 45 },
    { type: 'RegexStr', offset: 48, lexeme: '//' },
    { type: 'Str', offset: 50, lexeme: 'view/' },
    { type: '(', offset: 55 },
    { type: 'Str', offset: 56, lexeme: 'jarrr' },
    { type: 'ErrorToken', offset: 61, lexeme: '|' },
    { type: 'Str', offset: 62, lexeme: 'doooor' },
    { type: 'ErrorToken', offset: 68, lexeme: '|' },
    { type: 'Str', offset: 69, lexeme: 'bees' },
    { type: ')', offset: 73 },
    { type: 'RegexStr', offset: 74, lexeme: '/(web|templates)/' },
    { type: 'ErrorToken', offset: 91, lexeme: '/ && resourceExtname in foo.Bar' },
    { type: 'EOF', offset: 122 },
  ]);
});

test('foo =~ /file:// || bar', () => {
  const input = JSON.parse('"foo =~ /file:// || bar"');
  assert.deepStrictEqual(scan(input), [
    { type: 'Str', offset: 0, lexeme: 'foo' },
    { type: '=~', offset: 4 },
    { type: 'RegexStr', offset: 7, lexeme: '/file:/' },
    { type: 'ErrorToken', offset: 14, lexeme: '/ || bar' },
    { type: 'EOF', offset: 22 },
  ]);
});
