// ignore_for_file: deprecated_member_use_from_same_package

import 'dart:async';
import 'dart:math' show Random, min;

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

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

@isTestGroup
void chunkedTests<T>(
  String title,
  T Function() factory,
  Stream<T> Function(T input, int Function() splitter) chunker,
  FutureOr<void> Function(Stream<T> stream) callback,
) =>
    group(title, () {
      for (var i = 1; i <= 512; i *= 2) {
        test(
          'chunks equally sized $i',
          () => callback(chunker(factory(), () => i)),
        );
      }
      final random = Random(title.hashCode);
      for (var i = 1; i <= 512; i *= 2) {
        test(
          'chunks randomly sized $i',
          () => callback(chunker(factory(), () => random.nextInt(1 + i))),
        );
      }
    });

Stream<String> stringChunker(String input, int Function() splitter) async* {
  while (input.isNotEmpty) {
    final size = min(splitter(), input.length);
    yield input.substring(0, size);
    input = input.substring(size);
  }
}

Stream<List<T>> listChunker<T>(List<T> input, int Function() splitter) async* {
  while (input.isNotEmpty) {
    final size = min(splitter(), input.length);
    yield input.sublist(0, size);
    input = input.sublist(size);
  }
}

void main() {
  group('events', () {
    for (final entry in allXml.entries) {
      group(entry.key, () {
        late XmlDocument document;
        late String source;
        late List<XmlEvent> events;
        setUp(() {
          document = XmlDocument.parse(entry.value);
          source = document.toXmlString();
          events = parseEvents(source).toList(growable: false);
        });
        chunkedTests<String>(
          'string -> events',
          () => source,
          stringChunker,
          (stream) {
            final actual = stream.toXmlEvents().normalizeEvents().flatten();
            expect(actual, emitsInOrder([...events, emitsDone]));
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> nodes',
          () => events,
          listChunker,
          (stream) {
            final actual = stream.toXmlNodes().flatten();
            final expected =
                document.children.map((node) => predicate<XmlNode>((actual) {
                      compareNode(actual, node);
                      return true;
                    }, 'matches $node'));
            expect(actual, emitsInOrder([...expected, emitsDone]));
          },
        );
        chunkedTests<List<XmlNode>>(
          'nodes -> events',
          () => document.children,
          listChunker,
          (stream) {
            final actual = stream.toXmlEvents().flatten();
            expect(actual, emitsInOrder([...events, emitsDone]));
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> string',
          () => events,
          listChunker,
          (stream) {
            final actual = stream.toXmlString().join();
            expect(actual, completion(source));
          },
        );
        chunkedTests<String>(
          'string -> events -> string',
          () => source,
          stringChunker,
          (stream) {
            final actual =
                stream.toXmlEvents().normalizeEvents().toXmlString().join();
            expect(actual, completion(source));
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> string -> events',
          () => events,
          listChunker,
          (stream) {
            final actual = stream
                .toXmlString()
                .toXmlEvents()
                .normalizeEvents()
                .flatten()
                .toList();
            expect(actual, completion(events));
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> nodes -> events',
          () => events,
          listChunker,
          (stream) {
            final actual = stream.toXmlNodes().toXmlEvents().flatten().toList();
            expect(actual, completion(events));
          },
        );
        chunkedTests<List<XmlNode>>(
          'nodes -> events -> nodes',
          () => document.children,
          listChunker,
          (stream) async {
            final actual =
                await stream.toXmlEvents().toXmlNodes().flatten().toList();
            expect(
                actual,
                pairwiseCompare<XmlNode, XmlNode>(document.children,
                    (actual, expected) {
                  compareNode(actual, expected);
                  return true;
                }, 'not matching'));
          },
        );
        if (entry.value == shiporderXsd) {
          chunkedTests<List<XmlEvent>>(
            'events -> subtree -> nodes',
            () => events,
            listChunker,
            (stream) async {
              final actual = await stream
                  .selectSubtreeEvents((event) => event.name == 'xsd:element')
                  .toXmlNodes()
                  .flatten()
                  .toList();
              final expected = document
                  .findAllElements('element', namespace: '*')
                  .where((element) => !element.ancestors
                      .whereType<XmlElement>()
                      .any((parent) => parent.name.local == 'element'))
                  .toList();
              expect(
                  actual,
                  pairwiseCompare<XmlNode, XmlNode>(expected,
                      (actual, expected) {
                    compareNode(actual, expected);
                    return true;
                  }, 'not matching'));
              actual
                  .expand((node) => [node, ...node.descendants])
                  .whereType<XmlHasName>()
                  .forEach((node) => expect(node.name.namespaceUri, isNull));
            },
          );
          chunkedTests<List<XmlEvent>>(
            'events -> parents -> subtree -> nodes',
            () => events,
            listChunker,
            (stream) async {
              final actual = await stream
                  .withParentEvents()
                  .selectSubtreeEvents((event) => event.name == 'xsd:element')
                  .toXmlNodes()
                  .flatten()
                  .toList();
              final expected = document
                  .findAllElements('element', namespace: '*')
                  .where((element) => !element.ancestors
                      .whereType<XmlElement>()
                      .any((parent) => parent.name.local == 'element'))
                  .toList();
              expect(
                  actual,
                  pairwiseCompare<XmlNode, XmlNode>(expected,
                      (actual, expected) {
                    compareNode(actual, expected);
                    return true;
                  }, 'not matching'));
              actual
                  .expand((node) => [node, ...node.descendants])
                  .whereType<XmlHasName>()
                  .where((node) => node.name.prefix == 'xsd')
                  .forEach((node) => expect(node.name.namespaceUri,
                      'http://www.w3.org/2001/XMLSchema'));
            },
          );
        }
        chunkedTests<List<XmlEvent>>(
          'event -> forEachEvent',
          () => events,
          listChunker,
          (stream) async {
            final cdata = <XmlCDATAEvent>[];
            final comment = <XmlCommentEvent>[];
            final declaration = <XmlDeclarationEvent>[];
            final doctype = <XmlDoctypeEvent>[];
            final endElement = <XmlEndElementEvent>[];
            final processing = <XmlProcessingEvent>[];
            final startElement = <XmlStartElementEvent>[];
            final text = <XmlTextEvent>[];
            await stream.flatten().forEachEvent(
                  onCDATA: cdata.add,
                  onComment: comment.add,
                  onDeclaration: declaration.add,
                  onDoctype: doctype.add,
                  onEndElement: endElement.add,
                  onProcessing: processing.add,
                  onStartElement: startElement.add,
                  onText: text.add,
                );
            expect(cdata, events.whereType<XmlCDATAEvent>());
            expect(comment, events.whereType<XmlCommentEvent>());
            expect(declaration, events.whereType<XmlDeclarationEvent>());
            expect(doctype, events.whereType<XmlDoctypeEvent>());
            expect(endElement, events.whereType<XmlEndElementEvent>());
            expect(processing, events.whereType<XmlProcessingEvent>());
            expect(startElement, events.whereType<XmlStartElementEvent>());
            expect(text, events.whereType<XmlTextEvent>());
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> tapEachEvent',
          () => events,
          listChunker,
          (stream) async {
            final cdata = <XmlCDATAEvent>[];
            final comment = <XmlCommentEvent>[];
            final declaration = <XmlDeclarationEvent>[];
            final doctype = <XmlDoctypeEvent>[];
            final endElement = <XmlEndElementEvent>[];
            final processing = <XmlProcessingEvent>[];
            final startElement = <XmlStartElementEvent>[];
            final text = <XmlTextEvent>[];
            await stream
                .flatten()
                .tapEachEvent(
                  onCDATA: cdata.add,
                  onComment: comment.add,
                  onDeclaration: declaration.add,
                  onDoctype: doctype.add,
                  onEndElement: endElement.add,
                  onProcessing: processing.add,
                  onStartElement: startElement.add,
                  onText: text.add,
                )
                .drain<void>();
            expect(cdata, events.whereType<XmlCDATAEvent>());
            expect(comment, events.whereType<XmlCommentEvent>());
            expect(declaration, events.whereType<XmlDeclarationEvent>());
            expect(doctype, events.whereType<XmlDoctypeEvent>());
            expect(endElement, events.whereType<XmlEndElementEvent>());
            expect(processing, events.whereType<XmlProcessingEvent>());
            expect(startElement, events.whereType<XmlStartElementEvent>());
            expect(text, events.whereType<XmlTextEvent>());
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> forEachEvent',
          () => events,
          listChunker,
          (stream) async {
            final cdata = <XmlCDATAEvent>[];
            final comment = <XmlCommentEvent>[];
            final declaration = <XmlDeclarationEvent>[];
            final doctype = <XmlDoctypeEvent>[];
            final endElement = <XmlEndElementEvent>[];
            final processing = <XmlProcessingEvent>[];
            final startElement = <XmlStartElementEvent>[];
            final text = <XmlTextEvent>[];
            await stream.forEachEvent(
              onCDATA: cdata.add,
              onComment: comment.add,
              onDeclaration: declaration.add,
              onDoctype: doctype.add,
              onEndElement: endElement.add,
              onProcessing: processing.add,
              onStartElement: startElement.add,
              onText: text.add,
            );
            expect(cdata, events.whereType<XmlCDATAEvent>());
            expect(comment, events.whereType<XmlCommentEvent>());
            expect(declaration, events.whereType<XmlDeclarationEvent>());
            expect(doctype, events.whereType<XmlDoctypeEvent>());
            expect(endElement, events.whereType<XmlEndElementEvent>());
            expect(processing, events.whereType<XmlProcessingEvent>());
            expect(startElement, events.whereType<XmlStartElementEvent>());
            expect(text, events.whereType<XmlTextEvent>());
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> tapEachEvent',
          () => events,
          listChunker,
          (stream) async {
            final cdata = <XmlCDATAEvent>[];
            final comment = <XmlCommentEvent>[];
            final declaration = <XmlDeclarationEvent>[];
            final doctype = <XmlDoctypeEvent>[];
            final endElement = <XmlEndElementEvent>[];
            final processing = <XmlProcessingEvent>[];
            final startElement = <XmlStartElementEvent>[];
            final text = <XmlTextEvent>[];
            await stream
                .tapEachEvent(
                  onCDATA: cdata.add,
                  onComment: comment.add,
                  onDeclaration: declaration.add,
                  onDoctype: doctype.add,
                  onEndElement: endElement.add,
                  onProcessing: processing.add,
                  onStartElement: startElement.add,
                  onText: text.add,
                )
                .drain<void>();
            expect(cdata, events.whereType<XmlCDATAEvent>());
            expect(comment, events.whereType<XmlCommentEvent>());
            expect(declaration, events.whereType<XmlDeclarationEvent>());
            expect(doctype, events.whereType<XmlDoctypeEvent>());
            expect(endElement, events.whereType<XmlEndElementEvent>());
            expect(processing, events.whereType<XmlProcessingEvent>());
            expect(startElement, events.whereType<XmlStartElementEvent>());
            expect(text, events.whereType<XmlTextEvent>());
          },
        );
        chunkedTests<List<XmlEvent>>(
          'events -> withParent -> map',
          () => events,
          listChunker,
          (stream) async {
            final stacks = await stream
                .withParentEvents()
                .normalizeEvents()
                .flatten()
                .map((event) {
              final stack = <XmlEvent>[];
              for (XmlEvent? current = event;
                  current != null;
                  current = current.parent) {
                stack.insert(0, current);
              }
              return stack;
            }).toList();
            expect(stacks.map((events) => events.last), events);
          },
        );
      });
    }
  });
  group('errors', () {
    chunkedTests<String>(
      'missing tag closing',
      () => '<hello',
      stringChunker,
      (stream) {
        expect(
            stream.toXmlEvents(withLocation: true),
            emitsThrough(emitsError(isXmlParserException(
              message: '">" expected',
              position: 6,
            ))));
      },
    );
    chunkedTests<String>(
      'missing attribute closing',
      () => '<foo bar="abc',
      stringChunker,
      (stream) {
        expect(
            stream.toXmlEvents(withLocation: true),
            emitsThrough(emitsError(isXmlParserException(
              message: '">" expected',
              position: 8,
            ))));
      },
    );
    chunkedTests<String>(
      'missing comment closing',
      () => '<!-- comment',
      stringChunker,
      (stream) {
        expect(
            stream.toXmlEvents(withLocation: true),
            emitsThrough(emitsError(isXmlParserException(
              message: '"-->" expected',
              position: 4,
            ))));
      },
    );
    group('tags not validated', () {
      chunkedTests<String>(
        'unexpected end tag',
        () => '</foo>',
        stringChunker,
        (stream) {
          expect(
              stream.toXmlEvents(withLocation: true).flatten(),
              emitsInOrder([
                XmlEndElementEvent('foo'),
                emitsDone,
              ]));
        },
      );
      chunkedTests<String>(
        'missing end tag',
        () => '<foo>',
        stringChunker,
        (stream) {
          expect(
              stream.toXmlEvents(withLocation: true).flatten(),
              emitsInOrder([
                XmlStartElementEvent('foo', [], false),
                emitsDone,
              ]));
        },
      );
      chunkedTests<String>(
        'not matching end tag',
        () => '<foo></bar></foo>',
        stringChunker,
        (stream) {
          expect(
              stream.toXmlEvents(withLocation: true).flatten(),
              emitsInOrder([
                XmlStartElementEvent('foo', [], false),
                XmlEndElementEvent('bar'),
                XmlEndElementEvent('foo'),
                emitsDone,
              ]));
        },
      );
    });
    group('tags validated', () {
      chunkedTests<String>(
        'unexpected end tag',
        () => '</foo>',
        stringChunker,
        (stream) {
          expect(
              stream
                  .toXmlEvents(validateNesting: true, withLocation: true)
                  .flatten(),
              emitsThrough(emitsError(
                  isXmlTagException(actualName: 'foo', position: 0))));
        },
      );
      chunkedTests<String>(
        'missing end tag',
        () => '<foo>',
        stringChunker,
        (stream) {
          expect(
              stream
                  .toXmlEvents(validateNesting: true, withLocation: true)
                  .flatten(),
              emitsThrough(emitsError(
                  isXmlTagException(expectedName: 'foo', position: 5))));
        },
      );
      chunkedTests<String>(
        'not matching end tag',
        () => '<foo></bar></foo>',
        stringChunker,
        (stream) {
          expect(
              stream
                  .toXmlEvents(validateNesting: true, withLocation: true)
                  .flatten(),
              emitsThrough(emitsError(isXmlTagException(
                  expectedName: 'foo', actualName: 'bar', position: 5))));
        },
      );
    });
    group('node decoder', () {
      const decoder = XmlNodeDecoder();
      test('mismatch closing tag', () {
        expect(
            () => decoder.convert([
                  XmlStartElementEvent('foo', [], false),
                  XmlEndElementEvent('bar'),
                ]),
            throwsA(isXmlTagException(
              message: 'Expected </foo>, but found </bar>',
              expectedName: 'foo',
              actualName: 'bar',
            )));
      });
      test('unexpected closing tag', () {
        expect(
            () => decoder.convert([XmlEndElementEvent('foo')]),
            throwsA(isXmlTagException(
              message: 'Unexpected </foo>',
              expectedName: isNull,
              actualName: 'foo',
            )));
      });
      test('missing closing tag', () {
        expect(
            () => decoder.convert([XmlStartElementEvent('foo', [], false)]),
            throwsA(isXmlTagException(
              message: 'Missing </foo>',
              expectedName: 'foo',
              actualName: isNull,
            )));
      });
      test('hidden parents', () {
        final grandparent = XmlStartElementEvent('grandparent', [], false);
        final parent = XmlStartElementEvent('parent', [], false)
          ..attachParent(grandparent);
        final child = XmlStartElementEvent('child', [], true)
          ..attachParent(parent);
        final node = decoder.convert([child]).single;
        expect(node.hasParent, isTrue);
        expect(node.parent?.hasParent, isTrue);
        expect(node.parent?.parent?.toXmlString(),
            '<grandparent><parent><child/></parent></grandparent>');
      });
    });
  });
  group('normalizeEvents', () {
    test('empty', () async {
      final input = <XmlEvent>[XmlTextEvent('')];
      final output = await Stream.fromIterable([input])
          .normalizeEvents()
          .flatten()
          .toList();
      const expected = <XmlEvent>[];
      expect(output, expected);
    });
    test('whitespace', () async {
      final input = <XmlEvent>[XmlTextEvent(' \n\t')];
      final actual = await Stream.fromIterable([input])
          .normalizeEvents()
          .flatten()
          .toList();
      final expected = <XmlEvent>[XmlTextEvent(' \n\t')];
      expect(actual, expected);
    });
    test('combine two', () async {
      final input = <XmlEvent>[XmlTextEvent('a'), XmlTextEvent('b')];
      final actual = await Stream.fromIterable([input])
          .normalizeEvents()
          .flatten()
          .toList();
      final expected = <XmlEvent>[XmlTextEvent('ab')];
      expect(actual, expected);
    });
    test('combine many', () async {
      final input = <XmlEvent>[
        XmlTextEvent('a'),
        XmlTextEvent('b'),
        XmlTextEvent('c'),
        XmlTextEvent('d'),
        XmlTextEvent('e'),
      ];
      final actual = await Stream.fromIterable([input])
          .normalizeEvents()
          .flatten()
          .toList();
      final expected = <XmlEvent>[XmlTextEvent('abcde')];
      expect(actual, expected);
    });
    test('chunked up', () async {
      final input = <XmlEvent>[
        XmlTextEvent('a'),
        XmlTextEvent('b'),
        XmlTextEvent('c'),
        XmlStartElementEvent('br', [], true),
        XmlTextEvent('d'),
        XmlTextEvent('e'),
      ];
      final actual = await Stream.fromIterable([input])
          .normalizeEvents()
          .flatten()
          .toList();
      final expected = <XmlEvent>[
        XmlTextEvent('abc'),
        XmlStartElementEvent('br', [], true),
        XmlTextEvent('de'),
      ];
      expect(actual, expected);
    });
  });
  group('withParentEvents', () {
    test('not parented', () async {
      final input = <XmlEvent>[
        XmlCDATAEvent('cdata'),
        XmlCommentEvent('comment'),
        XmlDeclarationEvent([]),
        XmlDoctypeEvent('doctype'),
        XmlProcessingEvent('target', 'text'),
        XmlStartElementEvent('element', [], true),
        XmlTextEvent('text'),
      ];
      final output = await Stream.fromIterable([input])
          .withParentEvents()
          .flatten()
          .toList();
      expect(output, input, reason: 'equality is unaffected');
      for (var i = 0; i < input.length; i++) {
        expect(input[i], same(output[i]), reason: 'root element is identical');
      }
    });
    test('basic parented', () async {
      final input = <XmlEvent>[
        XmlStartElementEvent('element', [], false),
        XmlCDATAEvent('cdata'),
        XmlCommentEvent('comment'),
        XmlDeclarationEvent([]),
        XmlDoctypeEvent('doctype'),
        XmlProcessingEvent('target', 'text'),
        XmlStartElementEvent('element', [], true),
        XmlTextEvent('text'),
        XmlEndElementEvent('element'),
      ];
      final output = await Stream.fromIterable([input])
          .withParentEvents()
          .flatten()
          .toList();
      expect(output, input, reason: 'equality is unaffected');
      for (var i = 1; i < input.length; i++) {
        expect(output[i].parent, same(output[0]));
        expect(output[i].parent, same(input[0]));
      }
    });
    test('deeply parented', () async {
      final input = <XmlEvent>[
        XmlStartElementEvent('first', [], false),
        XmlStartElementEvent('second', [], false),
        XmlStartElementEvent('third', [], false),
        XmlEndElementEvent('third'),
        XmlEndElementEvent('second'),
        XmlEndElementEvent('first'),
      ];
      final output = await Stream.fromIterable([input])
          .withParentEvents()
          .flatten()
          .toList();
      expect(output, input, reason: 'equality is unaffected');
      expect(output[0], same(input[0]), reason: 'root element is identical');
      expect(output[0].parent, isNull);
      expect(output[1].parent, same(output[0]));
      expect(output[2].parent, same(output[1]));
      expect(output[3].parent, same(output[2]));
      expect(output[4].parent, same(output[1]));
      expect(output[5].parent, same(output[0]));
    });
    test('closing tag mismatch', () {
      final input = <List<XmlEvent>>[
        [XmlStartElementEvent('open', [], false)],
        [XmlEndElementEvent('close')],
        [XmlTextEvent('after')],
      ];
      final stream = Stream.fromIterable(input).withParentEvents().flatten();
      expect(
          stream,
          emitsInOrder([
            input[0][0],
            emitsError(isXmlTagException(
                message: 'Expected </open>, but found </close>')),
          ]));
    });
    test('closing tag missing', () {
      final input = <List<XmlEvent>>[
        [XmlStartElementEvent('open', [], false)],
      ];
      final stream = Stream.fromIterable(input).withParentEvents().flatten();
      expect(
          stream,
          emitsInOrder([
            input[0][0],
            emitsError(isXmlTagException(message: 'Missing </open>')),
          ]));
    });
    test('closing tag unexpected', () {
      final input = <List<XmlEvent>>[
        [XmlEndElementEvent('close')],
        [XmlTextEvent('after')],
      ];
      final stream = Stream.fromIterable(input).withParentEvents().flatten();
      expect(
        stream,
        emitsError(isXmlTagException(message: 'Unexpected </close>')),
      );
    });
    test('after normalization', () {
      final input = [
        XmlStartElementEvent('outer', [], false),
        XmlTextEvent('first'),
        XmlTextEvent(' '),
        XmlTextEvent('second'),
        XmlEndElementEvent('outer'),
      ];
      final actual = const XmlWithParentEvents()
          .convert(const XmlNormalizeEvents().convert(input));
      expect(actual, hasLength(3));
      expect(actual[1].parent, same(actual[0]));
      expect(actual[2].parent, same(actual[0]));
    });
    test('before normalization', () {
      final input = [
        XmlStartElementEvent('outer', [], false),
        XmlTextEvent('first'),
        XmlTextEvent(' '),
        XmlTextEvent('second'),
        XmlEndElementEvent('outer'),
      ];
      final actual = const XmlNormalizeEvents()
          .convert(const XmlWithParentEvents().convert(input));
      expect(actual, hasLength(3));
      expect(actual[1].parent, same(actual[0]));
      expect(actual[2].parent, same(actual[0]));
    });
    test('default namespace', () async {
      const url = 'http://www.w3.org/1999/xhtml';
      const input = '<html xmlns="$url"><body lang="en"/></html>';
      final events = await Stream.fromIterable([input])
          .toXmlEvents()
          .withParentEvents()
          .flatten()
          .toList();
      for (final event in events) {
        if (event is XmlStartElementEvent) {
          expect(event.namespaceUri, url);
          event.attributes
              .where((attribute) => attribute.localName != 'xmlns')
              .forEach((attribute) => expect(attribute.namespaceUri, url));
        } else if (event is XmlEndElementEvent) {
          expect(event.namespaceUri, url);
        }
      }
    });
    test('prefix namespace', () async {
      const url = 'http://www.w3.org/1999/xhtml';
      const input = '<xhtml:html xmlns:xhtml="$url">'
          '<xhtml:body xhtml:lang="en"/>'
          '</xhtml:html>';
      final events = await Stream.fromIterable([input])
          .toXmlEvents()
          .withParentEvents()
          .flatten()
          .toList();
      for (final event in events) {
        if (event is XmlStartElementEvent) {
          expect(event.namespaceUri, url);
          event.attributes
              .where((attribute) => attribute.namespacePrefix != 'xmlns')
              .forEach((attribute) => expect(attribute.namespaceUri, url));
        } else if (event is XmlEndElementEvent) {
          expect(event.namespaceUri, url);
        }
      }
    });
  });
}
