// 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 'dart:math';
import 'dart:ui';

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

import 'semantics_tester.dart';

void main() {
  setUp(() {
    debugResetSemanticsIdCounter();
  });

  testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
    SemanticsTester? semantics = SemanticsTester(tester);

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          label: 'test1',
          textDirection: TextDirection.ltr,
        ),
      ],
    );

    await tester.pumpWidget(
      Semantics(
        label: 'test1',
        textDirection: TextDirection.ltr,
        child: Container(),
      ),
    );

    expect(semantics, hasSemantics(
      expectedSemantics,
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));

    semantics.dispose();
    semantics = null;

    expect(tester.binding.hasScheduledFrame, isFalse);
    semantics = SemanticsTester(tester);
    expect(tester.binding.hasScheduledFrame, isTrue);
    await tester.pump();

    expect(semantics, hasSemantics(
      expectedSemantics,
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));
    semantics.dispose();
  }, semanticsEnabled: false);

  testWidgets('Detach and reattach assert', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final GlobalKey key = GlobalKey();

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Semantics(
        label: 'test1',
        child: Semantics(
          key: key,
          container: true,
          label: 'test2a',
          child: Container(),
        ),
      ),
    ));

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics.rootChild(
            label: 'test1',
            children: <TestSemantics>[
              TestSemantics(
                label: 'test2a',
              ),
            ],
          ),
        ],
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
    ));

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Semantics(
        label: 'test1',
        child: Semantics(
          container: true,
          label: 'middle',
          child: Semantics(
            key: key,
            container: true,
            label: 'test2b',
            child: Container(),
          ),
        ),
      ),
    ));

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics.rootChild(
            label: 'test1',
            children: <TestSemantics>[
              TestSemantics(
                label: 'middle',
                children: <TestSemantics>[
                  TestSemantics(
                    label: 'test2b',
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
    ));

    semantics.dispose();
  });

  testWidgets('Semantics and Directionality - RTL', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.rtl,
        child: Semantics(
          label: 'test1',
          child: Container(),
        ),
      ),
    );

    expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.rtl));
    semantics.dispose();
  });

  testWidgets('Semantics and Directionality - LTR', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'test1',
          child: Container(),
        ),
      ),
    );

    expect(semantics, includesNodeWith(label: 'test1', textDirection: TextDirection.ltr));
    semantics.dispose();
  });

  testWidgets('Semantics and Directionality - cannot override RTL with LTR', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          label: 'test1',
          textDirection: TextDirection.ltr,
        ),
      ],
    );

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.rtl,
        child: Semantics(
          label: 'test1',
          textDirection: TextDirection.ltr,
          child: Container(),
        ),
      ),
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });

  testWidgets('Semantics and Directionality - cannot override LTR with RTL', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          label: 'test1',
          textDirection: TextDirection.rtl,
        ),
      ],
    );

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'test1',
          textDirection: TextDirection.rtl,
          child: Container(),
        ),
      ),
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });

  testWidgets('Semantics label and hint', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'label',
          hint: 'hint',
          value: 'value',
          child: Container(),
        ),
      ),
    );

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          label: 'label',
          hint: 'hint',
          value: 'value',
          textDirection: TextDirection.ltr,
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });

  testWidgets('Semantics hints can merge', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          container: true,
          child: Column(
            children: <Widget>[
              Semantics(
                hint: 'hint one',
              ),
              Semantics(
                hint: 'hint two',
              ),

            ],
          ),
        ),
      ),
    );

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          hint: 'hint one\nhint two',
          textDirection: TextDirection.ltr,
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });

  testWidgets('Semantics values do not merge', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          container: true,
          child: Column(
            children: <Widget>[
              Semantics(
                value: 'value one',
                child: const SizedBox(
                  height: 10.0,
                  width: 10.0,
                ),
              ),
              Semantics(
                value: 'value two',
                child: const SizedBox(
                  height: 10.0,
                  width: 10.0,
                ),
              ),
            ],
          ),
        ),
      ),
    );

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              value: 'value one',
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              value: 'value two',
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });

  testWidgets('Semantics value and hint can merge', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          container: true,
          child: Column(
            children: <Widget>[
              Semantics(
                hint: 'hint',
              ),
              Semantics(
                value: 'value',
              ),
            ],
          ),
        ),
      ),
    );

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          hint: 'hint',
          value: 'value',
          textDirection: TextDirection.ltr,
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });

    testWidgets('Semantics tagForChildren works', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          container: true,
          tagForChildren: const SemanticsTag('custom tag'),
          child: Column(
            children: <Widget>[
              Semantics(
                container: true,
                child: const Text('child 1'),
              ),
              Semantics(
                container: true,
                child: const Text('child 2'),
              ),
            ],
          ),
        ),
      ),
    );

    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              label: 'child 1',
              tags: <SemanticsTag>[const SemanticsTag('custom tag')],
              textDirection: TextDirection.ltr,
            ),
            TestSemantics(
              label: 'child 2',
              tags: <SemanticsTag>[const SemanticsTag('custom tag')],
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });

  testWidgets('Semantics widget supports all actions', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    final List<SemanticsAction> performedActions = <SemanticsAction>[];

    await tester.pumpWidget(
      Semantics(
        container: true,
        onDismiss: () => performedActions.add(SemanticsAction.dismiss),
        onTap: () => performedActions.add(SemanticsAction.tap),
        onLongPress: () => performedActions.add(SemanticsAction.longPress),
        onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
        onScrollRight: () => performedActions.add(SemanticsAction.scrollRight),
        onScrollUp: () => performedActions.add(SemanticsAction.scrollUp),
        onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
        onIncrease: () => performedActions.add(SemanticsAction.increase),
        onDecrease: () => performedActions.add(SemanticsAction.decrease),
        onCopy: () => performedActions.add(SemanticsAction.copy),
        onCut: () => performedActions.add(SemanticsAction.cut),
        onPaste: () => performedActions.add(SemanticsAction.paste),
        onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
        onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
        onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
        onSetText: (String _) => performedActions.add(SemanticsAction.setText),
        onDidGainAccessibilityFocus: () => performedActions.add(SemanticsAction.didGainAccessibilityFocus),
        onDidLoseAccessibilityFocus: () => performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
      ),
    );

    final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
      ..remove(SemanticsAction.moveCursorForwardByWord)
      ..remove(SemanticsAction.moveCursorBackwardByWord)
      ..remove(SemanticsAction.customAction) // customAction is not user-exposed.
      ..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed

    const int expectedId = 1;
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: expectedId,
          rect: TestSemantics.fullScreen,
          actions: allActions.fold<int>(0, (int previous, SemanticsAction action) => previous | action.index),
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics));

    // Do the actions work?
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
    int expectedLength = 1;
    for (final SemanticsAction action in allActions) {
      switch (action) {
        case SemanticsAction.moveCursorBackwardByCharacter:
        case SemanticsAction.moveCursorForwardByCharacter:
          semanticsOwner.performAction(expectedId, action, true);
          break;
        case SemanticsAction.setSelection:
          semanticsOwner.performAction(expectedId, action, <dynamic, dynamic>{
            'base': 4,
            'extent': 5,
          });
          break;
        case SemanticsAction.setText:
          semanticsOwner.performAction(expectedId, action, 'text');
          break;
        case SemanticsAction.copy:
        case SemanticsAction.customAction:
        case SemanticsAction.cut:
        case SemanticsAction.decrease:
        case SemanticsAction.didGainAccessibilityFocus:
        case SemanticsAction.didLoseAccessibilityFocus:
        case SemanticsAction.dismiss:
        case SemanticsAction.increase:
        case SemanticsAction.longPress:
        case SemanticsAction.moveCursorBackwardByWord:
        case SemanticsAction.moveCursorForwardByWord:
        case SemanticsAction.paste:
        case SemanticsAction.scrollDown:
        case SemanticsAction.scrollLeft:
        case SemanticsAction.scrollRight:
        case SemanticsAction.scrollUp:
        case SemanticsAction.showOnScreen:
        case SemanticsAction.tap:
          semanticsOwner.performAction(expectedId, action);
          break;
      }
      expect(performedActions.length, expectedLength);
      expect(performedActions.last, action);
      expectedLength += 1;
    }

    semantics.dispose();
  });

  testWidgets('Semantics widget supports all flags', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    // Note: checked state and toggled state are mutually exclusive.
    await tester.pumpWidget(
        Semantics(
          key: const Key('a'),
          container: true,
          explicitChildNodes: true,
          // flags
          enabled: true,
          hidden: true,
          checked: true,
          selected: true,
          button: true,
          slider: true,
          keyboardKey: true,
          link: true,
          textField: true,
          readOnly: true,
          focused: true,
          focusable: true,
          inMutuallyExclusiveGroup: true,
          header: true,
          obscured: true,
          multiline: true,
          scopesRoute: true,
          namesRoute: true,
          image: true,
          liveRegion: true,
        ),
    );
    final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
    flags
      ..remove(SemanticsFlag.hasToggledState)
      ..remove(SemanticsFlag.isToggled)
      ..remove(SemanticsFlag.hasImplicitScrolling);

    TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          rect: TestSemantics.fullScreen,
          flags: flags,
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));

    await tester.pumpWidget(Semantics(
      key: const Key('b'),
      container: true,
      scopesRoute: false,
    ));
    expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          rect: TestSemantics.fullScreen,
          flags: <SemanticsFlag>[],
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));

    await tester.pumpWidget(
      Semantics(
        key: const Key('c'),
        toggled: true,
      ),
    );

    expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          rect: TestSemantics.fullScreen,
          flags: <SemanticsFlag>[
            SemanticsFlag.hasToggledState,
            SemanticsFlag.isToggled,
          ],
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
    semantics.dispose();
  });

  testWidgets('Actions can be replaced without triggering semantics update', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    int semanticsUpdateCount = 0;
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
      listener: () {
        semanticsUpdateCount += 1;
      },
    );

    final List<String> performedActions = <String>[];

    await tester.pumpWidget(
      Semantics(
        container: true,
        onTap: () => performedActions.add('first'),
      ),
    );

    const int expectedId = 1;
    final TestSemantics expectedSemantics = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: expectedId,
          rect: TestSemantics.fullScreen,
          actions: SemanticsAction.tap.index,
        ),
      ],
    );

    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;

    expect(semantics, hasSemantics(expectedSemantics));
    semanticsOwner.performAction(expectedId, SemanticsAction.tap);
    expect(semanticsUpdateCount, 1);
    expect(performedActions, <String>['first']);

    semanticsUpdateCount = 0;
    performedActions.clear();

    // Updating existing handler should not trigger semantics update
    await tester.pumpWidget(
      Semantics(
        container: true,
        onTap: () => performedActions.add('second'),
      ),
    );

    expect(semantics, hasSemantics(expectedSemantics));
    semanticsOwner.performAction(expectedId, SemanticsAction.tap);
    expect(semanticsUpdateCount, 0);
    expect(performedActions, <String>['second']);

    semanticsUpdateCount = 0;
    performedActions.clear();

    // Adding a handler works
    await tester.pumpWidget(
      Semantics(
        container: true,
        onTap: () => performedActions.add('second'),
        onLongPress: () => performedActions.add('longPress'),
      ),
    );

    final TestSemantics expectedSemanticsWithLongPress = TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          id: expectedId,
          rect: TestSemantics.fullScreen,
          actions: SemanticsAction.tap.index | SemanticsAction.longPress.index,
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemanticsWithLongPress));
    semanticsOwner.performAction(expectedId, SemanticsAction.longPress);
    expect(semanticsUpdateCount, 1);
    expect(performedActions, <String>['longPress']);

    semanticsUpdateCount = 0;
    performedActions.clear();

    // Removing a handler works
    await tester.pumpWidget(
      Semantics(
        container: true,
        onTap: () => performedActions.add('second'),
      ),
    );

    expect(semantics, hasSemantics(expectedSemantics));
    expect(semanticsUpdateCount, 1);

    handle.dispose();
    semantics.dispose();
  });

  testWidgets('onTapHint and onLongPressHint create custom actions', (WidgetTester tester) async {
    final SemanticsHandle semantics = tester.ensureSemantics();
    await tester.pumpWidget(Semantics(
      container: true,
      onTap: () { },
      onTapHint: 'test',
    ));

    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
      hasTapAction: true,
      onTapHint: 'test',
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      onLongPress: () { },
      onLongPressHint: 'foo',
    ));

    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
      hasLongPressAction: true,
      onLongPressHint: 'foo',
    ));
    semantics.dispose();
  });

  testWidgets('CustomSemanticsActions can be added to a Semantics widget', (WidgetTester tester) async {
    final SemanticsHandle semantics = tester.ensureSemantics();
    await tester.pumpWidget(Semantics(
      container: true,
      customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
        const CustomSemanticsAction(label: 'foo'): () { },
        const CustomSemanticsAction(label: 'bar'): () { },
      },
    ));

    expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
      customActions: <CustomSemanticsAction>[
        const CustomSemanticsAction(label: 'bar'),
        const CustomSemanticsAction(label: 'foo'),
      ],
    ));
    semantics.dispose();
  });

  testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          container: true,
          value: '10s',
          increasedValue: '11s',
          decreasedValue: '9s',
          onIncrease: () => () { },
          onDecrease: () => () { },
        ),
      ),
    );

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          actions: SemanticsAction.increase.index | SemanticsAction.decrease.index,
          textDirection: TextDirection.ltr,
          value: '10s',
          increasedValue: '11s',
          decreasedValue: '9s',
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));

    semantics.dispose();
  });

  testWidgets('Semantics widgets built in a widget tree are sorted properly', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    int semanticsUpdateCount = 0;
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
      listener: () {
        semanticsUpdateCount += 1;
      },
    );
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Semantics(
          sortKey: const CustomSortKey(0.0),
          explicitChildNodes: true,
          child: Column(
            children: <Widget>[
              Semantics(sortKey: const CustomSortKey(3.0), child: const Text('Label 1')),
              Semantics(sortKey: const CustomSortKey(2.0), child: const Text('Label 2')),
              Semantics(
                sortKey: const CustomSortKey(1.0),
                explicitChildNodes: true,
                child: Row(
                  children: <Widget>[
                    Semantics(sortKey: const OrdinalSortKey(3.0), child: const Text('Label 3')),
                    Semantics(sortKey: const OrdinalSortKey(2.0), child: const Text('Label 4')),
                    Semantics(sortKey: const OrdinalSortKey(1.0), child: const Text('Label 5')),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            id: 1,
            children: <TestSemantics>[
              TestSemantics(
                id: 4,
                children: <TestSemantics>[
                  TestSemantics(
                    id: 7,
                    label: r'Label 5',
                    textDirection: TextDirection.ltr,
                  ),
                  TestSemantics(
                    id: 6,
                    label: r'Label 4',
                    textDirection: TextDirection.ltr,
                  ),
                  TestSemantics(
                    id: 5,
                    label: r'Label 3',
                    textDirection: TextDirection.ltr,
                  ),
                ],
              ),
              TestSemantics(
                id: 3,
                label: r'Label 2',
                textDirection: TextDirection.ltr,
              ),
              TestSemantics(
                id: 2,
                label: r'Label 1',
                textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
      ignoreTransform: true,
      ignoreRect: true,
    ));

    handle.dispose();
    semantics.dispose();
  });

  testWidgets('Semantics widgets built with explicit sort orders are sorted properly', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    int semanticsUpdateCount = 0;
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
      listener: () {
        semanticsUpdateCount += 1;
      },
    );
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Row(
          children: <Widget>[
            Semantics(
              sortKey: const CustomSortKey(3.0),
              child: const Text('Label 1'),
            ),
            Semantics(
              sortKey: const CustomSortKey(1.0),
              child: const Text('Label 2'),
            ),
            Semantics(
              sortKey: const CustomSortKey(2.0),
              child: const Text('Label 3'),
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            id: 2,
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            id: 3,
            label: r'Label 3',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            id: 1,
            label: r'Label 1',
            textDirection: TextDirection.ltr,
          ),
        ],
      ),
      ignoreTransform: true,
      ignoreRect: true,
    ));

    handle.dispose();
    semantics.dispose();
  });

  testWidgets('Semantics widgets without sort orders are sorted properly', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    int semanticsUpdateCount = 0;
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
      listener: () {
        semanticsUpdateCount += 1;
      },
    );
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Column(
          children: <Widget>[
            const Text('Label 1'),
            const Text('Label 2'),
            Row(
              children: const <Widget>[
                Text('Label 3'),
                Text('Label 4'),
                Text('Label 5'),
              ],
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
      TestSemantics(
        children: <TestSemantics>[
          TestSemantics(
            label: r'Label 1',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 3',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 4',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 5',
            textDirection: TextDirection.ltr,
          ),
        ],
      ),
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));

    handle.dispose();
    semantics.dispose();
  });

  testWidgets('Semantics widgets that are transformed are sorted properly', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    int semanticsUpdateCount = 0;
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(
      listener: () {
        semanticsUpdateCount += 1;
      },
    );
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Column(
          children: <Widget>[
            const Text('Label 1'),
            const Text('Label 2'),
            Transform.rotate(
              angle: pi / 2.0,
              child: Row(
                children: const <Widget>[
                  Text('Label 3'),
                  Text('Label 4'),
                  Text('Label 5'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(semantics, hasSemantics(
      TestSemantics(
        children: <TestSemantics>[
          TestSemantics(
            label: r'Label 1',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 2',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 3',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 4',
            textDirection: TextDirection.ltr,
          ),
          TestSemantics(
            label: r'Label 5',
            textDirection: TextDirection.ltr,
          ),
        ],
      ),
      ignoreTransform: true,
      ignoreRect: true,
      ignoreId: true,
    ));

    handle.dispose();
    semantics.dispose();
  });

  testWidgets('Semantics widgets without sort orders are sorted properly when no Directionality is present', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    int semanticsUpdateCount = 0;
    final SemanticsHandle handle = tester.binding.pipelineOwner.ensureSemantics(listener: () {
      semanticsUpdateCount += 1;
    });
    await tester.pumpWidget(
      Stack(
        alignment: Alignment.center,
        children: <Widget>[
          // Set this up so that the placeholder takes up the whole screen,
          // and place the positioned boxes so that if we traverse in the
          // geometric order, we would go from box [4, 3, 2, 1, 0], but if we
          // go in child order, then we go from box [4, 1, 2, 3, 0]. We're verifying
          // that we go in child order here, not geometric order, since there
          // is no directionality, so we don't have a geometric opinion about
          // horizontal order. We do still want to sort vertically, however,
          // which is why the order isn't [0, 1, 2, 3, 4].
          Semantics(
            button: true,
            child: const Placeholder(),
          ),
          Positioned(
            top: 200.0,
            left: 100.0,
            child: Semantics( // Box 0
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
          Positioned(
            top: 100.0,
            left: 200.0,
            child: Semantics( // Box 1
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
          Positioned(
            top: 100.0,
            left: 100.0,
            child: Semantics( // Box 2
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
          Positioned(
            top: 100.0,
            left: 0.0,
            child: Semantics( // Box 3
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
          Positioned(
            top: 10.0,
            left: 100.0,
            child: Semantics( // Box 4
              button: true,
              child: const SizedBox(width: 30.0, height: 30.0),
            ),
          ),
        ],
      ),
    );
    expect(semanticsUpdateCount, 1);
    expect(
      semantics,
      hasSemantics(
        TestSemantics(
          children: <TestSemantics>[
            TestSemantics(
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
            TestSemantics(
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
            TestSemantics(
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
            TestSemantics(
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
            TestSemantics(
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
            TestSemantics(
              flags: <SemanticsFlag>[SemanticsFlag.isButton],
            ),
          ],
        ),
        ignoreTransform: true,
        ignoreRect: true,
        ignoreId: true,
      ),
    );

    handle.dispose();
    semantics.dispose();
  });

  testWidgets('Semantics excludeSemantics ignores children', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await tester.pumpWidget(Semantics(
      label: 'label',
      excludeSemantics: true,
      textDirection: TextDirection.ltr,
      child: Semantics(
        label: 'other label',
        textDirection: TextDirection.ltr,
      ),
    ));

    expect(semantics, hasSemantics(
      TestSemantics(
        children: <TestSemantics>[
          TestSemantics(
            label: 'label',
            textDirection: TextDirection.ltr,
          ),
        ],
      ),
      ignoreId: true,
      ignoreRect: true,
      ignoreTransform: true,
    ));
    semantics.dispose();
  });

  testWidgets('Can change handlers', (WidgetTester tester) async {
    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onTap: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasTapAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDismiss: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDismissAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onLongPress: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasLongPressAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollLeft: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollLeftAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollRight: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollRightAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollUp: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollUpAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onScrollDown: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasScrollDownAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onIncrease: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasIncreaseAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDecrease: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDecreaseAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onCopy: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasCopyAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onCut: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasCutAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onPaste: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasPasteAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onSetSelection: (TextSelection _) {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasSetSelectionAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDidGainAccessibilityFocus: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDidGainAccessibilityFocusAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));


    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
      onDidLoseAccessibilityFocus: () {},
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      hasDidLoseAccessibilityFocusAction: true,
      textDirection: TextDirection.ltr,
    ));

    await tester.pumpWidget(Semantics(
      container: true,
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));

    expect(tester.getSemantics(find.bySemanticsLabel('foo')), matchesSemantics(
      label: 'foo',
      textDirection: TextDirection.ltr,
    ));
  });
}

class CustomSortKey extends OrdinalSortKey {
  const CustomSortKey(double order, {String? name}) : super(order, name: name);
}