// 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.

// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;

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

List<TreeSliverNode<String>> simpleNodeSet = <TreeSliverNode<String>>[
  TreeSliverNode<String>('Root 0'),
  TreeSliverNode<String>(
    'Root 1',
    expanded: true,
    children: <TreeSliverNode<String>>[
      TreeSliverNode<String>('Child 1:0'),
      TreeSliverNode<String>('Child 1:1'),
    ],
  ),
  TreeSliverNode<String>(
    'Root 2',
    children: <TreeSliverNode<String>>[
      TreeSliverNode<String>('Child 2:0'),
      TreeSliverNode<String>('Child 2:1'),
    ],
  ),
  TreeSliverNode<String>('Root 3'),
];

void main() {
  group('TreeSliverNode', () {
    setUp(() {
      // Reset node conditions for each test.
      simpleNodeSet = <TreeSliverNode<String>>[
        TreeSliverNode<String>('Root 0'),
        TreeSliverNode<String>(
          'Root 1',
          expanded: true,
          children: <TreeSliverNode<String>>[
            TreeSliverNode<String>('Child 1:0'),
            TreeSliverNode<String>('Child 1:1'),
          ],
        ),
        TreeSliverNode<String>(
          'Root 2',
          children: <TreeSliverNode<String>>[
            TreeSliverNode<String>('Child 2:0'),
            TreeSliverNode<String>('Child 2:1'),
          ],
        ),
        TreeSliverNode<String>('Root 3'),
      ];
    });

    test('getters, toString', () {
      final List<TreeSliverNode<String>> children = <TreeSliverNode<String>>[
        TreeSliverNode<String>('child'),
      ];
      final TreeSliverNode<String> node = TreeSliverNode<String>(
        'parent',
        children: children,
        expanded: true,
      );
      expect(node.content, 'parent');
      expect(node.children, children);
      expect(node.isExpanded, isTrue);
      expect(node.children.first.content, 'child');
      expect(node.children.first.children.isEmpty, isTrue);
      expect(node.children.first.isExpanded, isFalse);
      // Set by TreeSliver when built for tree integrity
      expect(node.depth, isNull);
      expect(node.parent, isNull);
      expect(node.children.first.depth, isNull);
      expect(node.children.first.parent, isNull);

      expect(node.toString(), 'TreeSliverNode: parent, depth: null, parent, expanded: true');
      expect(node.children.first.toString(), 'TreeSliverNode: child, depth: null, leaf');
    });

    testWidgets('TreeSliverNode sets ups parent and depth properties', (WidgetTester tester) async {
      final List<TreeSliverNode<String>> children = <TreeSliverNode<String>>[
        TreeSliverNode<String>('child'),
      ];
      final TreeSliverNode<String> node = TreeSliverNode<String>(
        'parent',
        children: children,
        expanded: true,
      );
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[
              TreeSliver<String>(tree: <TreeSliverNode<String>>[node]),
            ],
          ),
        ),
      );
      expect(node.content, 'parent');
      expect(node.children, children);
      expect(node.isExpanded, isTrue);
      expect(node.children.first.content, 'child');
      expect(node.children.first.children.isEmpty, isTrue);
      expect(node.children.first.isExpanded, isFalse);
      // Set by TreeSliver when built for tree integrity
      expect(node.depth, 0);
      expect(node.parent, isNull);
      expect(node.children.first.depth, 1);
      expect(node.children.first.parent, node);

      expect(node.toString(), 'TreeSliverNode: parent, depth: root, parent, expanded: true');
      expect(node.children.first.toString(), 'TreeSliverNode: child, depth: 1, leaf');
    });
  });

  group('TreeController', () {
    setUp(() {
      // Reset node conditions for each test.
      simpleNodeSet = <TreeSliverNode<String>>[
        TreeSliverNode<String>('Root 0'),
        TreeSliverNode<String>(
          'Root 1',
          expanded: true,
          children: <TreeSliverNode<String>>[
            TreeSliverNode<String>('Child 1:0'),
            TreeSliverNode<String>('Child 1:1'),
          ],
        ),
        TreeSliverNode<String>(
          'Root 2',
          children: <TreeSliverNode<String>>[
            TreeSliverNode<String>('Child 2:0'),
            TreeSliverNode<String>('Child 2:1'),
          ],
        ),
        TreeSliverNode<String>('Root 3'),
      ];
    });

    testWidgets('Can set controller on TreeSliver', (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      TreeSliverController? returnedController;
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[
              TreeSliver<String>(
                tree: simpleNodeSet,
                controller: controller,
                treeNodeBuilder: (
                  BuildContext context,
                  TreeSliverNode<Object?> node,
                  AnimationStyle toggleAnimationStyle,
                ) {
                  returnedController ??= TreeSliverController.of(context);
                  return TreeSliver.defaultTreeNodeBuilder(context, node, toggleAnimationStyle);
                },
              ),
            ],
          ),
        ),
      );
      expect(controller, returnedController);
    });

    testWidgets('Can get default controller on TreeSliver', (WidgetTester tester) async {
      TreeSliverController? returnedController;
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[
              TreeSliver<String>(
                tree: simpleNodeSet,
                treeNodeBuilder: (
                  BuildContext context,
                  TreeSliverNode<Object?> node,
                  AnimationStyle toggleAnimationStyle,
                ) {
                  returnedController ??= TreeSliverController.maybeOf(context);
                  return TreeSliver.defaultTreeNodeBuilder(context, node, toggleAnimationStyle);
                },
              ),
            ],
          ),
        ),
      );
      expect(returnedController, isNotNull);
    });

    testWidgets('Can get node for TreeSliverNode.content', (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
          ),
        ),
      );

      expect(controller.getNodeFor('Root 0'), simpleNodeSet[0]);
    });

    testWidgets('Can get isExpanded for a node', (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
          ),
        ),
      );
      expect(controller.isExpanded(simpleNodeSet[0]), isFalse);
      expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
    });

    testWidgets('Can get isActive for a node', (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
          ),
        ),
      );
      expect(controller.isActive(simpleNodeSet[0]), isTrue);
      expect(controller.isActive(simpleNodeSet[1]), isTrue);
      // The parent 'Root 2' is not expanded, so its children are not active.
      expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
      expect(controller.isActive(simpleNodeSet[2].children[0]), isFalse);
    });

    testWidgets('Can toggleNode, to collapse or expand', (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
          ),
        ),
      );

      // The parent 'Root 2' is not expanded, so its children are not active.
      expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
      expect(controller.isActive(simpleNodeSet[2].children[0]), isFalse);
      // Toggle 'Root 2' to expand it
      controller.toggleNode(simpleNodeSet[2]);
      expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
      expect(controller.isActive(simpleNodeSet[2].children[0]), isTrue);

      // The parent 'Root 1' is expanded, so its children are active.
      expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
      expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue);
      // Collapse 'Root 1'
      controller.toggleNode(simpleNodeSet[1]);
      expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
      expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue);
      // Nodes are not removed from the active list until the collapse animation
      // completes. The parent expansion state also updates.
      await tester.pumpAndSettle();
      expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
      expect(controller.isActive(simpleNodeSet[1].children[0]), isFalse);
    });

    testWidgets('Can expandNode, then collapseAll', (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
          ),
        ),
      );

      // The parent 'Root 2' is not expanded, so its children are not active.
      expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
      expect(controller.isActive(simpleNodeSet[2].children[0]), isFalse);
      // Expand 'Root 2'
      controller.expandNode(simpleNodeSet[2]);
      expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
      expect(controller.isActive(simpleNodeSet[2].children[0]), isTrue);

      // Both parents from our simple node set are expanded.
      // 'Root 1'
      expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
      // 'Root 2'
      expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
      // Collapse both.
      controller.collapseAll();
      await tester.pumpAndSettle();
      // Both parents from our simple node set have collapsed.
      // 'Root 1'
      expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
      // 'Root 2'
      expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
    });

    testWidgets('Can collapseNode, then expandAll', (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
          ),
        ),
      );

      // The parent 'Root 1' is expanded, so its children are active.
      expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
      expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue);
      // Collapse 'Root 1'
      controller.collapseNode(simpleNodeSet[1]);
      expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
      expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue);
      // Nodes are not removed from the active list until the collapse animation
      // completes.
      await tester.pumpAndSettle();
      expect(controller.isActive(simpleNodeSet[1].children[0]), isFalse);

      // Both parents from our simple node set are collapsed.
      // 'Root 1'
      expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
      // 'Root 2'
      expect(controller.isExpanded(simpleNodeSet[2]), isFalse);
      // Expand both.
      controller.expandAll();
      // Both parents from our simple node set are expanded.
      // 'Root 1'
      expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
      // 'Root 2'
      expect(controller.isExpanded(simpleNodeSet[2]), isTrue);
    });
  });

  test('TreeSliverIndentationType values are properly reflected', () {
    double value = TreeSliverIndentationType.standard.value;
    expect(value, 10.0);

    value = TreeSliverIndentationType.none.value;
    expect(value, 0.0);

    value = TreeSliverIndentationType.custom(50.0).value;
    expect(value, 50.0);
  });

  testWidgets('.toggleNodeWith, onNodeToggle', (WidgetTester tester) async {
    simpleNodeSet = <TreeSliverNode<String>>[
      TreeSliverNode<String>('Root 0'),
      TreeSliverNode<String>(
        'Root 1',
        expanded: true,
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 1:0'),
          TreeSliverNode<String>('Child 1:1'),
        ],
      ),
      TreeSliverNode<String>(
        'Root 2',
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 2:0'),
          TreeSliverNode<String>('Child 2:1'),
        ],
      ),
      TreeSliverNode<String>('Root 3'),
    ];

    final TreeSliverController controller = TreeSliverController();
    // The default node builder wraps the leading icon with toggleNodeWith.
    bool toggled = false;
    TreeSliverNode<String>? toggledNode;
    await tester.pumpWidget(
      MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            TreeSliver<String>(
              tree: simpleNodeSet,
              controller: controller,
              onNodeToggle: (TreeSliverNode<Object?> node) {
                toggled = true;
                toggledNode = node as TreeSliverNode<String>;
              },
            ),
          ],
        ),
      ),
    );
    expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
    await tester.tap(find.byType(Icon).first);
    await tester.pump();
    expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
    expect(toggled, isTrue);
    expect(toggledNode, simpleNodeSet[1]);
    await tester.pumpAndSettle();
    expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
    toggled = false;
    toggledNode = null;
    // Use toggleNodeWith to make the whole row trigger the node state.
    await tester.pumpWidget(
      MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            TreeSliver<String>(
              tree: simpleNodeSet,
              controller: controller,
              onNodeToggle: (TreeSliverNode<Object?> node) {
                toggled = true;
                toggledNode = node as TreeSliverNode<String>;
              },
              treeNodeBuilder: (
                BuildContext context,
                TreeSliverNode<Object?> node,
                AnimationStyle toggleAnimationStyle,
              ) {
                final Duration animationDuration =
                    toggleAnimationStyle.duration ?? TreeSliver.defaultAnimationDuration;
                final Curve animationCurve =
                    toggleAnimationStyle.curve ?? TreeSliver.defaultAnimationCurve;
                // This makes the whole row trigger toggling.
                return TreeSliver.wrapChildToToggleNode(
                  node: node,
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Row(
                      children: <Widget>[
                        // Icon for parent nodes
                        SizedBox.square(
                          dimension: 30.0,
                          child:
                              node.children.isNotEmpty
                                  ? AnimatedRotation(
                                    turns: node.isExpanded ? 0.25 : 0.0,
                                    duration: animationDuration,
                                    curve: animationCurve,
                                    child: const Icon(IconData(0x25BA), size: 14),
                                  )
                                  : null,
                        ),
                        // Spacer
                        const SizedBox(width: 8.0),
                        // Content
                        Text(node.content.toString()),
                      ],
                    ),
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
    // Still collapsed from earlier
    expect(controller.isExpanded(simpleNodeSet[1]), isFalse);
    // Tapping on the text instead of the Icon.
    await tester.tap(find.text('Root 1'));
    await tester.pump();
    expect(controller.isExpanded(simpleNodeSet[1]), isTrue);
    expect(toggled, isTrue);
    expect(toggledNode, simpleNodeSet[1]);
  });

  testWidgets('AnimationStyle is piped through to node builder', (WidgetTester tester) async {
    simpleNodeSet = <TreeSliverNode<String>>[
      TreeSliverNode<String>('Root 0'),
      TreeSliverNode<String>(
        'Root 1',
        expanded: true,
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 1:0'),
          TreeSliverNode<String>('Child 1:1'),
        ],
      ),
      TreeSliverNode<String>(
        'Root 2',
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 2:0'),
          TreeSliverNode<String>('Child 2:1'),
        ],
      ),
      TreeSliverNode<String>('Root 3'),
    ];

    AnimationStyle? style;
    await tester.pumpWidget(
      MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            TreeSliver<String>(
              tree: simpleNodeSet,
              treeNodeBuilder: (
                BuildContext context,
                TreeSliverNode<Object?> node,
                AnimationStyle toggleAnimationStyle,
              ) {
                style ??= toggleAnimationStyle;
                return Text(node.content.toString());
              },
            ),
          ],
        ),
      ),
    );
    // Default
    expect(style, TreeSliver.defaultToggleAnimationStyle);

    await tester.pumpWidget(
      MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            TreeSliver<String>(
              tree: simpleNodeSet,
              toggleAnimationStyle: AnimationStyle.noAnimation,
              treeNodeBuilder: (
                BuildContext context,
                TreeSliverNode<Object?> node,
                AnimationStyle toggleAnimationStyle,
              ) {
                style = toggleAnimationStyle;
                return Text(node.content.toString());
              },
            ),
          ],
        ),
      ),
    );
    expect(style, isNotNull);
    expect(style!.curve, isNull);
    expect(style!.duration, Duration.zero);
    style = null;

    await tester.pumpWidget(
      MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            TreeSliver<String>(
              tree: simpleNodeSet,
              toggleAnimationStyle: const AnimationStyle(
                curve: Curves.easeIn,
                duration: Duration(milliseconds: 200),
              ),
              treeNodeBuilder: (
                BuildContext context,
                TreeSliverNode<Object?> node,
                AnimationStyle toggleAnimationStyle,
              ) {
                style ??= toggleAnimationStyle;
                return Text(node.content.toString());
              },
            ),
          ],
        ),
      ),
    );
    expect(style, isNotNull);
    expect(style!.curve, Curves.easeIn);
    expect(style!.duration, const Duration(milliseconds: 200));
  });

  testWidgets('Adding more root TreeViewNodes are reflected in the tree', (
    WidgetTester tester,
  ) async {
    simpleNodeSet = <TreeSliverNode<String>>[
      TreeSliverNode<String>('Root 0'),
      TreeSliverNode<String>(
        'Root 1',
        expanded: true,
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 1:0'),
          TreeSliverNode<String>('Child 1:1'),
        ],
      ),
      TreeSliverNode<String>(
        'Root 2',
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 2:0'),
          TreeSliverNode<String>('Child 2:1'),
        ],
      ),
      TreeSliverNode<String>('Root 3'),
    ];
    final TreeSliverController controller = TreeSliverController();
    await tester.pumpWidget(
      MaterialApp(
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Scaffold(
              body: CustomScrollView(
                slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: () {
                  setState(() {
                    simpleNodeSet.add(TreeSliverNode<String>('Added root'));
                  });
                },
              ),
            );
          },
        ),
      ),
    );
    await tester.pump();

    expect(find.text('Root 0'), findsOneWidget);
    expect(find.text('Root 1'), findsOneWidget);
    expect(find.text('Child 1:0'), findsOneWidget);
    expect(find.text('Child 1:1'), findsOneWidget);
    expect(find.text('Root 2'), findsOneWidget);
    expect(find.text('Child 2:0'), findsNothing);
    expect(find.text('Child 2:1'), findsNothing);
    expect(find.text('Root 3'), findsOneWidget);
    expect(find.text('Added root'), findsNothing);

    await tester.tap(find.byType(FloatingActionButton));
    await tester.pump();

    expect(find.text('Root 0'), findsOneWidget);
    expect(find.text('Root 1'), findsOneWidget);
    expect(find.text('Child 1:0'), findsOneWidget);
    expect(find.text('Child 1:1'), findsOneWidget);
    expect(find.text('Root 2'), findsOneWidget);
    expect(find.text('Child 2:0'), findsNothing);
    expect(find.text('Child 2:1'), findsNothing);
    expect(find.text('Root 3'), findsOneWidget);
    // Node was added
    expect(find.text('Added root'), findsOneWidget);
  });

  testWidgets('Adding more TreeViewNodes below the root are reflected in the tree', (
    WidgetTester tester,
  ) async {
    simpleNodeSet = <TreeSliverNode<String>>[
      TreeSliverNode<String>('Root 0'),
      TreeSliverNode<String>(
        'Root 1',
        expanded: true,
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 1:0'),
          TreeSliverNode<String>('Child 1:1'),
        ],
      ),
      TreeSliverNode<String>(
        'Root 2',
        children: <TreeSliverNode<String>>[
          TreeSliverNode<String>('Child 2:0'),
          TreeSliverNode<String>('Child 2:1'),
        ],
      ),
      TreeSliverNode<String>('Root 3'),
    ];
    final TreeSliverController controller = TreeSliverController();
    await tester.pumpWidget(
      MaterialApp(
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Scaffold(
              body: CustomScrollView(
                slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: () {
                  setState(() {
                    simpleNodeSet[1].children.add(TreeSliverNode<String>('Added child'));
                  });
                },
              ),
            );
          },
        ),
      ),
    );
    await tester.pump();
    expect(find.text('Root 0'), findsOneWidget);
    expect(find.text('Root 1'), findsOneWidget);
    expect(find.text('Child 1:0'), findsOneWidget);
    expect(find.text('Child 1:1'), findsOneWidget);
    expect(find.text('Added child'), findsNothing);
    expect(find.text('Root 2'), findsOneWidget);
    expect(find.text('Child 2:0'), findsNothing);
    expect(find.text('Child 2:1'), findsNothing);
    expect(find.text('Root 3'), findsOneWidget);
    await tester.tap(find.byType(FloatingActionButton));
    await tester.pump();
    expect(find.text('Root 0'), findsOneWidget);
    expect(find.text('Root 1'), findsOneWidget);
    expect(find.text('Child 1:0'), findsOneWidget);
    expect(find.text('Child 1:1'), findsOneWidget);
    // Child node was added
    expect(find.text('Added child'), findsOneWidget);
    expect(find.text('Root 2'), findsOneWidget);
    expect(find.text('Child 2:0'), findsNothing);
    expect(find.text('Child 2:1'), findsNothing);
    expect(find.text('Root 3'), findsOneWidget);
  });

  testWidgets(
    'TreeSliverNode should close all children when collapsed when animation is disabled',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/153889
      final TreeSliverController controller = TreeSliverController();
      final List<TreeSliverNode<String>> tree = <TreeSliverNode<String>>[
        TreeSliverNode<String>('First'),
        TreeSliverNode<String>(
          'Second',
          children: <TreeSliverNode<String>>[
            TreeSliverNode<String>(
              'alpha',
              children: <TreeSliverNode<String>>[
                TreeSliverNode<String>('uno'),
                TreeSliverNode<String>('dos'),
                TreeSliverNode<String>('tres'),
              ],
            ),
            TreeSliverNode<String>('beta'),
            TreeSliverNode<String>('kappa'),
          ],
        ),
        TreeSliverNode<String>(
          'Third',
          expanded: true,
          children: <TreeSliverNode<String>>[
            TreeSliverNode<String>('gamma'),
            TreeSliverNode<String>('delta'),
            TreeSliverNode<String>('epsilon'),
          ],
        ),
        TreeSliverNode<String>('Fourth'),
      ];

      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[
              TreeSliver<String>(
                tree: tree,
                controller: controller,
                toggleAnimationStyle: AnimationStyle.noAnimation,
                treeNodeBuilder: (
                  BuildContext context,
                  TreeSliverNode<Object?> node,
                  AnimationStyle animationStyle,
                ) {
                  final Widget child = GestureDetector(
                    behavior: HitTestBehavior.translucent,
                    onTap: () => controller.toggleNode(node),
                    child: TreeSliver.defaultTreeNodeBuilder(context, node, animationStyle),
                  );

                  return child;
                },
              ),
            ],
          ),
        ),
      );

      expect(find.text('First'), findsOneWidget);
      expect(find.text('Second'), findsOneWidget);
      expect(find.text('Third'), findsOneWidget);
      expect(find.text('Fourth'), findsOneWidget);
      expect(find.text('alpha'), findsNothing);
      expect(find.text('beta'), findsNothing);
      expect(find.text('kappa'), findsNothing);
      expect(find.text('gamma'), findsOneWidget);
      expect(find.text('delta'), findsOneWidget);
      expect(find.text('epsilon'), findsOneWidget);
      expect(find.text('uno'), findsNothing);
      expect(find.text('dos'), findsNothing);
      expect(find.text('tres'), findsNothing);

      await tester.tap(find.text('Second'));
      await tester.pumpAndSettle();

      expect(find.text('alpha'), findsOneWidget);

      await tester.tap(find.text('alpha'));
      await tester.pumpAndSettle();

      expect(find.text('uno'), findsOneWidget);
      expect(find.text('dos'), findsOneWidget);
      expect(find.text('tres'), findsOneWidget);

      await tester.tap(find.text('alpha'));
      await tester.pumpAndSettle();

      expect(find.text('uno'), findsNothing);
      expect(find.text('dos'), findsNothing);
      expect(find.text('tres'), findsNothing);
    },
  );

  testWidgets(
    'TreeSliverNode should close all children when collapsed when animation is completed',
    (WidgetTester tester) async {
      final TreeSliverController controller = TreeSliverController();
      final List<TreeSliverNode<String>> tree = <TreeSliverNode<String>>[
        TreeSliverNode<String>(
          'First',
          expanded: true,
          children: <TreeSliverNode<String>>[
            TreeSliverNode<String>(
              'alpha',
              expanded: true,
              children: <TreeSliverNode<String>>[
                TreeSliverNode<String>('uno'),
                TreeSliverNode<String>('dos'),
                TreeSliverNode<String>('tres'),
              ],
            ),
            TreeSliverNode<String>('beta'),
            TreeSliverNode<String>('kappa'),
          ],
        ),
      ];

      Widget buildTreeSliver(TreeSliverController controller) {
        return MaterialApp(
          home: Scaffold(
            body: CustomScrollView(
              shrinkWrap: true,
              slivers: <Widget>[
                TreeSliver<String>(
                  tree: tree,
                  controller: controller,
                  toggleAnimationStyle: const AnimationStyle(
                    curve: Curves.easeInOut,
                    duration: Duration(milliseconds: 200),
                  ),
                  treeNodeBuilder: (
                    BuildContext context,
                    TreeSliverNode<Object?> node,
                    AnimationStyle animationStyle,
                  ) {
                    final Widget child = GestureDetector(
                      key: ValueKey<String>(node.content! as String),
                      behavior: HitTestBehavior.translucent,
                      onTap: () => controller.toggleNode(node),
                      child: TreeSliver.defaultTreeNodeBuilder(context, node, animationStyle),
                    );
                    return child;
                  },
                ),
              ],
            ),
          ),
        );
      }

      await tester.pumpWidget(buildTreeSliver(controller));

      expect(find.text('alpha'), findsOneWidget);
      expect(find.text('uno'), findsOneWidget);
      expect(find.text('dos'), findsOneWidget);
      expect(find.text('tres'), findsOneWidget);

      // Using runAsync to handle collapse and animations properly.
      await tester.runAsync(() async {
        await tester.tap(find.text('alpha'));
        await tester.pumpAndSettle();

        expect(find.text('uno'), findsNothing);
        expect(find.text('dos'), findsNothing);
        expect(find.text('tres'), findsNothing);
      });
    },
  );

  testWidgets('TreeSliver and PinnedHeaderSliver can render correctly when used together.', (
    WidgetTester tester,
  ) async {
    const ValueKey<String> key = ValueKey<String>('sliver_tree_pined_header');
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Align(
          alignment: Alignment.topLeft,
          child: RepaintBoundary(
            key: key,
            child: SizedBox(
              height: 20,
              width: 20,
              child: CustomScrollView(
                slivers: <Widget>[
                  const PinnedHeaderSliver(child: SizedBox(height: 10)),
                  TreeSliver<Object>(
                    tree: <TreeSliverNode<Object>>[TreeSliverNode<Object>(Object())],
                    treeRowExtentBuilder: (_, _) => 10,
                    treeNodeBuilder: (
                      BuildContext context,
                      TreeSliverNode<Object?> node,
                      AnimationStyle animationStyle,
                    ) {
                      return const ColoredBox(color: Colors.red);
                    },
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
    await expectLater(find.byKey(key), matchesGoldenFile('sliver_tree.pined_header.0.png'));
    expect(tester.getTopLeft(find.byType(ColoredBox)), const Offset(0, 10));
  });
}
