/*
 * @author acrazing joking.young@gmail.com
 * @since 2017-08-19 14:15:25
 */

import * as assert from 'node:assert';
import { tokenize, TokenKind } from './tokenize';
import type { IToken } from './tokenize';

interface ICase {
  name: string;
  input: string;
  tokens: IToken[];
}

export let tokenIndex = 0;

export function token(value: string, type: TokenKind = TokenKind.Literal, start = tokenIndex) {
  const v = {
    start: start,
    end: start + value.length,
    value,
    type,
  };
  tokenIndex = v.end;
  return v;
}

const cases: ICase[] = [
  {
    name: 'single Literal',
    input: 'hello',
    tokens: [token('hello', TokenKind.Literal, 0)],
  },
  {
    name: 'Literal end with <',
    input: 'hello<',
    tokens: [token('hello', void 0, 0), token('<')],
  },
  {
    name: 'Literal unexpected <',
    input: 'hello< world',
    tokens: [token('hello', void 0, 0), token('< world')],
  },
  {
    name: 'OpenTag EOF',
    input: '<div',
    tokens: [token('div', TokenKind.OpenTag, 1)],
  },
  {
    name: 'attribute names',
    input: "<div a1 'b2' \"c3\" 'd4'e5 'f6\"' \"g7'\"></div>",
    tokens: [
      token('div', TokenKind.OpenTag, 1),
      token(' ', TokenKind.Whitespace),
      token('a1', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token("'b2'", TokenKind.AttrValueSq),
      token(' ', TokenKind.Whitespace),
      token('"c3"', TokenKind.AttrValueDq),
      token(' ', TokenKind.Whitespace),
      token("'d4'", TokenKind.AttrValueSq),
      token('e5', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token("'f6\"'", TokenKind.AttrValueSq),
      token(' ', TokenKind.Whitespace),
      token('"g7\'"', TokenKind.AttrValueDq),
      token('', TokenKind.OpenTagEnd),
      token('div', TokenKind.CloseTag, tokenIndex + 3),
    ],
  },
  {
    name: 'attribute values',
    input: '<div a b= c=1 d e = f = g \'h\'=i "j"k=lmn o=\'pq\' r="st"u>M</div>',
    tokens: [
      token('div', TokenKind.OpenTag, 1),
      token(' ', TokenKind.Whitespace),
      token('a', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token('b', TokenKind.AttrValueNq),
      token('=', TokenKind.AttrValueEq),
      token(' ', TokenKind.Whitespace),
      token('c', TokenKind.AttrValueNq),
      token('=', TokenKind.AttrValueEq),
      token('1', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token('d', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token('e', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token('=', TokenKind.AttrValueEq),
      token(' ', TokenKind.Whitespace),
      token('f', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token('=', TokenKind.AttrValueEq),
      token(' ', TokenKind.Whitespace),
      token('g', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token("'h'", TokenKind.AttrValueSq),
      token('=', TokenKind.AttrValueEq),
      token('i', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token('"j"', TokenKind.AttrValueDq),
      token('k', TokenKind.AttrValueNq),
      token('=', TokenKind.AttrValueEq),
      token('lmn', TokenKind.AttrValueNq),
      token(' ', TokenKind.Whitespace),
      token('o', TokenKind.AttrValueNq),
      token('=', TokenKind.AttrValueEq),
      token("'pq'", TokenKind.AttrValueSq),
      token(' ', TokenKind.Whitespace),
      token('r', TokenKind.AttrValueNq),
      token('=', TokenKind.AttrValueEq),
      token('"st"', TokenKind.AttrValueDq),
      token('u', TokenKind.AttrValueNq),
      token('', TokenKind.OpenTagEnd),
      token('M', void 0, tokenIndex + 1),
      token('div', TokenKind.CloseTag, tokenIndex + 2),
    ],
  },
  {
    name: 'normal doctype',
    input: '<!doctype html>',
    tokens: [
      token('!doctype', TokenKind.OpenTag, 1),
      token(' ', TokenKind.Whitespace),
      token('html', TokenKind.AttrValueNq),
      token('', TokenKind.OpenTagEnd),
    ],
  },
  {
    name: 'unexpected eof end doctype',
    input: '<!doctype',
    tokens: [token('!doctype', TokenKind.OpenTag, 1)],
  },
  {
    name: 'unexpected eof in doctype',
    input: '<!doctyp',
    tokens: [token('!', TokenKind.OpenTag, 1), token('doctyp')],
  },
  {
    name: 'normal comment',
    input: '<!-- hello world -->',
    tokens: [
      token('!--', TokenKind.OpenTag, 1),
      token(' hello world '),
      token('--', TokenKind.OpenTagEnd),
    ],
  },
  {
    name: 'short comment',
    input: '<? hello world ?><!- hello world ->',
    tokens: [
      token('', TokenKind.OpenTag, 1),
      token('? hello world ?'),
      token('', TokenKind.OpenTagEnd),
      token('!', TokenKind.OpenTag, tokenIndex + 2),
      token('- hello world -'),
      token('', TokenKind.OpenTagEnd),
    ],
  },
  {
    name: 'open tag end',
    input: '<a1><b2/><c3 /><d4  /   ><e5    f6/><g7     /h8><i9      /j10/><k11//>',
    tokens: [
      token('a1', TokenKind.OpenTag, 1),
      token('', TokenKind.OpenTagEnd),
      token('b2', TokenKind.OpenTag, tokenIndex + 2),
      token('/', TokenKind.OpenTagEnd),
      token('c3', TokenKind.OpenTag, tokenIndex + 2),
      token(' ', TokenKind.Whitespace),
      token('/', TokenKind.OpenTagEnd),
      token('d4', TokenKind.OpenTag, tokenIndex + 2),
      token('  ', TokenKind.Whitespace),
      token('/', TokenKind.AttrValueNq),
      token('   ', TokenKind.Whitespace),
      token('', TokenKind.OpenTagEnd),
      token('e5', TokenKind.OpenTag, tokenIndex + 2),
      token('    ', TokenKind.Whitespace),
      token('f6', TokenKind.AttrValueNq),
      token('/', TokenKind.OpenTagEnd),
      token('g7', TokenKind.OpenTag, tokenIndex + 2),
      token('     ', TokenKind.Whitespace),
      token('/', TokenKind.AttrValueNq),
      token('h8', TokenKind.AttrValueNq),
      token('', TokenKind.OpenTagEnd),
      token('i9', TokenKind.OpenTag, tokenIndex + 2),
      token('      ', TokenKind.Whitespace),
      token('/', TokenKind.AttrValueNq),
      token('j10', TokenKind.AttrValueNq),
      token('/', TokenKind.OpenTagEnd),
      token('k11', TokenKind.OpenTag, tokenIndex + 2),
      token('/', TokenKind.AttrValueNq),
      token('/', TokenKind.OpenTagEnd),
    ],
  },
  {
    name: 'close tag',
    input: '</div></ div >',
    tokens: [
      token('div', TokenKind.CloseTag, 2),
      token(' div ', TokenKind.CloseTag, tokenIndex + 3),
    ],
  },
  {
    name: 'special normal comment',
    input: '<!---- - -- ---->',
    tokens: [
      token('!--', TokenKind.OpenTag, 1),
      token('-- '),
      token('- '),
      token('-- '),
      token('-'),
      token('-'),
      token('--', TokenKind.OpenTagEnd),
    ],
  },
  {
    name: 'script',
    input: '<script></div></script</script >',
    tokens: [
      token('script', TokenKind.OpenTag, 1),
      token('', TokenKind.OpenTagEnd),
      token('</div>', TokenKind.Literal, tokenIndex + 1),
      token('</script'),
      token('script ', TokenKind.CloseTag, tokenIndex + 2),
    ],
  },
  {
    name: 'style',
    input: '<style></div></style</style >',
    tokens: [
      token('style', TokenKind.OpenTag, 1),
      token('', TokenKind.OpenTagEnd),
      token('</div>', TokenKind.Literal, tokenIndex + 1),
      token('</style'),
      token('style ', TokenKind.CloseTag, tokenIndex + 2),
    ],
  },
];

describe('simple cases', () => {
  for (const _case of cases) {
    it(`case "${_case.name}"`, () => {
      const tokens = tokenize(_case.input);
      assert.deepStrictEqual(tokens, _case.tokens);
    });
  }
});

function fixedToken(value: string, type: TokenKind, start: number): IToken {
  return {
    start,
    end: start + value.length,
    value,
    type,
  };
}

function textElementTokens(tagName: string, bodyParts: string[]): IToken[] {
  const openEnd = tagName.length + 1;
  let start = openEnd + 1;
  const tokens = [
    fixedToken(tagName, TokenKind.OpenTag, 1),
    fixedToken('', TokenKind.OpenTagEnd, openEnd),
  ];
  for (const bodyPart of bodyParts) {
    tokens.push(fixedToken(bodyPart, TokenKind.Literal, start));
    start += bodyPart.length;
  }
  tokens.push(fixedToken(tagName, TokenKind.CloseTag, start + 2));
  return tokens;
}

describe('text element modes', () => {
  const body = '<span>text</span>';

  for (const tagName of [
    'title',
    'textarea',
    'style',
    'xmp',
    'iframe',
    'noembed',
    'noframes',
    'script',
    'noscript',
  ]) {
    it(`treats ${tagName} content as text until its closing tag`, () => {
      assert.deepStrictEqual(
        tokenize(`<${tagName}>${body}</${tagName}>`),
        textElementTokens(tagName, ['<span>text', '</span>']),
      );
    });
  }

  it('treats noscript content as HTML when scripting is disabled', () => {
    assert.deepStrictEqual(
      tokenize('<noscript><span>text</span></noscript>', {
        scriptingEnabled: false,
      }),
      [
        fixedToken('noscript', TokenKind.OpenTag, 1),
        fixedToken('', TokenKind.OpenTagEnd, 9),
        fixedToken('span', TokenKind.OpenTag, 11),
        fixedToken('', TokenKind.OpenTagEnd, 15),
        fixedToken('text', TokenKind.Literal, 16),
        fixedToken('span', TokenKind.CloseTag, 22),
        fixedToken('noscript', TokenKind.CloseTag, 29),
      ],
    );
  });

  it('ignores a leading LF in textarea content', () => {
    assert.deepStrictEqual(tokenize('<textarea>\n<span>text</span></textarea>'), [
      fixedToken('textarea', TokenKind.OpenTag, 1),
      fixedToken('', TokenKind.OpenTagEnd, 9),
      fixedToken('<span>text', TokenKind.Literal, 11),
      fixedToken('</span>', TokenKind.Literal, 21),
      fixedToken('textarea', TokenKind.CloseTag, 30),
    ]);
  });

  it('treats plaintext content as text until EOF', () => {
    const body = '<span>text</span></plaintext><p>after</p>';
    assert.deepStrictEqual(tokenize(`<plaintext>${body}`), [
      fixedToken('plaintext', TokenKind.OpenTag, 1),
      fixedToken('', TokenKind.OpenTagEnd, 10),
      fixedToken(body, TokenKind.Literal, 11),
    ]);
  });
});
