import { beforeEach, describe, test, expect } from '@jest/globals';
import {
  toMathML,
  tex2mml,
  page2mml,
  setupTex,
  setupTexPage,
  setupComponents,
  trapOutput,
  trapErrors,
  expectTexError,
} from '#helpers';
import '#js/input/tex/ams/AmsConfiguration';
import '#js/input/tex/newcommand/NewcommandConfiguration';

import { TeX } from '#js/input/tex.js';
import { MmlFactory } from '#js/core/MmlTree/MmlFactory.js';

import {
  Configuration,
  ConfigurationHandler,
} from '#js/input/tex/Configuration.js';
import { HandlerType, ConfigurationType } from '#js/input/tex/HandlerTypes.js';
import { CommandMap } from '#js/input/tex/TokenMap.js';
import { Token } from '#js/input/tex/Token.js';
import { TagsFactory } from '#js/input/tex/Tags.js';
import TexError from '#js/input/tex/TexError.js';
import {
  ParseUtil,
  KeyValueTypes,
  KeyValueDef,
} from '#js/input/tex/ParseUtil.js';

/**********************************************************************************/

const tex = new TeX();
tex.setMmlFactory(new MmlFactory());

describe('TeX', () => {
  test('Reset', () => expect(tex.reset()).toBe(undefined));
});

/**********************************************************************************/

describe('NodeFactory', () => {
  test('createText null text', () => {
    expect(tex.parseOptions.nodeFactory.create('text', null)).toBe(null);
  });

  test('create null kind', () => {
    expect(
      toMathML(tex.parseOptions.nodeFactory.create('undefined', 'mi'))
    ).toMatchSnapshot();
  });
});

/**********************************************************************************/

describe('NodeUtil', () => {
  test('inferred propery', () => {
    const mrow = tex.parseOptions.nodeFactory.create('node', 'mrow', [], {
      inferred: true,
    });
    expect(mrow.getProperty('inferred')).toBe(undefined);
  });
});

/**********************************************************************************/

describe('Stack', () => {
  new CommandMap(
    'stackMacros',
    {
      showStack: (parser) => {
        parser.stack.toString();
      },
      getTop: 'getTop', // coverage for macro defined by name of method in methods object
    },
    {
      getTop: (parser) => {
        const top = parser.stack.Top(5);
        const mtext = parser.create(
          'token',
          'mtext',
          {},
          top === null ? 'yes' : 'no'
        );
        parser.Push(mtext);
      },
    }
  );

  Configuration.create('stackMacros', {
    [ConfigurationType.HANDLER]: {
      [HandlerType.MACRO]: ['stackMacros'],
    },
  });

  beforeEach(() => setupTex(['base', 'stackMacros']));

  test('toString', () => {
    expect(tex2mml('\\showStack')).toMatchSnapshot();
  });

  test('getTop', () => {
    setupTex(['base', 'stackMacros']);
    expect(tex2mml('\\getTop')).toMatchSnapshot();
  });
});

/**********************************************************************************/

describe('Token', () => {
  test('token', () => {
    const token = new Token('x', 'y', {});
    expect(token.token).toBe('x');
  });
});

/**********************************************************************************/

describe('Tags', () => {
  test('noTag', () => {
    expect((tex.parseOptions.tags as any).noTag).toBe(false);
  });

  test('Unknown default', () => {
    TagsFactory.setDefault('unknown');
    const message = trapErrors(() => {
      TagsFactory.create('error');
    });
    TagsFactory.setDefault('none');
    expect(message).toBe('Unknown tags class');
  });
});

/**********************************************************************************/

describe('TexError', () => {
  test('Number argument', () => {
    const err = new TexError('test', 'Number: %1', 1 as any);
    expect(err.message).toBe('Number: 1');
  });

  test('Braced insertion', () => {
    const err = new TexError('test', 'Msg: %{1}, Number: %{2}', 'OK', 2 as any);
    expect(err.message).toBe('Msg: OK, Number: 2');
  });

  test('Plural', () => {
    const err = new TexError('test', '%{plural:%1|abc}', 'apple');
    expect(err.message).toBe('%{plural:%1|abc}');
  });

  test('Percent', () => {
    const err = new TexError('test', '10%%');
    expect(err.message).toBe('10%');
  });
});

