import 'package:test/test.dart';
import 'package:xml/xml.dart';

import 'utils/assertions.dart';
import 'utils/matchers.dart';

const nameStartChars = {
  'A',
  'Z',
  '_',
  'a',
  'z',
  '\u{c0}',
  '\u{d6}',
  '\u{d8}',
  '\u{f6}',
  '\u{f8}',
  '\u{2ff}',
  '\u{370}',
  '\u{37d}',
  '\u{37f}',
  '\u{1fff}',
  '\u{200c}',
  '\u{200d}',
  '\u{2070}',
  '\u{218f}',
  '\u{2c00}',
  '\u{2fef}',
  '\u{3001}',
  '\u{d7ff}',
  '\u{f900}',
  '\u{fdcf}',
  '\u{fdf0}',
  '\u{fffd}',
  '\u{10000}',
  '\u{effff}',
};
const nameChars = {
  ...nameStartChars,
  '-',
  '.',
  '0',
  '9',
  '\u{B7}',
  '\u{300}',
  '\u{36F}',
  '\u{203F}',
  '\u{2040}',
};

void main() {
  group('document', () {
    test('cdata', () {
      assertDocumentParseInvariants('<data><![CDATA[]]></data>');
    });
    test('cdata with xml', () {
      assertDocumentParseInvariants('<data><![CDATA[<data></data>]]></data>');
    });
    test('comment', () {
      assertDocumentParseInvariants(
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<schema><!-- comment --></schema>',
      );
    });
    test('comment with xml', () {
      assertDocumentParseInvariants(
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<schema><!-- <foo></foo> --></schema>',
      );
    });
    test('declaration', () {
      assertDocumentParseInvariants('<?xml?><data />');
    });
    test('declaration with attribute', () {
      assertDocumentParseInvariants('<?xml version="1.0"?><data />');
    });
    test('doctype (system)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root-name SYSTEM "uri-reference">'
        '<root />',
      );
    });
    test('doctype (public)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root-name PUBLIC "public-identifier" "uri-reference">'
        '<root />',
      );
    });
    test('doctype (empty)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root []>\n'
        '<root />',
      );
    });
    test('doctype (comment)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!-- comment -->\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (processing)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <?processing?>\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (element type declarations)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ELEMENT br EMPTY>\n'
        '  <!ELEMENT p (#PCDATA|emph)* >\n'
        '  <!ELEMENT %name.para; %content.para; >\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (element content models)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ELEMENT spec (front, body, back?)>\n'
        '  <!ELEMENT div1 (head, (p | list | note)*, div2*)>\n'
        '  <!ELEMENT dictionary-body (%div.mix; | %dict.mix;)*>\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (element mixed content)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ELEMENT p (#PCDATA|a|ul|b|i|em)*>\n'
        '  <!ELEMENT p (#PCDATA | %font; | %phrase; | %special; | %form;)* >\n'
        '  <!ELEMENT b (#PCDATA)>\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (attribute-list)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ATTLIST termdef\n'
        '    id      ID      #REQUIRED\n'
        '    name    CDATA   #IMPLIED>\n'
        '  <!ATTLIST list\n'
        '    type    (bullets|ordered|glossary)  "ordered">\n'
        '  <!ATTLIST form\n'
        '    method  CDATA   #FIXED "POST">\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (internal entity)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ENTITY Pub-Status "This is a pre-release.">\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (internal entity, included)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ENTITY % YN "Yes" >\n'
        '  <!ENTITY WhatHeSaid "He said %YN;" >\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (internal entity, replacement text)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ENTITY % pub    "&#xc9;ditions Gallimard" >\n'
        '  <!ENTITY   rights "All rights reserved" >\n'
        '  <!ENTITY   book   "La Peste: Albert Camus,\n'
        '    &#xA9; 1947 %pub;. &rights;" >\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (entity reference)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ENTITY % ISOLat2\n'
        '    SYSTEM "http://www.xml.com/iso/isolat2-xml.entities" >\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (external entities )', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!ENTITY open-hatch\n'
        '    SYSTEM "http://www.textuality.com/boilerplate/OpenHatch.xml">\n'
        '  <!ENTITY open-hatch\n'
        '    PUBLIC "-//Textuality//TEXT Standard open-hatch boilerplate//EN"\n'
        '    "http://www.textuality.com/boilerplate/OpenHatch.xml">\n'
        '  <!ENTITY hatch-pic\n'
        '    SYSTEM "../grafix/OpenHatch.gif"\n'
        '    NDATA gif >\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (notation)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE root [\n'
        '  <!NOTATION open-hatch\n'
        '    SYSTEM "http://www.textuality.com/boilerplate/OpenHatch.xml">\n'
        '  <!NOTATION open-hatch\n'
        '    PUBLIC "-//Textuality//TEXT Standard open-hatch boilerplate//EN">\n'
        '  <!NOTATION open-hatch\n'
        '    PUBLIC "-//Textuality//TEXT Standard open-hatch boilerplate//EN"\n'
        '    "http://www.textuality.com/boilerplate/OpenHatch.xml">\n'
        ']>\n'
        '<root />',
      );
    });
    test('doctype (ambiguous)', () {
      assertDocumentParseInvariants(
        '<!DOCTYPE ambiguous [\n'
        '  <!-- comment [<brackets>] -->\n'
        '  <?pi processing="[<brackets>]" ?>\n'
        '  <!NOTATION not PUBLIC "[<brackets>]" \'[<brackets>]\'>\n'
        '  <!ENTITY entity1 "[<brackets>]">\n'
        '  <!ENTITY entity2 \'[<brackets>]\'>\n'
        '  <!ENTITY % entity3 "[<brackets>]">\n'
        '  <!ENTITY % entity4 \'[<brackets>]\'>\n'
        '  <!ELEMENT element1 (#PCDATA)>\n'
        '  <!ELEMENT element2 (element1 | element2)+>\n'
        '  <!ELEMENT element3 (#PCDATA , element3)*>\n'
        '  <!ATTLIST attlist1 entity1 (foo | bar)>\n'
        '  <!ATTLIST attlist2 entity2 "[<brackets>]" #REQUIRED>\n'
        '  <!ATTLIST attlist2 entity3 \'[<brackets>]\' #IMPLIED>\n'
        ']>\n'
        '<root />',
      );
    });
    test('element', () {
      assertDocumentParseInvariants('<root/>');
      assertDocumentParseInvariants('<root />');
      assertDocumentParseInvariants('<root key="value"/>');
      assertDocumentParseInvariants('<root key="value" />');
    });
    test('element with namespace', () {
      assertDocumentParseInvariants('<xs:schema xs:attr="1"></xs:schema>');
    });
    test('element with closing', () {
      assertDocumentParseInvariants('<schema></schema>');
    });
    test('element with double quote attribute', () {
      assertDocumentParseInvariants('<schema foo="bar"></schema>');
      assertDocumentParseInvariants('<schema foo="bar" take="drop"></schema>');
    });
    test('element with single quote attribute', () {
      assertDocumentParseInvariants("<schema foo='bar'></schema>");
      assertDocumentParseInvariants("<schema foo='bar' take='drop'></schema>");
    });
    test('element with attribute without quotes', () {
      assertDocumentParseInvariants('<schema foo=bar></schema>');
      assertDocumentParseInvariants('<schema foo=bar take=drop></schema>');
    });
    test('element with attribute without value', () {
      assertDocumentParseInvariants('<schema foo></schema>');
      assertDocumentParseInvariants('<schema foo bar></schema>');
    });
    test('element with special initial char', () {
      for (final char in nameStartChars) {
        final qualified = char;
        final closed = XmlDocument.parse('<$qualified />');
        expect(closed.rootElement.name.qualified, qualified);
        final openClosed = XmlDocument.parse('<$qualified></$qualified>');
        expect(openClosed.rootElement.name.qualified, qualified);
      }
    });
    test('element with special continuation char', () {
      for (final char in nameChars) {
        final qualified = 'x$char';
        final closed = XmlDocument.parse('<$qualified />');
        expect(closed.rootElement.name.qualified, qualified);
        final openClosed = XmlDocument.parse('<$qualified></$qualified>');
        expect(openClosed.rootElement.name.qualified, qualified);
      }
    });
    test('element with attribute with special initial char', () {
      for (final char in nameStartChars) {
        final qualified = char;
        final root = XmlDocument.parse('<root $qualified="ok" />');
        expect(root.rootElement.attributes.first.name.qualified, qualified);
        expect(root.rootElement.getAttribute(qualified), 'ok');
      }
    });
    test('element with attribute with with special continuation char', () {
      for (final char in nameChars) {
        final qualified = 'x$char';
        final root = XmlDocument.parse('<root $qualified="ok" />');
        expect(root.rootElement.attributes.first.name.qualified, qualified);
        expect(root.rootElement.getAttribute(qualified), 'ok');
      }
    });
    test('processing instruction', () {
      assertDocumentParseInvariants('<?pi?><data />');
    });
    test('processing instruction with attribute', () {
      assertDocumentParseInvariants('<?pi foo="bar"?><data />');
    });
    test('document with comments', () {
      assertDocumentParseInvariants(
        '<?xml version="1.0"?>'
        '<!--comment-->'
        '<!DOCTYPE root-name SYSTEM "uri-reference">'
        '<data />',
      );
      assertDocumentParseInvariants(
        '<?xml version="1.0"?>'
        '<!DOCTYPE root-name SYSTEM "uri-reference">'
        '<!--comment-->'
        '<data />',
      );
      assertDocumentParseInvariants(
        '<?xml version="1.0"?>'
        '<!DOCTYPE root-name SYSTEM "uri-reference">'
        '<data />'
        '<!--comment-->',
      );
    });
    group('validation errors', () {
      test('empty', () {
        expect(
          () => XmlDocument.parse(''),
          throwsA(
            isXmlParserException(
              message: 'Expected a single root element',
              position: 0,
            ),
          ),
        );
      });
      test('whitespace', () {
        expect(
          () => XmlDocument.parse('  '),
          throwsA(
            isXmlParserException(
              message: 'Expected a single root element',
              position: 2,
            ),
          ),
        );
      });
      test('repeated declaration', () {
        expect(
          () => XmlDocument.parse(
            '<?xml version="1.0"?>'
            '<?xml version="1.1"?>'
            '<root />',
          ),
          throwsA(
            isXmlParserException(
              message: 'Expected at most one XML declaration',
              position: 21,
            ),
          ),
        );
      });
      test('repeated doctype', () {
        expect(
          () => XmlDocument.parse(
            '<!DOCTYPE root-name SYSTEM "uri-ref">'
            '<!DOCTYPE root-name PUBLIC "pub-id" "uri-ref">'
            '<root />',
          ),
          throwsA(
            isXmlParserException(
              message: 'Expected at most one doctype declaration',
              position: 37,
            ),
          ),
        );
      });
      test('unexpected declaration', () {
        expect(
          () => XmlDocument.parse(
            '<!DOCTYPE root-name SYSTEM "uri-ref">'
            '<?xml version="1.1"?>',
          ),
          throwsA(
            isXmlParserException(
              message: 'Unexpected XML declaration',
              position: 37,
            ),
          ),
        );
        expect(
          () => XmlDocument.parse(
            '<root />'
            '<?xml version="1.1"?>',
          ),
          throwsA(
            isXmlParserException(
              message: 'Unexpected XML declaration',
              position: 8,
            ),
          ),
        );
      });
      test('unexpected doctype', () {
        expect(
          () => XmlDocument.parse(
            '<root />'
            '<!DOCTYPE root-name SYSTEM "uri-ref">',
          ),
          throwsA(
            isXmlParserException(
              message: 'Unexpected doctype declaration',
              position: 8,
            ),
          ),
        );
      });
      test('unexpected root element', () {
        expect(
          () => XmlDocument.parse('<root1 /><root2 />'),
          throwsA(
            isXmlParserException(
              message: 'Unexpected root element',
              position: 9,
            ),
          ),
        );
      });
    });
    group('parse errors', () {
      test('nesting', () {
        expect(
          () => XmlDocument.parse('<foo></bar>'),
          throwsA(
            isXmlTagException(
              message: 'Expected </foo>, but found </bar>',
              position: 5,
            ),
          ),
        );
        expect(
          () => XmlDocument.parse('<bar>'),
          throwsA(isXmlTagException(message: 'Missing </bar>', position: 5)),
        );
        expect(
          () => XmlDocument.parse('</bar>'),
          throwsA(isXmlTagException(message: 'Unexpected </bar>', position: 0)),
        );
      });
      test('element', () {
        expect(
          () => XmlDocument.parse('<'),
          throwsA(isXmlParserException(message: 'name expected', position: 1)),
        );
        expect(
          () => XmlDocument.parse('<data'),
          throwsA(isXmlParserException(message: '">" expected', position: 5)),
        );
        expect(
          () => XmlDocument.parse('<data key'),
          throwsA(isXmlParserException(message: '">" expected', position: 9)),
        );
        expect(
          () => XmlDocument.parse('<data key="ab'),
          throwsA(isXmlParserException(message: '">" expected', position: 9)),
        );
      });
      test('comment', () {
        expect(
          () => XmlDocument.parse('<!--'),
          throwsA(isXmlParserException(message: '"-->" expected', position: 4)),
        );
        expect(
          () => XmlDocument.parse('<!-- comment'),
          throwsA(isXmlParserException(message: '"-->" expected', position: 4)),
        );
      });
      test('cdata', () {
        expect(
          () => XmlDocument.parse('<![CDATA['),
          throwsA(isXmlParserException(message: '"]]>" expected', position: 9)),
        );
        expect(
          () => XmlDocument.parse('<![CDATA[ cdata'),
          throwsA(isXmlParserException(message: '"]]>" expected', position: 9)),
        );
      });
      test('doctype', () {
        expect(
          () => XmlDocument.parse('<!DOCTYPE'),
          throwsA(
            isXmlParserException(message: 'whitespace expected', position: 9),
          ),
        );
        expect(
          () => XmlDocument.parse('<!DOCTYPE data'),
          throwsA(isXmlParserException(message: '">" expected', position: 14)),
        );
        expect(
          () => XmlDocument.parse('<!DOCTYPE data ['),
          throwsA(isXmlParserException(message: '">" expected', position: 15)),
        );
      });
      test('declaration', () {
        expect(
          () => XmlDocument.parse('<?'),
          throwsA(isXmlParserException(message: 'name expected', position: 2)),
        );
        expect(
          () => XmlDocument.parse('<?xml'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 5)),
        );
        expect(
          () => XmlDocument.parse('<?xml version'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 13)),
        );
        expect(
          () => XmlDocument.parse('<?xml version='),
          throwsA(isXmlParserException(message: '"?>" expected', position: 13)),
        );
        expect(
          () => XmlDocument.parse('<?xml version="1.0'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 13)),
        );
      });
      test('processing', () {
        expect(
          () => XmlDocument.parse('<?'),
          throwsA(isXmlParserException(message: 'name expected', position: 2)),
        );
        expect(
          () => XmlDocument.parse('<?processing'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 12)),
        );
        expect(
          () => XmlDocument.parse('<?processing whatever'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 12)),
        );
      });
    });
  });
  group('fragment', () {
    test('cdata', () {
      assertFragmentParseInvariants('<![CDATA[]]>');
    });
    test('cdata with xml', () {
      assertFragmentParseInvariants('<![CDATA[<data></data>]]>');
    });
    test('comment', () {
      assertFragmentParseInvariants('<!-- comment -->');
    });
    test('comment with xml', () {
      assertFragmentParseInvariants('<!-- <foo></foo> -->');
    });
    test('declaration', () {
      assertFragmentParseInvariants('<?xml?><data />');
    });
    test('declaration with attribute', () {
      assertFragmentParseInvariants('<?xml version="1.0"?><data />');
    });
    test('doctype (system)', () {
      assertFragmentParseInvariants(
        '<!DOCTYPE root-name SYSTEM "uri-reference">',
      );
    });
    test('doctype (public)', () {
      assertFragmentParseInvariants(
        '<!DOCTYPE root-name PUBLIC "public-identifier" "uri-reference">',
      );
    });
    test('doctype (subset)', () {
      assertFragmentParseInvariants(
        '<!DOCTYPE root ['
        '  <!ELEMENT root (child)>'
        '  <!ATTLIST root attribute #IMPLIED>'
        '  <!ENTITY copy "©">'
        ']>',
      );
    });
    test('doctype (combined)', () {
      assertFragmentParseInvariants(
        '<!DOCTYPE root SYSTEM "uri-reference" ['
        '  <!ELEMENT root (child)>'
        '  <!ATTLIST root attribute #IMPLIED>'
        '  <!ENTITY copy "©">'
        ']>',
      );
    });
    test('element', () {
      assertFragmentParseInvariants('<root/>');
      assertFragmentParseInvariants('<root />');
      assertFragmentParseInvariants('<root key="value"/>');
      assertFragmentParseInvariants('<root key="value" />');
    });
    test('element with namespace', () {
      assertFragmentParseInvariants('<xs:schema xs:attr="1"></xs:schema>');
    });
    test('element with closing', () {
      assertFragmentParseInvariants('<schema></schema>');
    });
    test('element double quote attribute', () {
      assertFragmentParseInvariants('<schema foo="bar"></schema>');
    });
    test('element single quote attribute', () {
      assertFragmentParseInvariants('<schema foo=\'bar\'></schema>');
    });
    test('processing instruction', () {
      assertFragmentParseInvariants('<?pi?><data />');
    });
    test('processing instruction with attribute', () {
      assertFragmentParseInvariants('<?pi foo="bar"?><data />');
    });
    test('text', () {
      assertFragmentParseInvariants(
        'I have a heart I swear I do, '
        'Just not baby when it comes to you.',
      );
    });
    test('empty', () {
      assertFragmentParseInvariants('');
      assertFragmentParseInvariants(' ');
      assertFragmentParseInvariants('\t');
      assertFragmentParseInvariants('\n');
      assertFragmentParseInvariants('  ');
    });
    group('parse errors', () {
      test('nesting', () {
        expect(
          () => XmlDocumentFragment.parse('<foo></bar>'),
          throwsA(
            isXmlTagException(
              message: 'Expected </foo>, but found </bar>',
              position: 5,
            ),
          ),
        );
        expect(
          () => XmlDocumentFragment.parse('<bar>'),
          throwsA(isXmlTagException(message: 'Missing </bar>', position: 5)),
        );
        expect(
          () => XmlDocumentFragment.parse('</bar>'),
          throwsA(isXmlTagException(message: 'Unexpected </bar>', position: 0)),
        );
      });
      test('element', () {
        expect(
          () => XmlDocumentFragment.parse('<'),
          throwsA(isXmlParserException(message: 'name expected', position: 1)),
        );
        expect(
          () => XmlDocumentFragment.parse('<data'),
          throwsA(isXmlParserException(message: '">" expected', position: 5)),
        );
        expect(
          () => XmlDocumentFragment.parse('<data key'),
          throwsA(isXmlParserException(message: '">" expected', position: 9)),
        );
        expect(
          () => XmlDocumentFragment.parse('<data key="ab'),
          throwsA(isXmlParserException(message: '">" expected', position: 9)),
        );
      });
      test('comment', () {
        expect(
          () => XmlDocumentFragment.parse('<!--'),
          throwsA(isXmlParserException(message: '"-->" expected', position: 4)),
        );
        expect(
          () => XmlDocumentFragment.parse('<!-- comment'),
          throwsA(isXmlParserException(message: '"-->" expected', position: 4)),
        );
      });
      test('cdata', () {
        expect(
          () => XmlDocumentFragment.parse('<![CDATA['),
          throwsA(isXmlParserException(message: '"]]>" expected', position: 9)),
        );
        expect(
          () => XmlDocumentFragment.parse('<![CDATA[ cdata'),
          throwsA(isXmlParserException(message: '"]]>" expected', position: 9)),
        );
      });
      test('doctype', () {
        expect(
          () => XmlDocumentFragment.parse('<!DOCTYPE'),
          throwsA(
            isXmlParserException(message: 'whitespace expected', position: 9),
          ),
        );
        expect(
          () => XmlDocumentFragment.parse('<!DOCTYPE data'),
          throwsA(isXmlParserException(message: '">" expected', position: 14)),
        );
        expect(
          () => XmlDocumentFragment.parse('<!DOCTYPE data ['),
          throwsA(isXmlParserException(message: '">" expected', position: 15)),
        );
      });
      test('declaration', () {
        expect(
          () => XmlDocumentFragment.parse('<?'),
          throwsA(isXmlParserException(message: 'name expected', position: 2)),
        );
        expect(
          () => XmlDocumentFragment.parse('<?xml'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 5)),
        );
        expect(
          () => XmlDocumentFragment.parse('<?xml version'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 13)),
        );
        expect(
          () => XmlDocumentFragment.parse('<?xml version='),
          throwsA(isXmlParserException(message: '"?>" expected', position: 13)),
        );
        expect(
          () => XmlDocumentFragment.parse('<?xml version="1.0'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 13)),
        );
      });
      test('processing', () {
        expect(
          () => XmlDocumentFragment.parse('<?'),
          throwsA(isXmlParserException(message: 'name expected', position: 2)),
        );
        expect(
          () => XmlDocumentFragment.parse('<?processing'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 12)),
        );
        expect(
          () => XmlDocumentFragment.parse('<?processing whatever'),
          throwsA(isXmlParserException(message: '"?>" expected', position: 12)),
        );
      });
    });
  });
}
