import 'dart:convert';

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/widget/prosemirror/pm_node.dart';

/// A 1x1 transparent PNG as a base64 data URI (valid).
const _pngDataUri =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGP4DwQACfsD/fteaysAAAAASUVORK5CYII=';

/// A realistic TipTap `getJSON()` document exercising the whole vocabulary.
Map<String, dynamic> _kitchenSinkDoc() => {
  'type': 'doc',
  'content': [
    {
      'type': 'heading',
      'attrs': {'level': 1, 'textAlign': 'center'},
      'content': [
        {'type': 'text', 'text': 'Title'},
      ],
    },
    {
      'type': 'paragraph',
      'attrs': {'textAlign': 'left'},
      'content': [
        {
          'type': 'text',
          'text': 'bold',
          'marks': [
            {'type': 'bold'},
          ],
        },
        {'type': 'text', 'text': ' and '},
        {
          'type': 'text',
          'text': 'link',
          'marks': [
            {
              'type': 'link',
              'attrs': {'href': 'https://example.org', 'target': '_blank'},
            },
          ],
        },
        {'type': 'hardBreak'},
        {
          'type': 'text',
          'text': 'sized highlighted',
          'marks': [
            {
              'type': 'textStyle',
              'attrs': {'fontSize': '1.5em'},
            },
            {
              'type': 'highlight',
              'attrs': {'color': '#ff0'},
            },
          ],
        },
      ],
    },
    {
      'type': 'bulletList',
      'content': [
        {
          'type': 'listItem',
          'content': [
            {
              'type': 'paragraph',
              'content': [
                {'type': 'text', 'text': 'one'},
              ],
            },
          ],
        },
      ],
    },
    {
      'type': 'orderedList',
      'attrs': {'start': 3},
      'content': [
        {
          'type': 'listItem',
          'content': [
            {
              'type': 'paragraph',
              'content': [
                {'type': 'text', 'text': 'three'},
              ],
            },
          ],
        },
      ],
    },
    {
      'type': 'blockquote',
      'content': [
        {
          'type': 'paragraph',
          'content': [
            {'type': 'text', 'text': 'quoted'},
          ],
        },
      ],
    },
    {
      'type': 'codeBlock',
      'attrs': {'language': 'dart'},
      'content': [
        {'type': 'text', 'text': 'void main() {}'},
      ],
    },
    {'type': 'horizontalRule'},
    {
      'type': 'callout',
      'attrs': {'variant': 'warning'},
      'content': [
        {
          'type': 'paragraph',
          'content': [
            {'type': 'text', 'text': 'careful'},
          ],
        },
      ],
    },
    {
      'type': 'image',
      'attrs': {
        'src': _pngDataUri,
        'alt': 'dot',
        'align': 'center',
        'width': '50%',
        'href': 'https://example.org',
      },
    },
    {
      'type': 'table',
      'content': [
        {
          'type': 'tableRow',
          'content': [
            {
              'type': 'tableHeader',
              'attrs': {'colspan': 2, 'rowspan': 1},
              'content': [
                {
                  'type': 'paragraph',
                  'content': [
                    {'type': 'text', 'text': 'head'},
                  ],
                },
              ],
            },
          ],
        },
        {
          'type': 'tableRow',
          'content': [
            {
              'type': 'tableCell',
              'content': [
                {
                  'type': 'paragraph',
                  'content': [
                    {'type': 'text', 'text': 'a'},
                  ],
                },
              ],
            },
            {
              'type': 'tableCell',
              'content': [
                {
                  'type': 'paragraph',
                  'content': [
                    {'type': 'text', 'text': 'b'},
                  ],
                },
              ],
            },
          ],
        },
      ],
    },
  ],
};

bool _hasUnknown(PmNode node) {
  if (node is PmUnknown) return true;
  return node.children.any(_hasUnknown);
}

