// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Element diagnostics json includes widgetRuntimeType', () async {
    final Element element = _TestElement();

    final Map<String, Object?> json = element.toDiagnosticsNode().toJsonMap(const DiagnosticsSerializationDelegate());
    expect(json['widgetRuntimeType'], 'Placeholder');
    expect(json['stateful'], isFalse);
  });

  test('StatefulElement diagnostics are stateful', () {
    final Element element = StatefulElement(const Tooltip(message: 'foo'));

    final Map<String, Object?> json = element.toDiagnosticsNode().toJsonMap(const DiagnosticsSerializationDelegate());
    expect(json['widgetRuntimeType'], 'Tooltip');
    expect(json['stateful'], isTrue);
  });

  group('Serialization', () {
    final TestTree testTree = TestTree(
      properties: <DiagnosticsNode>[
        StringProperty('stringProperty1', 'value1', quoted: false),
        DoubleProperty('doubleProperty1', 42.5),
        DoubleProperty('roundedProperty', 1.0 / 3.0),
        StringProperty('DO_NOT_SHOW', 'DO_NOT_SHOW', level: DiagnosticLevel.hidden, quoted: false),
        DiagnosticsProperty<Object>('DO_NOT_SHOW_NULL', null, defaultValue: null),
        DiagnosticsProperty<Object>('nullProperty', null),
        StringProperty('node_type', '<root node>', showName: false, quoted: false),
      ],
      children: <TestTree>[
        TestTree(name: 'node A'),
        TestTree(
          name: 'node B',
          properties: <DiagnosticsNode>[
            StringProperty('p1', 'v1', quoted: false),
            StringProperty('p2', 'v2', quoted: false),
          ],
          children: <TestTree>[
            TestTree(name: 'node B1'),
            TestTree(
              name: 'node B2',
              properties: <DiagnosticsNode>[StringProperty('property1', 'value1', quoted: false)],
            ),
            TestTree(
              name: 'node B3',
              properties: <DiagnosticsNode>[
                StringProperty('node_type', '<leaf node>', showName: false, quoted: false),
                IntProperty('foo', 42),
              ],
            ),
          ],
        ),
        TestTree(
          name: 'node C',
          properties: <DiagnosticsNode>[
            StringProperty('foo', 'multi\nline\nvalue!', quoted: false),
          ],
        ),
      ],
    );

    test('default', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(const DiagnosticsSerializationDelegate());
      expect(result.containsKey('properties'), isFalse);
      expect(result.containsKey('children'), isFalse);
    });

    test('subtreeDepth 1', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(const DiagnosticsSerializationDelegate(subtreeDepth: 1));
      expect(result.containsKey('properties'), isFalse);
      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children[0].containsKey('children'), isFalse);
      expect(children[1].containsKey('children'), isFalse);
      expect(children[2].containsKey('children'), isFalse);
    });

    test('subtreeDepth 5', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(const DiagnosticsSerializationDelegate(subtreeDepth: 5));
      expect(result.containsKey('properties'), isFalse);
      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children[0]['children'], hasLength(0));
      expect(children[1]['children'], hasLength(3));
      expect(children[2]['children'], hasLength(0));
    });

    test('includeProperties', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(const DiagnosticsSerializationDelegate(includeProperties: true));
      expect(result.containsKey('children'), isFalse);
      expect(result['properties'], hasLength(7));
    });

    test('includeProperties with subtreedepth 1', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(const DiagnosticsSerializationDelegate(
        includeProperties: true,
        subtreeDepth: 1,
      ));
      expect(result['properties'], hasLength(7));
      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children, hasLength(3));
      expect(children[0]['properties'], hasLength(0));
      expect(children[1]['properties'], hasLength(2));
      expect(children[2]['properties'], hasLength(1));
    });

    test('additionalNodeProperties', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(const TestDiagnosticsSerializationDelegate(
        includeProperties: true,
        subtreeDepth: 1,
        additionalNodePropertiesMap: <String, Object>{
          'foo': true,
        },
      ));
      expect(result['foo'], isTrue);
      final List<Map<String, Object?>> properties = result['properties']! as List<Map<String, Object?>>;
      expect(properties, hasLength(7));
      expect(properties.every((Map<String, Object?> property) => property['foo'] == true), isTrue);

      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children, hasLength(3));
      expect(children.every((Map<String, Object?> child) => child['foo'] == true), isTrue);
    });

    test('filterProperties - sublist', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(TestDiagnosticsSerializationDelegate(
          includeProperties: true,
          propertyFilter: (List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
            return nodes.whereType<StringProperty>().toList();
          },
      ));
      final List<Map<String, Object?>> properties = result['properties']! as List<Map<String, Object?>>;
      expect(properties, hasLength(3));
      expect(properties.every((Map<String, Object?> property) => property['type'] == 'StringProperty'), isTrue);
    });

    test('filterProperties - replace', () {
      bool replaced = false;
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(TestDiagnosticsSerializationDelegate(
          includeProperties: true,
          propertyFilter: (List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
            if (replaced) {
              return nodes;
            }
            replaced = true;
            return <DiagnosticsNode>[
              StringProperty('foo', 'bar'),
            ];
          },
      ));
      final List<Map<String, Object?>> properties = result['properties']! as List<Map<String, Object?>>;
      expect(properties, hasLength(1));
      expect(properties.single['name'], 'foo');
    });

    test('filterChildren - sublist', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(TestDiagnosticsSerializationDelegate(
          subtreeDepth: 1,
          childFilter: (List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
            return nodes.where((DiagnosticsNode node) => node.getProperties().isEmpty).toList();
          },
      ));
      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children, hasLength(1));
    });

    test('filterChildren - replace', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(TestDiagnosticsSerializationDelegate(
          subtreeDepth: 1,
          childFilter: (List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
            return nodes.expand((DiagnosticsNode node) => node.getChildren()).toList();
          },
      ));
      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children, hasLength(3));
      expect(children.first['name'], 'child node B1');
    });

    test('nodeTruncator', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(TestDiagnosticsSerializationDelegate(
          subtreeDepth: 5,
          includeProperties: true,
          nodeTruncator: (List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
            return nodes.take(2).toList();
          },
      ));
      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children, hasLength(3));
      expect(children.last['truncated'], isTrue);

      final List<Map<String, Object?>> properties = result['properties']! as List<Map<String, Object?>>;
      expect(properties, hasLength(3));
      expect(properties.last['truncated'], isTrue);
    });

    test('delegateForAddingNodes', () {
      final Map<String, Object?> result = testTree.toDiagnosticsNode().toJsonMap(TestDiagnosticsSerializationDelegate(
          subtreeDepth: 5,
          includeProperties: true,
          nodeDelegator: (DiagnosticsNode node, DiagnosticsSerializationDelegate delegate) {
            return delegate.copyWith(includeProperties: false);
          },
      ));
      final List<Map<String, Object?>> properties = result['properties']! as List<Map<String, Object?>>;
      expect(properties, hasLength(7));
      expect(properties.every((Map<String, Object?> property) => !property.containsKey('properties')), isTrue);

      final List<Map<String, Object?>> children = result['children']! as List<Map<String, Object?>>;
      expect(children, hasLength(3));
      expect(children.every((Map<String, Object?> child) => !child.containsKey('properties')), isTrue);
    });
  });
}