/**********************************************************************************/

setupComponents({
  loader: { load: ['input/tex-base'] },
});

describe('FindTeX', () => {
  setupTexPage(['base']);

  test('display math', async () => {
    expect(await page2mml('abc $$ x + 1 $$ def')).toMatchSnapshot();
  });

  test('environment', async () => {
    expect(
      await page2mml('abc \\begin{equation} x=y \\end{equation} def')
    ).toMatchSnapshot();
  });

  test('Nested braces', async () => {
    expect(await page2mml('abc $$a + {\\bf b} + c$$ def')).toMatchSnapshot();
  });

  test('processEscapes', async () => {
    expect(await page2mml('abc \\$ def')).toMatchSnapshot();
  });

  test('ref undefined', async () => {
    expect(await page2mml('abc \\ref{x} def')).toMatchSnapshot();
  });

  test('No close delim', () => {
    expect(page2mml('abc $$ x + 1')).resolves.toEqual([]);
  });

  test('No close delim', () => {
    expect(page2mml('abc \\begin{align} x + 1')).resolves.toEqual([]);
  });
});

/**********************************************************************************/

describe('Configuration', () => {
  test('keys', () => {
    expect(Array.from(ConfigurationHandler.keys()).length > 0).toBe(true);
  });

  test('init', () => {
    const init = () => {};
    const config = Configuration.create('init', {
      [ConfigurationType.INIT]: init,
    });
    expect(config.init).toBe(init);
  });

  test('init null', () => {
    expect(ConfigurationHandler.get('base').init).toBe(null);
  });

  test('Package priority', () => {
    const order: number[] = [];
    Configuration.create('priority 1', {
      [ConfigurationType.INIT]: () => {
        order.push(1);
      },
    });
    Configuration.create('priority 2', {
      [ConfigurationType.INIT]: () => {
        order.push(2);
      },
    });
    setupTex([
      ['priority 1', 1],
      ['priority 2', 2],
    ]);
    expect(order).toEqual([1, 2]);
    setupTex([
      ['priority 1', 2],
      ['priority 2', 1],
    ]);
    expect(order).toEqual([1, 2, 2, 1]);
  });

  test('Parser error', () => {
    Configuration.create('error', {
      [ConfigurationType.PARSER]: 'error',
    });
    const message = trapErrors(() => {
      new TeX({ packages: ['base', 'error'] });
    });
    expect(message).toBe("Package 'error' doesn't target the proper parser");
  });

  test('Package error', () => {
    const message = trapOutput(
      'warn',
      () => new TeX({ packages: ['base', 'undefined'] })
    );
    expect(message).toBe(
      "MathJax Warning: Package 'undefined' not found.  Omitted."
    );
  });
});

/**********************************************************************************/

describe('MapHandler', () => {
  test('Undefined', () => {
    Configuration.create('BadHandler', {
      [ConfigurationType.HANDLER]: {
        [HandlerType.MACRO]: ['undefindHandler'],
      },
    });
    const message = trapOutput(
      'log',
      () => new TeX({ packages: ['base', 'BadHandler'] })
    );
    expect(message).toBe(
      "TexParser Warning: Configuration 'undefindHandler' not found! Omitted."
    );
  });

  test('remove', () => {
    const fallback = () => {};
    Configuration.create('fallback', {
      [ConfigurationType.FALLBACK]: {
        [HandlerType.MACRO]: fallback,
      },
    });
    const tex = new TeX({ packages: ['fallback'] });
    const map = (tex as any).configuration.handlers.get(HandlerType.MACRO);
    expect(Array.from((map as any)._fallback).length).toBe(1);
    map.remove([], fallback);
    expect(Array.from((map as any)._fallback).length).toBe(0);
  });

  test('toString', () => {
    const map = (tex as any).configuration.handlers.get(HandlerType.MACRO);
    expect(map.toString()).toBe(
      'mathchar7, mathchar0mo, mathchar0mi, ucGreek, lcGreek, macros, delimiter'
    );
  });

  test('retrieve', () => {
    const handlers = (tex as any).configuration.handlers;
    expect(handlers.retrieve('undefined')).toBe(null);
  });

  test('keys', () => {
    const handlers = (tex as any).configuration.handlers;
    expect(Array.from(handlers.keys()).sort()).toEqual(
      Object.values(HandlerType).sort()
    );
  });
});