void main() {
  test('kitchen-sink doc parses without any PmUnknown', () {
    final doc = PmNode.fromJson(_kitchenSinkDoc());
    // The root `doc` node has no dedicated subclass by design and maps to
    // PmUnknown; assert instead that no *content* node is unknown.
    expect(doc, isA<PmUnknown>());
    expect(doc.children.any(_hasUnknown), isFalse);
  });

  test('every top-level node maps to its typed subclass', () {
    final doc = PmNode.fromJson(_kitchenSinkDoc());
    final types = doc.children.map((n) => n.runtimeType).toList();
    expect(types, [
      PmHeading,
      PmParagraph,
      PmBulletList,
      PmOrderedList,
      PmBlockquote,
      PmCodeBlock,
      PmHorizontalRule,
      PmCallout,
      PmImage,
      PmTable,
    ]);
  });

  test('unknown node type becomes PmUnknown without throwing', () {
    final node = PmNode.fromJson({
      'type': 'youTubeEmbed',
      'attrs': {'videoId': 'abc'},
      'content': [
        {
          'type': 'paragraph',
          'content': [
            {'type': 'text', 'text': 'caption'},
          ],
        },
      ],
    });
    expect(node, isA<PmUnknown>());
    expect((node as PmUnknown).rawType, 'youTubeEmbed');
    expect(node.children.single, isA<PmParagraph>());
  });

  test('heading level and align parse and clamp', () {
    final node =
        PmNode.fromJson({
              'type': 'heading',
              'attrs': {'level': 9, 'textAlign': 'right'},
            })
            as PmHeading;
    expect(node.level, 6);
    expect(node.align, TextAlign.right);
  });

  test('orderedList start defaults to 1 and honours attr', () {
    final withStart =
        PmNode.fromJson({
              'type': 'orderedList',
              'attrs': {'start': 5},
            })
            as PmOrderedList;
    final without = PmNode.fromJson({'type': 'orderedList'}) as PmOrderedList;
    expect(withStart.start, 5);
    expect(without.start, 1);
  });

  test('marks parse with attrs; unknown marks are dropped tolerantly', () {
    final node =
        PmNode.fromJson({
              'type': 'text',
              'text': 'x',
              'marks': [
                {'type': 'bold'},
                {
                  'type': 'link',
                  'attrs': {'href': 'mailto:a@b.de'},
                },
                {'type': 42},
                {
                  'type': 'superscript',
                  'attrs': {'foo': 'bar'},
                },
              ],
            })
            as PmText;
    final markTypes = node.marks.map((m) => m.type).toList();
    expect(markTypes, contains('bold'));
    expect(markTypes, contains('link'));
    expect(markTypes, contains('superscript'));
    expect(markTypes, isNot(contains('')));
    final link = node.marks.firstWhere((m) => m.type == 'link');
    expect(link.attrs['href'], 'mailto:a@b.de');
  });

  test('valid base64 data image is decoded once into bytes', () {
    final image =
        PmNode.fromJson({
              'type': 'image',
              'attrs': {'src': _pngDataUri},
            })
            as PmImage;
    expect(image.bytes, isNotNull);
    expect(image.bytes!.isNotEmpty, isTrue);
  });

  test('broken base64 data image does not crash and yields null bytes', () {
    final image =
        PmNode.fromJson({
              'type': 'image',
              'attrs': {'src': 'data:image/png;base64,@@@not-base64@@@'},
            })
            as PmImage;
    expect(image.bytes, isNull);
    expect(image.src, startsWith('data:image/png'));
  });

  test('network image keeps null bytes', () {
    final image =
        PmNode.fromJson({
              'type': 'image',
              'attrs': {'src': 'https://example.org/a.png'},
            })
            as PmImage;
    expect(image.bytes, isNull);
  });

  test('callout variant parses; invalid falls back to info', () {
    final ok =
        PmNode.fromJson({
              'type': 'callout',
              'attrs': {'variant': 'success'},
            })
            as PmCallout;
    final bad =
        PmNode.fromJson({
              'type': 'callout',
              'attrs': {'variant': 'nope'},
            })
            as PmCallout;
    expect(ok.variant, PmCalloutVariant.success);
    expect(bad.variant, PmCalloutVariant.info);
  });

  test('table cells carry header flag and spans', () {
    final table =
        PmNode.fromJson(
              (_kitchenSinkDoc()['content'] as List).last
                  as Map<String, dynamic>,
            )
            as PmTable;
    final firstRow = table.children.first as PmTableRow;
    final header = firstRow.children.first as PmTableCell;
    expect(header.header, isTrue);
    expect(header.colspan, 2);
  });

  test('parsing survives a JSON string round-trip', () {
    final raw = jsonEncode(_kitchenSinkDoc());
    final decoded = jsonDecode(raw) as Map<String, dynamic>;
    final doc = PmNode.fromJson(decoded);
    expect(doc.children.any(_hasUnknown), isFalse);
  });
}