class _TestElement extends Element {
  _TestElement() : super(const Placeholder());

  @override
  bool get debugDoingBuild => throw UnimplementedError();
}

class TestTree extends Object with DiagnosticableTreeMixin {
  TestTree({
    this.name = '',
    this.style,
    this.children = const <TestTree>[],
    this.properties = const <DiagnosticsNode>[],
  });

  final String name;
  final List<TestTree> children;
  final List<DiagnosticsNode> properties;
  final DiagnosticsTreeStyle? style;

  @override
  List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[
    for (final TestTree child in children)
      child.toDiagnosticsNode(
        name: 'child ${child.name}',
        style: child.style,
      ),
  ];

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    if (style != null) {
      properties.defaultDiagnosticsTreeStyle = style!;
    }

    this.properties.forEach(properties.add);
  }
}

typedef NodeDelegator = DiagnosticsSerializationDelegate Function(DiagnosticsNode node, TestDiagnosticsSerializationDelegate delegate);
typedef NodeTruncator = List<DiagnosticsNode> Function(List<DiagnosticsNode> nodes, DiagnosticsNode? owner);
typedef NodeFilter = List<DiagnosticsNode> Function(List<DiagnosticsNode> nodes, DiagnosticsNode owner);

class TestDiagnosticsSerializationDelegate implements DiagnosticsSerializationDelegate {
  const TestDiagnosticsSerializationDelegate({
    this.includeProperties = false,
    this.subtreeDepth = 0,
    this.additionalNodePropertiesMap = const <String, Object>{},
    this.childFilter,
    this.propertyFilter,
    this.nodeTruncator,
    this.nodeDelegator,
  });

  final Map<String, Object> additionalNodePropertiesMap;
  final NodeFilter? childFilter;
  final NodeFilter? propertyFilter;
  final NodeTruncator? nodeTruncator;
  final NodeDelegator? nodeDelegator;

  @override
  Map<String, Object> additionalNodeProperties(DiagnosticsNode node) {
    return additionalNodePropertiesMap;
  }

  @override
  DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node) {
    if (nodeDelegator != null) {
      return nodeDelegator!(node, this);
    }
    return subtreeDepth > 0 ? copyWith(subtreeDepth: subtreeDepth - 1) : this;
  }

  @override
  bool get expandPropertyValues => false;

  @override
  List<DiagnosticsNode> filterChildren(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
    return childFilter?.call(nodes, owner) ?? nodes;
  }

  @override
  List<DiagnosticsNode> filterProperties(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
    return propertyFilter?.call(nodes, owner) ?? nodes;
  }

  @override
  final bool includeProperties;

  @override
  final int subtreeDepth;

  @override
  List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
    return nodeTruncator?.call(nodes, owner) ?? nodes;
  }

  @override
  DiagnosticsSerializationDelegate copyWith({int? subtreeDepth, bool? includeProperties}) {
    return TestDiagnosticsSerializationDelegate(
      includeProperties: includeProperties ?? this.includeProperties,
      subtreeDepth: subtreeDepth ?? this.subtreeDepth,
      additionalNodePropertiesMap: additionalNodePropertiesMap,
      childFilter: childFilter,
      propertyFilter: propertyFilter,
      nodeTruncator: nodeTruncator,
      nodeDelegator: nodeDelegator,
    );
  }
}