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

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

void main() {
  test('basic', () {
    final builder = XmlBuilder();
    builder.declaration(encoding: 'UTF-8');
    builder.processing('xml-stylesheet',
        'href="/style.css" type="text/css" title="default stylesheet"');
    builder.element('bookstore', nest: () {
      builder.comment('Only one book?');
      builder.element('book', nest: () {
        builder.element('title', nest: () {
          builder.attribute('lang', 'en');
          builder.text('Harry ');
          builder.cdata('Potter');
        });
        builder.element('price', nest: 29.99);
      });
    });
    final document = builder.buildDocument();
    assertDocumentTreeInvariants(document);
    final actual = document.toString();
    const expected = '<?xml version="1.0" encoding="UTF-8"?>'
        '<?xml-stylesheet href="/style.css" type="text/css" title="default stylesheet"?>'
        '<bookstore>'
        '<!--Only one book?-->'
        '<book>'
        '<title lang="en">Harry <![CDATA[Potter]]></title>'
        '<price>29.99</price>'
        '</book>'
        '</bookstore>';
    expect(actual, expected);
  });
  test('all', () {
    final builder = XmlBuilder();
    builder.declaration();
    builder.doctype('note', systemId: 'Note.dtd');
    builder.processing('processing', 'instruction');
    builder.element('element1', attributes: {'attribute1': 'value1'}, nest: () {
      builder.attribute('attribute2', 'value2',
          attributeType: XmlAttributeType.DOUBLE_QUOTE);
      builder.attribute('attribute3', 'value3',
          attributeType: XmlAttributeType.SINGLE_QUOTE);
      builder.element('element2');
      builder.comment('comment');
      builder.cdata('cdata');
      builder.text('textual');
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<?xml version="1.0"?>'
        '<!DOCTYPE note SYSTEM "Note.dtd">'
        '<?processing instruction?>'
        '<element1 attribute1="value1" attribute2="value2" '
        'attribute3=\'value3\'>'
        '<element2/>'
        '<!--comment-->'
        '<![CDATA[cdata]]>'
        'textual'
        '</element1>';
    expect(actual, expected);
  });
  test('self-closing', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.element('self-closing-default');
      builder.element('self-closing-true', isSelfClosing: true);
      builder.element('self-closing-true-with-children',
          isSelfClosing: true, nest: '!');
      builder.element('self-closing-false', isSelfClosing: false);
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<element>'
        '<self-closing-default/>'
        '<self-closing-true/>'
        '<self-closing-true-with-children>!</self-closing-true-with-children>'
        '<self-closing-false></self-closing-false>'
        '</element>';
    expect(actual, expected);
  });
  test('nested string', () {
    final builder = XmlBuilder();
    builder.element('element', nest: 'string');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<element>string</element>';
    expect(actual, expected);
  });
  test('nested iterable', () {
    final builder = XmlBuilder();
    builder.element('element', nest: [
      () => builder.text('st'),
      'ri',
      ['n', 'g']
    ]);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<element>string</element>';
    expect(actual, expected);
  });
  test('nested node (element)', () {
    final builder = XmlBuilder();
    final nested = XmlElement(XmlName('nested'));
    builder.element('element', nest: nested);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
    expect(xml.children[0].children[0], isNot(same(nested)));
    final actual = xml.toString();
    const expected = '<element><nested/></element>';
    expect(actual, expected);
  });
  test('nested node (element, repeated)', () {
    final builder = XmlBuilder();
    final nested = XmlElement(XmlName('nested'));
    builder.element('element', nest: [nested, nested]);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
    expect(xml.children[0].children[0], isNot(same(nested)));
    expect(xml.children[0].children[1].toXmlString(), nested.toXmlString());
    expect(xml.children[0].children[1], isNot(same(nested)));
    final actual = xml.toString();
    const expected = '<element><nested/><nested/></element>';
    expect(actual, expected);
  });
  test('nested node (text)', () {
    final builder = XmlBuilder();
    final nested = XmlText('text');
    builder.element('element', nest: nested);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
    expect(xml.children[0].children[0], isNot(same(nested)));
    final actual = xml.toString();
    const expected = '<element>text</element>';
    expect(actual, expected);
  });
  test('nested node (text, repeated)', () {
    final builder = XmlBuilder();
    final nested = XmlText('text');
    builder.element('element', nest: [nested, nested]);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    expect(xml.children[0].children[0].value, 'texttext');
    expect(xml.children[0].children[0], isNot(same(nested)));
    final actual = xml.toString();
    const expected = '<element>texttext</element>';
    expect(actual, expected);
  });
  test('nested node (data)', () {
    final builder = XmlBuilder();
    final nested = XmlComment('abc');
    builder.element('element', nest: nested);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    expect(xml.children[0].children[0].toXmlString(), nested.toXmlString());
    expect(xml.children[0].children[0], isNot(same(nested)));
    final actual = xml.toString();
    const expected = '<element><!--abc--></element>';
    expect(actual, expected);
  });
  test('nested node (attribute)', () {
    final builder = XmlBuilder();
    final nested = XmlAttribute(XmlName('foo'), 'bar');
    builder.element('element', nest: nested);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    expect(xml.children[0].attributes[0].toXmlString(), nested.toXmlString());
    expect(xml.children[0].attributes[0], isNot(same(nested)));
    final actual = xml.toString();
    const expected = '<element foo="bar"/>';
    expect(actual, expected);
  });
  test('nested node (document)', () {
    final builder = XmlBuilder();
    final nested = XmlDocument([]);
    expect(() => builder.element('element', nest: nested), throwsArgumentError);
  });
  test('nested node (document fragment)', () {
    final builder = XmlBuilder();
    final nested = XmlDocumentFragment([XmlText('foo'), XmlComment('bar')]);
    builder.element('element', nest: nested);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    expect(xml.children[0].children[0].toXmlString(),
        nested.children[0].toXmlString());
    expect(xml.children[0].children[0], isNot(same(nested.children[0])));
    expect(xml.children[0].children[1].toXmlString(),
        nested.children[1].toXmlString());
    expect(xml.children[0].children[1], isNot(same(nested.children[1])));
    final actual = xml.toString();
    const expected = '<element>foo<!--bar--></element>';
    expect(actual, expected);
  });
  test('text', () {
    final builder = XmlBuilder();
    builder.element('text', nest: () {
      builder.text('abc');
      builder.text('');
      builder.text('def');
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<text>abcdef</text>';
    expect(actual, expected);
  });
  test('doctype (plain)', () {
    final builder = XmlBuilder()
      ..doctype('note')
      ..element('root');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<!DOCTYPE note><root/>';
    expect(actual, expected);
  });
  test('doctype (system ID)', () {
    final builder = XmlBuilder()
      ..doctype('note', systemId: 'system.dtd')
      ..element('root');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<!DOCTYPE note SYSTEM "system.dtd"><root/>';
    expect(actual, expected);
  });
  test('doctype (public ID)', () {
    final builder = XmlBuilder()
      ..doctype('note', publicId: 'public.dtd', systemId: 'system.dtd')
      ..element('root');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<!DOCTYPE note PUBLIC "public.dtd" "system.dtd"><root/>';
    expect(actual, expected);
  });
  test('doctype (internal subset)', () {
    final builder = XmlBuilder()
      ..doctype('note', internalSubset: '<!ELEMENT br EMPTY>')
      ..element('root');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<!DOCTYPE note [<!ELEMENT br EMPTY>]><root/>';
    expect(actual, expected);
  });
  test('doctype (error)', () {
    final builder = XmlBuilder();
    expect(() => builder.doctype('note', publicId: 'public.dtd'),
        throwsArgumentError);
  });
  test('attribute', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.attribute('foo', 'bar');
    });
    final xml = builder.buildDocument();
    assertDocumentInvariants(xml);
    final actual = xml.toString();
    expect(actual, '<element foo="bar"/>');
  });
  test('attribute (single quote)', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.attribute('foo', 'bar',
          attributeType: XmlAttributeType.SINGLE_QUOTE);
    });
    final xml = builder.buildDocument();
    assertDocumentInvariants(xml);
    final actual = xml.toString();
    expect(actual, '<element foo=\'bar\'/>');
  });
  test('attribute (replaced)', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.attribute('foo', 'bar');
      builder.attribute('foo', 'zork');
    });
    final xml = builder.buildDocument();
    assertDocumentInvariants(xml);
    final actual = xml.toString();
    expect(actual, '<element foo="zork"/>');
  });
  test('attribute (removed)', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.attribute('foo', 'bar');
      builder.attribute('foo', null);
    });
    final xml = builder.buildDocument();
    assertDocumentInvariants(xml);
    final actual = xml.toString();
    expect(actual, '<element/>');
  });
  test('attribute (multiple)', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      for (var i = 1; i <= 5; i++) {
        builder.attribute('a$i', i);
      }
    });
    final xml = builder.buildDocument();
    assertDocumentInvariants(xml);
    final actual = xml.toString();
    expect(actual, '<element a1="1" a2="2" a3="3" a4="4" a5="5"/>');
  });
  test('attribute (update)', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.attribute('lang', 'en');
      builder.attribute('lang', 'de');
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<element lang="de"/>';
    expect(actual, expected);
  });
  test('attribute (update with namespace)', () {
    const namespaces = {
      'http://www.w3.org/1999/xhtml': 'xhtml',
      'http://www.w3.org/1999/xlink': 'xlink',
    };
    final builder = XmlBuilder();
    builder.element('element', namespaces: namespaces, nest: () {
      for (final uri in namespaces.keys) {
        builder.attribute('lang', 'en', namespace: uri);
      }
      for (final uri in namespaces.keys) {
        builder.attribute('lang', 'de', namespace: uri);
      }
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<element '
        'xmlns:xhtml="http://www.w3.org/1999/xhtml" '
        'xmlns:xlink="http://www.w3.org/1999/xlink" '
        'xhtml:lang="de" xlink:lang="de"/>';
    expect(actual, expected);
  });
  test('attribute (update with unknown namespace)', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.attribute('xhtml:lang', 'en');
      builder.attribute('xhtml:lang', 'de');
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<element xhtml:lang="de"/>';
    expect(actual, expected);
  });
  test('xml', () {
    final builder = XmlBuilder();
    builder.xml('<element attr="value"/>');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<element attr="value"/>';
    expect(actual, expected);
  });
  test('xml (nested)', () {
    final builder = XmlBuilder();
    builder.element('outer', nest: () {
      builder.xml('<inner attr="value"/>');
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<outer><inner attr="value"/></outer>';
    expect(actual, expected);
  });
  test('xml (multiple)', () {
    final builder = XmlBuilder();
    builder.element('outer', nest: () {
      builder.xml('<inner1/>');
      builder.xml('<inner2>hello</inner2>');
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<outer><inner1/><inner2>hello</inner2></outer>';
    expect(actual, expected);
  });
  test('xml (invalid)', () {
    final builder = XmlBuilder();
    builder.element('outer', nest: () {
      expect(
          () => builder.xml('<broken>'),
          throwsA(isXmlTagException(
            message: 'Missing </broken>',
            expectedName: 'broken',
            actualName: isNull,
            buffer: '<broken>',
            position: 8,
          )));
    });
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<outer/>';
    expect(actual, expected);
  });
  test('namespace binding', () {
    const uri = 'http://www.w3.org/2001/XMLSchema';
    final builder = XmlBuilder();
    builder.element('schema', nest: () {
      builder.namespace(uri, 'xsd');
      builder.attribute('lang', 'en', namespace: uri);
      builder.element('element', namespace: uri);
    }, namespace: uri);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected =
        '<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsd:lang="en">'
        '<xsd:element/>'
        '</xsd:schema>';
    expect(actual, expected);
  });
  test('default namespace binding', () {
    const uri = 'http://www.w3.org/2001/XMLSchema';
    final builder = XmlBuilder();
    builder.element('schema', nest: () {
      builder.namespace(uri);
      builder.attribute('lang', 'en', namespace: uri);
      builder.element('element', namespace: uri);
    }, namespace: uri);
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected =
        '<schema xmlns="http://www.w3.org/2001/XMLSchema" lang="en">'
        '<element/>'
        '</schema>';
    expect(actual, expected);
  });
  test('undefined namespace', () {
    final builder = XmlBuilder();
    expect(() => builder.element('element', namespace: 'http://foo.com/'),
        throwsArgumentError);
  });
  test('invalid namespace', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      expect(() => builder.namespace('http://foo.com/', 'xml'),
          throwsArgumentError);
      expect(() => builder.namespace('http://2.foo.com/', 'xmlns'),
          throwsArgumentError);
    });
    final actual = builder.buildDocument().toString();
    const expected = '<element/>';
    expect(actual, expected);
  });
  test('conflicting namespace', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.namespace('http://foo.com/', 'foo');
      expect(() => builder.namespace('http://2.foo.com/', 'foo'),
          throwsArgumentError);
    }, namespace: 'http://foo.com/');
    final actual = builder.buildDocument().toString();
    const expected = '<foo:element xmlns:foo="http://foo.com/"/>';
    expect(actual, expected);
  });
  test('unused namespace', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.namespace('http://foo.com/', 'foo');
    });
    final actual = builder.buildDocument().toString();
    const expected = '<element xmlns:foo="http://foo.com/"/>';
    expect(actual, expected);
  });
  test('unused namespace (optimized)', () {
    final builder = XmlBuilder(optimizeNamespaces: true);
    builder.element('element', nest: () {
      builder.namespace('http://foo.com/', 'foo');
    });
    final actual = builder.buildDocument().toString();
    const expected = '<element/>';
    expect(actual, expected);
  });
  test('duplicate namespace', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      builder.namespace('http://foo.com/', 'foo');
      builder.element('outer', nest: () {
        builder.namespace('http://foo.com/', 'foo');
        builder.element('inner', nest: () {
          builder.namespace('http://foo.com/', 'foo');
          builder.attribute('lang', 'en', namespace: 'http://foo.com/');
        });
      });
    });
    final actual = builder.buildDocument().toString();
    const expected = '<element xmlns:foo="http://foo.com/">'
        '<outer xmlns:foo="http://foo.com/">'
        '<inner xmlns:foo="http://foo.com/" foo:lang="en"/>'
        '</outer>'
        '</element>';
    expect(actual, expected);
  });
  test('duplicate namespace on attribute (optimized)', () {
    final builder = XmlBuilder(optimizeNamespaces: true);
    builder.element('element', nest: () {
      builder.namespace('http://foo.com/', 'foo');
      builder.element('outer', nest: () {
        builder.namespace('http://foo.com/', 'foo');
        builder.element('inner', nest: () {
          builder.namespace('http://foo.com/', 'foo');
          builder.attribute('lang', 'en', namespace: 'http://foo.com/');
        });
      });
    });
    final actual = builder.buildDocument().toString();
    const expected = '<element xmlns:foo="http://foo.com/">'
        '<outer>'
        '<inner foo:lang="en"/>'
        '</outer>'
        '</element>';
    expect(actual, expected);
  });
  test('duplicate namespace on element (optimized)', () {
    final builder = XmlBuilder(optimizeNamespaces: true);
    builder.element('element', nest: () {
      builder.namespace('http://foo.com/', 'foo');
      builder.element('outer', nest: () {
        builder.namespace('http://foo.com/', 'foo');
        builder.element('inner', namespace: 'http://foo.com/');
      });
    });
    final actual = builder.buildDocument().toString();
    const expected = '<element xmlns:foo="http://foo.com/">'
        '<outer>'
        '<foo:inner/>'
        '</outer>'
        '</element>';
    expect(actual, expected);
  });
  test('namespace defined with element', () {
    final builder = XmlBuilder();
    builder.element('element', namespaces: {
      'http://foo.com/': 'foo',
      'http://bar.com/': null,
    });
    final actual = builder.buildDocument().toString();
    const expected = '<element xmlns:foo="http://foo.com/" '
        'xmlns="http://bar.com/"/>';
    expect(actual, expected);
  });
  test('entities cdata escape', () {
    final builder = XmlBuilder();
    builder.element('element', nest: '<test><![CDATA[string]]></test>');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected =
        '<element>&lt;test>&lt;![CDATA[string]]&gt;&lt;/test></element>';
    expect(actual, expected);
  });
  test('declaration', () {
    final builder = XmlBuilder();
    builder.declaration(
        version: '0.5', encoding: 'ASCII', attributes: {'foo': 'bar'});
    builder.element('data');
    final xml = builder.buildDocument();
    assertDocumentTreeInvariants(xml);
    final actual = xml.toString();
    const expected = '<?xml version="0.5" encoding="ASCII" foo="bar"?><data/>';
    expect(actual, expected);
  });
  test('declaration outside of document', () {
    final builder = XmlBuilder();
    expect(
        () => builder.element('data', nest: builder.declaration),
        throwsA(isXmlNodeTypeException(
            message: startsWith('Got XmlNodeType.DECLARATION'),
            node: isNotNull,
            types: contains(XmlNodeType.ELEMENT))));
  });
  test('exception during nesting', () {
    final builder = XmlBuilder();
    builder.element('outer', nest: () {
      expect(
        () => builder.element('inner', nest: () => throw UnimplementedError()),
        throwsUnsupportedError,
      );
    });
    final document = builder.buildDocument();
    expect(document.toString(), '<outer/>');
  });
  test('incomplete builder', () {
    final builder = XmlBuilder();
    builder.element('element', nest: () {
      expect(builder.buildDocument, throwsStateError);
    });
    final document = builder.buildDocument();
    expect(document.toString(), '<element/>');
  });
  test('reused builder', () {
    final builder = XmlBuilder();
    builder.element('element-one');
    final firstDocument = builder.buildDocument();
    expect(firstDocument.toString(), '<element-one/>');
    builder.element('element-two');
    final secondDocument = builder.buildDocument();
    expect(secondDocument.toString(), '<element-two/>');
  });
  test('fragment builder', () {
    final builder = XmlBuilder();
    builder.element('element-one');
    builder.element('element-two');
    final xml = builder.buildFragment();
    assertFragmentInvariants(xml);
  });
}