/**********************************************************************************/

describe('TexParser', () => {
  test('toString', () => {
    let output = '';
    new CommandMap('showParser', {
      showparser(parser: any) {
        output = parser.toString();
      },
    });
    Configuration.create('showParser', {
      [ConfigurationType.HANDLER]: {
        [HandlerType.MACRO]: ['showParser'],
      },
    });
    setupTex(['base', 'showParser']);
    tex2mml('\\showparser');
    expect(output).toBe(
      [
        'character: digit, letter, special, command',
        'delimiter: delimiter',
        'macro: showParser, mathchar7, mathchar0mo, mathchar0mi, ucGreek, lcGreek, macros, delimiter',
        'environment: environment',
        '',
      ].join('\n')
    );
  });

  test('GetBrackets nested [', () => {
    setupTex(['base', 'ams']);
    expect(tex2mml('\\xrightarrow[{[x]}]{a}')).toMatchSnapshot();
  });

  test('GetBrackets matchBrakets', () => {
    new CommandMap('matchBrackets', {
      matchbrackets(parser: any, name: string) {
        const arg = parser.GetBrackets(name, '', true);
        parser.Push(parser.create('token', 'mtext', {}, arg));
      },
    });
    Configuration.create('matchBrackets', {
      [ConfigurationType.HANDLER]: {
        [HandlerType.MACRO]: ['matchBrackets'],
      },
    });
    setupTex(['base', 'matchBrackets']);
    expect(tex2mml('\\matchbrackets[[x]]')).toMatchSnapshot();
  });

  test('mml', () => {
    new CommandMap('mml', {
      mml(parser: any) {
        parser.mml();
      },
    });
    Configuration.create('mml', {
      [ConfigurationType.HANDLER]: {
        [HandlerType.MACRO]: ['mml'],
      },
    });
    setupTex(['base', 'mml']);
    expect(tex2mml('{\\mml}')).toMatchSnapshot();
  });

  test('Removed delimiter', () => {
    setupTex(['base', 'ams', 'newcommand']);
    expect(
      tex2mml('\\let\\vert=x \\begin{vmatrix} a \\end{vmatrix}')
    ).toMatchSnapshot();
  });
});

/**********************************************************************************/

describe('ParseUtil', () => {
  test('keyValue', () => {
    expect(ParseUtil.keyvalOptions('ab')).toEqual({ ab: true });
    expect(ParseUtil.keyvalOptions('ab=true')).toEqual({ ab: true });
    expect(ParseUtil.keyvalOptions('ab=false')).toEqual({ ab: false });
    expect(ParseUtil.keyvalOptions('ab=xyz')).toEqual({ ab: 'xyz' });
    expect(ParseUtil.keyvalOptions('ab=')).toEqual({ ab: '' });
    expect(ParseUtil.keyvalOptions('{ab=}')).toEqual({ ab: '' });
    expect(ParseUtil.keyvalOptions('{{ab=}}')).toEqual({ 'ab=': true });
    expect(ParseUtil.keyvalOptions('ab=1,cd')).toEqual({ ab: '1', cd: true });
    expect(ParseUtil.keyvalOptions('ab cd=x')).toEqual({ 'ab cd': 'x' });
    expect(ParseUtil.keyvalOptions('{ab,c=d}=x')).toEqual({ 'ab,c=d': 'x' });
    expect(ParseUtil.keyvalOptions('ab=\\x')).toEqual({ ab: '\\x' });
    expect(ParseUtil.keyvalOptions('ab=\\{')).toEqual({ ab: '\\{' });
    expect(ParseUtil.keyvalOptions('ab={a\\{b}')).toEqual({ ab: 'a\\{b' });
    expect(ParseUtil.keyvalOptions('ab=\\')).toEqual({ ab: '\\' });
    expect(ParseUtil.keyvalOptions('a\\b=c')).toEqual({ 'a\\b': 'c' });
  });

  test('keyValue braces', () => {
    expect(ParseUtil.keyvalOptions('ab={{{cd} ef}}')).toEqual({
      ab: '{cd} ef',
    });
    expect(
      ParseUtil.keyvalOptions('ab={{{cd} ef}}', null, false, true)
    ).toEqual({ ab: '{{cd} ef}' });
    expect(ParseUtil.keyvalOptions('ab={x,y=z}')).toEqual({ ab: 'x,y=z' });
  });

  test('keyValue extra open brace', () => {
    const message = trapErrors(() => ParseUtil.keyvalOptions('ab={c{d}'));
    expect(message).toBe('Extra open brace or missing close brace');
  });

  test('keyValue extra close brace', () => {
    const message = trapErrors(() => ParseUtil.keyvalOptions('ab=c{d}}'));
    expect(message).toBe('Extra close brace or missing open brace');
  });

  test('keyValue allowed values', () => {
    expect(ParseUtil.keyvalOptions('ab', { ab: 1 })).toEqual({ ab: true });
    expect(ParseUtil.keyvalOptions('ab', { abc: 1 })).toEqual({});
    expect(
      ParseUtil.keyvalOptions('ab', { ab: KeyValueTypes.boolean })
    ).toEqual({ ab: true });
    expect(
      ParseUtil.keyvalOptions('ab=true', { ab: KeyValueTypes.boolean })
    ).toEqual({ ab: true });
    expect(
      ParseUtil.keyvalOptions('ab=1.2', { ab: KeyValueTypes.number })
    ).toEqual({ ab: 1.2 });
    expect(
      ParseUtil.keyvalOptions('ab=12', { ab: KeyValueTypes.integer })
    ).toEqual({ ab: 12 });
    expect(
      ParseUtil.keyvalOptions('ab=xy', { ab: KeyValueTypes.string })
    ).toEqual({ ab: 'xy' });
    expect(
      ParseUtil.keyvalOptions('ab=x', { ab: KeyValueDef.oneof('x', 'y') })
    ).toEqual({ ab: 'x' });
    expect(
      ParseUtil.keyvalOptions('ab=y', { ab: KeyValueDef.oneof('x', 'y') })
    ).toEqual({ ab: 'y' });
    expect(
      ParseUtil.keyvalOptions('ab=2em', { ab: KeyValueTypes.dimen })
    ).toEqual({ ab: '2em' });
  });

  test('keyValue allowed with errors', () => {
    function trap(test: string, allow: any) {
      return expect(
        trapErrors(() => ParseUtil.keyvalOptions(test, allow, true))
      );
    }
    const error = "Value for key 'ab' is not of the expected type";
    trap('ab', { abc: 1 }).toBe('Invalid option: ab');
    trap('ab=x', { ab: KeyValueTypes.boolean }).toBe(error);
    trap('ab=x', { ab: KeyValueTypes.number }).toBe(error);
    trap('ab=1.2', { ab: KeyValueTypes.integer }).toBe(error);
    trap('ab=', { ab: KeyValueTypes.string }).toBe('(no error)');
    trap('ab=z', { ab: KeyValueDef.oneof('x', 'y') }).toBe(error);
    trap('ab=2xy', { ab: KeyValueTypes.dimen }).toBe(error);
    trap('ab=2', { ab: KeyValueTypes.dimen }).toBe(error);
  });

  test('fixedFence', () => {
    function create(kind: string, options?: any, children?: any[]) {
      return tex.mmlFactory.create(kind, options, children);
    }
    const mrow = create('mrow', {}, [create('mi')]);
    const mml = ParseUtil.fixedFence(tex.parseOptions, '(', mrow, ')');
    const kinds = mml.childNodes.map((node) => node.kind);
    expect(kinds).toEqual(['MathChoice', 'mi', 'MathChoice']);
  });
});

/**********************************************************************************/

describe('ColumnParser', () => {
  test('Error', () => {
    new CommandMap('cError', {
      cError(parser: any) {
        const array = parser.itemFactory.create('array');
        parser.configuration.columnParser.process(parser, '@{x', array);
      },
    });
    Configuration.create('cError', {
      [ConfigurationType.HANDLER]: {
        [HandlerType.MACRO]: ['cError'],
      },
    });
    setupTex(['base', 'cError']);
    expectTexError('\\cError').toBe('Missing close brace');
  });
});

/**********************************************************************************/
