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

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

import '../foundation/leak_tracking.dart';
import '../rendering/mock_canvas.dart';

void main() {
  testWidgetsWithLeakTracking('Navigation bar updates destinations when tapped', (WidgetTester tester) async {
    int mutatedIndex = -1;
    final Widget widget = _buildWidget(
      NavigationBar(
        destinations: const <Widget>[
          NavigationDestination(
            icon: Icon(Icons.ac_unit),
            label: 'AC',
          ),
          NavigationDestination(
            icon: Icon(Icons.access_alarm),
            label: 'Alarm',
          ),
        ],
        onDestinationSelected: (int i) {
          mutatedIndex = i;
        },
      ),
    );

    await tester.pumpWidget(widget);

    expect(find.text('AC'), findsOneWidget);
    expect(find.text('Alarm'), findsOneWidget);

    await tester.tap(find.text('Alarm'));
    expect(mutatedIndex, 1);

    await tester.tap(find.text('AC'));
    expect(mutatedIndex, 0);
  });

  testWidgetsWithLeakTracking('NavigationBar can update background color', (WidgetTester tester) async {
    const Color color = Colors.yellow;

    await tester.pumpWidget(
      _buildWidget(
        NavigationBar(
          backgroundColor: color,
          destinations: const <Widget>[
            NavigationDestination(
              icon: Icon(Icons.ac_unit),
              label: 'AC',
            ),
            NavigationDestination(
              icon: Icon(Icons.access_alarm),
              label: 'Alarm',
            ),
          ],
          onDestinationSelected: (int i) {},
        ),
      ),
    );

    expect(_getMaterial(tester).color, equals(color));
  });

  testWidgetsWithLeakTracking('NavigationBar can update elevation', (WidgetTester tester) async {
    const double elevation = 42.0;

    await tester.pumpWidget(
      _buildWidget(
        NavigationBar(
          elevation: elevation,
          destinations: const <Widget>[
            NavigationDestination(
              icon: Icon(Icons.ac_unit),
              label: 'AC',
            ),
            NavigationDestination(
              icon: Icon(Icons.access_alarm),
              label: 'Alarm',
            ),
          ],
          onDestinationSelected: (int i) {},
        ),
      ),
    );

    expect(_getMaterial(tester).elevation, equals(elevation));
  });

  testWidgetsWithLeakTracking('NavigationBar adds bottom padding to height', (WidgetTester tester) async {
    const double bottomPadding = 40.0;

    await tester.pumpWidget(
      _buildWidget(
        NavigationBar(
          destinations: const <Widget>[
            NavigationDestination(
              icon: Icon(Icons.ac_unit),
              label: 'AC',
            ),
            NavigationDestination(
              icon: Icon(Icons.access_alarm),
              label: 'Alarm',
            ),
          ],
          onDestinationSelected: (int i) {},
        ),
      ),
    );

    final double defaultSize = tester.getSize(find.byType(NavigationBar)).height;
    expect(defaultSize, 80);

    await tester.pumpWidget(
      _buildWidget(
        MediaQuery(
          data: const MediaQueryData(padding: EdgeInsets.only(bottom: bottomPadding)),
          child: NavigationBar(
            destinations: const <Widget>[
              NavigationDestination(
                icon: Icon(Icons.ac_unit),
                label: 'AC',
              ),
              NavigationDestination(
                icon: Icon(Icons.access_alarm),
                label: 'Alarm',
              ),
            ],
            onDestinationSelected: (int i) {},
          ),
        ),
      ),
    );

    final double expectedHeight = defaultSize + bottomPadding;
    expect(tester.getSize(find.byType(NavigationBar)).height, expectedHeight);
  });

  testWidgetsWithLeakTracking('NavigationBar respects the notch/system navigation bar in landscape mode', (WidgetTester tester) async {
    const double safeAreaPadding = 40.0;
    Widget navigationBar() {
      return NavigationBar(
        destinations: const <Widget>[
          NavigationDestination(
            icon: Icon(Icons.ac_unit),
            label: 'AC',
          ),
          NavigationDestination(
            key: Key('Center'),
            icon: Icon(Icons.center_focus_strong),
            label: 'Center',
          ),
          NavigationDestination(
            icon: Icon(Icons.access_alarm),
            label: 'Alarm',
          ),
        ],
        onDestinationSelected: (int i) {},
      );
    }

    await tester.pumpWidget(_buildWidget(navigationBar()));
    final double defaultWidth = tester.getSize(find.byType(NavigationBar)).width;
    final Finder defaultCenterItem = find.byKey(const Key('Center'));
    final Offset center = tester.getCenter(defaultCenterItem);
    expect(center.dx, defaultWidth / 2);

    await tester.pumpWidget(
      _buildWidget(
        MediaQuery(
          data: const MediaQueryData(
            padding: EdgeInsets.only(left: safeAreaPadding),
          ),
          child: navigationBar(),
        ),
      ),
    );

    // The position of center item of navigation bar should indicate whether
    // the safe area is sufficiently respected, when safe area is on the left side.
    // e.g. Android device with system navigation bar in landscape mode.
    final Finder leftPaddedCenterItem = find.byKey(const Key('Center'));
    final Offset leftPaddedCenter = tester.getCenter(leftPaddedCenterItem);
    expect(
      leftPaddedCenter.dx,
      closeTo((defaultWidth + safeAreaPadding) / 2.0, precisionErrorTolerance),
    );

    await tester.pumpWidget(
      _buildWidget(
        MediaQuery(
          data: const MediaQueryData(
              padding: EdgeInsets.only(right: safeAreaPadding)
          ),
          child: navigationBar(),
        ),
      ),
    );

    // The position of center item of navigation bar should indicate whether
    // the safe area is sufficiently respected, when safe area is on the right side.
    // e.g. Android device with system navigation bar in landscape mode.
    final Finder rightPaddedCenterItem = find.byKey(const Key('Center'));
    final Offset rightPaddedCenter = tester.getCenter(rightPaddedCenterItem);
    expect(
      rightPaddedCenter.dx,
      closeTo((defaultWidth - safeAreaPadding) / 2, precisionErrorTolerance),
    );

    await tester.pumpWidget(
      _buildWidget(
        MediaQuery(
          data: const MediaQueryData(
            padding: EdgeInsets.fromLTRB(
                safeAreaPadding,
                0,
                safeAreaPadding,
                safeAreaPadding
            ),
          ),
          child: navigationBar(),
        ),
      ),
    );

    // The position of center item of navigation bar should indicate whether
    // the safe area is sufficiently respected, when safe areas are on both sides.
    // e.g. iOS device with both sides of round corner.
    final Finder paddedCenterItem = find.byKey(const Key('Center'));
    final Offset paddedCenter = tester.getCenter(paddedCenterItem);
    expect(
      paddedCenter.dx,
      closeTo(defaultWidth / 2, precisionErrorTolerance),
    );
  });

  testWidgetsWithLeakTracking('Material2 - NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async {
    // M2 settings that were hand coded.
    await tester.pumpWidget(
      _buildWidget(
        NavigationBar(
          destinations: const <Widget>[
            NavigationDestination(
              icon: Icon(Icons.ac_unit),
              label: 'AC',
            ),
            NavigationDestination(
              icon: Icon(Icons.access_alarm),
              label: 'Alarm',
            ),
          ],
          onDestinationSelected: (int i) {},
        ),
        useMaterial3: false,
      ),
    );

    expect(_getMaterial(tester).color, const Color(0xffeaeaea));
    expect(_getMaterial(tester).surfaceTintColor, null);
    expect(_getMaterial(tester).elevation, 0);
    expect(tester.getSize(find.byType(NavigationBar)).height, 80);
    expect(_getIndicatorDecoration(tester)?.color, const Color(0x3d2196f3));
    expect(_getIndicatorDecoration(tester)?.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)));
  });

  testWidgetsWithLeakTracking('Material3 - NavigationBar uses proper defaults when no parameters are given', (WidgetTester tester) async {
    // M3 settings from the token database.
    final ThemeData theme = ThemeData(useMaterial3: true);
    await tester.pumpWidget(
      _buildWidget(
          NavigationBar(
            destinations: const <Widget>[
              NavigationDestination(
                icon: Icon(Icons.ac_unit),
                label: 'AC',
              ),
              NavigationDestination(
                icon: Icon(Icons.access_alarm),
                label: 'Alarm',
              ),
            ],
            onDestinationSelected: (int i) {},
          ),
          useMaterial3: theme.useMaterial3
      ),
    );

    expect(_getMaterial(tester).color, theme.colorScheme.surface);
    expect(_getMaterial(tester).surfaceTintColor, theme.colorScheme.surfaceTint);
    expect(_getMaterial(tester).elevation, 3);
    expect(tester.getSize(find.byType(NavigationBar)).height, 80);
    expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
    expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());
  });

  testWidgetsWithLeakTracking('Material2 - NavigationBar shows tooltips with text scaling', (WidgetTester tester) async {
    const String label = 'A';

    Widget buildApp({ required double textScaleFactor }) {
      return MediaQuery(
        data: MediaQueryData(textScaleFactor: textScaleFactor),
        child: Localizations(
          locale: const Locale('en', 'US'),
          delegates: const <LocalizationsDelegate<dynamic>>[
            DefaultMaterialLocalizations.delegate,
            DefaultWidgetsLocalizations.delegate,
          ],
          child: MaterialApp(
            theme: ThemeData(useMaterial3: false),
            home: Navigator(
              onGenerateRoute: (RouteSettings settings) {
                return MaterialPageRoute<void>(
                  builder: (BuildContext context) {
                    return Scaffold(
                      bottomNavigationBar: NavigationBar(
                        destinations: const <NavigationDestination>[
                          NavigationDestination(
                            label: label,
                            icon: Icon(Icons.ac_unit),
                            tooltip: label,
                          ),
                          NavigationDestination(
                            label: 'B',
                            icon: Icon(Icons.battery_alert),
                          ),
                        ],
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildApp(textScaleFactor: 1.0));
    expect(find.text(label), findsOneWidget);
    await tester.longPress(find.text(label));
    expect(find.text(label), findsNWidgets(2));

    // The default size of a tooltip with the text A.
    const Size defaultTooltipSize = Size(14.0, 14.0);
    expect(tester.getSize(find.text(label).last), defaultTooltipSize);
    // The duration is needed to ensure the tooltip disappears.
    await tester.pumpAndSettle(const Duration(seconds: 2));

    await tester.pumpWidget(buildApp(textScaleFactor: 4.0));
    expect(find.text(label), findsOneWidget);
    await tester.longPress(find.text(label));
    expect(tester.getSize(find.text(label).last), Size(defaultTooltipSize.width * 4, defaultTooltipSize.height * 4));
  });

  testWidgetsWithLeakTracking('Material3 - NavigationBar shows tooltips with text scaling', (WidgetTester tester) async {
    const String label = 'A';

    Widget buildApp({ required double textScaleFactor }) {
      return MediaQuery(
        data: MediaQueryData(textScaleFactor: textScaleFactor),
        child: Localizations(
          locale: const Locale('en', 'US'),
          delegates: const <LocalizationsDelegate<dynamic>>[
            DefaultMaterialLocalizations.delegate,
            DefaultWidgetsLocalizations.delegate,
          ],
          child: MaterialApp(
            theme: ThemeData(useMaterial3: true),
            home: Navigator(
              onGenerateRoute: (RouteSettings settings) {
                return MaterialPageRoute<void>(
                  builder: (BuildContext context) {
                    return Scaffold(
                      bottomNavigationBar: NavigationBar(
                        destinations: const <NavigationDestination>[
                          NavigationDestination(
                            label: label,
                            icon: Icon(Icons.ac_unit),
                            tooltip: label,
                          ),
                          NavigationDestination(
                            label: 'B',
                            icon: Icon(Icons.battery_alert),
                          ),
                        ],
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildApp(textScaleFactor: 1.0));
    expect(find.text(label), findsOneWidget);
    await tester.longPress(find.text(label));
    expect(find.text(label), findsNWidgets(2));

    if (kIsWeb && !isCanvasKit) {
      expect(tester.getSize(find.text(label).last), const Size(15.0, 21.0));
    } else {
      expect(tester.getSize(find.text(label).last), const Size(15.0, 20.0));
    }
    // The duration is needed to ensure the tooltip disappears.
    await tester.pumpAndSettle(const Duration(seconds: 2));

    await tester.pumpWidget(buildApp(textScaleFactor: 4.0));
    expect(find.text(label), findsOneWidget);
    await tester.longPress(find.text(label));

    if (kIsWeb && !isCanvasKit) {
      expect(tester.getSize(find.text(label).last), const Size(57.0, 81.0));
    } else {
      expect(tester.getSize(find.text(label).last), const Size(57.0, 80.0));
    }
  });

  testWidgetsWithLeakTracking('Custom tooltips in NavigationBarDestination', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          bottomNavigationBar: NavigationBar(
            destinations: const <NavigationDestination>[
              NavigationDestination(
                label: 'A',
                tooltip: 'A tooltip',
                icon: Icon(Icons.ac_unit),
              ),
              NavigationDestination(
                label: 'B',
                icon: Icon(Icons.battery_alert),
              ),
              NavigationDestination(
                label: 'C',
                icon: Icon(Icons.cake),
                tooltip: '',
              ),
            ],
          ),
        ),
      ),
    );

    expect(find.text('A'), findsOneWidget);
    await tester.longPress(find.text('A'));
    expect(find.byTooltip('A tooltip'), findsOneWidget);

    expect(find.text('B'), findsOneWidget);
    await tester.longPress(find.text('B'));
    expect(find.byTooltip('B'), findsOneWidget);

    expect(find.text('C'), findsOneWidget);
    await tester.longPress(find.text('C'));
    expect(find.byTooltip('C'), findsNothing);
  });


  testWidgetsWithLeakTracking('Navigation bar semantics', (WidgetTester tester) async {
    Widget widget({int selectedIndex = 0}) {
      return _buildWidget(
        NavigationBar(
          selectedIndex: selectedIndex,
          destinations: const <Widget>[
            NavigationDestination(
              icon: Icon(Icons.ac_unit),
              label: 'AC',
            ),
            NavigationDestination(
              icon: Icon(Icons.access_alarm),
              label: 'Alarm',
            ),
          ],
        ),
      );
    }

    await tester.pumpWidget(widget());

    expect(
      tester.getSemantics(find.text('AC')),
      matchesSemantics(
        label: 'AC\nTab 1 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        isSelected: true,
        hasTapAction: true,
      ),
    );
    expect(
      tester.getSemantics(find.text('Alarm')),
      matchesSemantics(
        label: 'Alarm\nTab 2 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        hasTapAction: true,
      ),
    );

    await tester.pumpWidget(widget(selectedIndex: 1));

    expect(
      tester.getSemantics(find.text('AC')),
      matchesSemantics(
        label: 'AC\nTab 1 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        hasTapAction: true,
      ),
    );
    expect(
      tester.getSemantics(find.text('Alarm')),
      matchesSemantics(
        label: 'Alarm\nTab 2 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        isSelected: true,
        hasTapAction: true,
      ),
    );
  });

  testWidgetsWithLeakTracking('Navigation bar semantics with some labels hidden', (WidgetTester tester) async {
    Widget widget({int selectedIndex = 0}) {
      return _buildWidget(
        NavigationBar(
          labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected,
          selectedIndex: selectedIndex,
          destinations: const <Widget>[
            NavigationDestination(
              icon: Icon(Icons.ac_unit),
              label: 'AC',
            ),
            NavigationDestination(
              icon: Icon(Icons.access_alarm),
              label: 'Alarm',
            ),
          ],
        ),
      );
    }

    await tester.pumpWidget(widget());

    expect(
      tester.getSemantics(find.text('AC')),
      matchesSemantics(
        label: 'AC\nTab 1 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        isSelected: true,
        hasTapAction: true,
      ),
    );
    expect(
      tester.getSemantics(find.text('Alarm')),
      matchesSemantics(
        label: 'Alarm\nTab 2 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        hasTapAction: true,
      ),
    );

    await tester.pumpWidget(widget(selectedIndex: 1));

    expect(
      tester.getSemantics(find.text('AC')),
      matchesSemantics(
        label: 'AC\nTab 1 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        hasTapAction: true,
      ),
    );
    expect(
      tester.getSemantics(find.text('Alarm')),
      matchesSemantics(
        label: 'Alarm\nTab 2 of 2',
        textDirection: TextDirection.ltr,
        isFocusable: true,
        isSelected: true,
        hasTapAction: true,
      ),
    );
  });

  testWidgetsWithLeakTracking('Navigation bar does not grow with text scale factor', (WidgetTester tester) async {
    const int animationMilliseconds = 800;

    Widget widget({double textScaleFactor = 1}) {
      return _buildWidget(
        MediaQuery(
          data: MediaQueryData(textScaleFactor: textScaleFactor),
          child: NavigationBar(
            animationDuration: const Duration(milliseconds: animationMilliseconds),
            destinations: const <NavigationDestination>[
              NavigationDestination(
                icon: Icon(Icons.ac_unit),
                label: 'AC',
              ),
              NavigationDestination(
                icon: Icon(Icons.access_alarm),
                label: 'Alarm',
              ),
            ],
          ),
        ),
      );
    }

    await tester.pumpWidget(widget());
    final double initialHeight = tester.getSize(find.byType(NavigationBar)).height;

    await tester.pumpWidget(widget(textScaleFactor: 2));
    final double newHeight = tester.getSize(find.byType(NavigationBar)).height;

    expect(newHeight, equals(initialHeight));
  });

  testWidgetsWithLeakTracking('Material3 - Navigation indicator renders ripple', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/116751.
    int selectedIndex = 0;

    Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: Scaffold(
          bottomNavigationBar: Center(
            child: NavigationBar(
              selectedIndex: selectedIndex,
              labelBehavior: labelBehavior,
              destinations: const <Widget>[
                NavigationDestination(
                  icon: Icon(Icons.ac_unit),
                  label: 'AC',
                ),
                NavigationDestination(
                  icon: Icon(Icons.access_alarm),
                  label: 'Alarm',
                ),
              ],
              onDestinationSelected: (int i) { },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildWidget());

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
    await tester.pumpAndSettle();

    final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
    Offset indicatorCenter = const Offset(600, 30);
    const Size includedIndicatorSize = Size(64, 32);
    const Size excludedIndicatorSize = Size(74, 40);

    // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
    expect(
      inkFeatures,
      paints
        ..clipPath(
          pathMatcher: isPathThat(
            includes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
            ],
            excludes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
            ],
          ),
        )
        ..circle(
          x: indicatorCenter.dx,
          y: indicatorCenter.dy,
          radius: 35.0,
          color: const Color(0x0a000000),
        )
    );

    // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
    await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide));
    await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
    await tester.pumpAndSettle();

    indicatorCenter = const Offset(600, 40);

    expect(
      inkFeatures,
      paints
        ..clipPath(
          pathMatcher: isPathThat(
            includes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
            ],
            excludes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
            ],
          ),
        )
        ..circle(
          x: indicatorCenter.dx,
          y: indicatorCenter.dy,
          radius: 35.0,
          color: const Color(0x0a000000),
        )
    );

    // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
    await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
    await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
    await tester.pumpAndSettle();

    expect(
      inkFeatures,
      paints
        ..clipPath(
          pathMatcher: isPathThat(
            includes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
            ],
            excludes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
            ],
          ),
        )
        ..circle(
          x: indicatorCenter.dx,
          y: indicatorCenter.dy,
          radius: 35.0,
          color: const Color(0x0a000000),
        )
    );

    // Make sure ripple is shifted when selectedIndex changes.
    selectedIndex = 1;
    await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
    await tester.pumpAndSettle();
    indicatorCenter = const Offset(600, 30);

    expect(
      inkFeatures,
      paints
        ..clipPath(
          pathMatcher: isPathThat(
            includes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
            ],
            excludes: <Offset>[
              // Left center.
              Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Top center.
              Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
              // Right center.
              Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
              // Bottom center.
              Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
            ],
          ),
        )
        ..circle(
          x: indicatorCenter.dx,
          y: indicatorCenter.dy,
          radius: 35.0,
          color: const Color(0x0a000000),
        )
    );
  });

  testWidgetsWithLeakTracking('Material3 - Navigation indicator ripple golden test', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/117420.

    Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: Scaffold(
          bottomNavigationBar: Center(
            child: NavigationBar(
              labelBehavior: labelBehavior,
              destinations: const <Widget>[
                NavigationDestination(
                  icon: SizedBox(),
                  label: 'AC',
                ),
                NavigationDestination(
                  icon: SizedBox(),
                  label: 'Alarm',
                ),
              ],
              onDestinationSelected: (int i) { },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildWidget());

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
    await tester.pumpAndSettle();

    // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
    await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysShow_m3.png'));

    // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
    await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide));
    await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
    await tester.pumpAndSettle();

    await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysHide_m3.png'));

    // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
    await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
    await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).first));
    await tester.pumpAndSettle();

    await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_selected_m3.png'));

    await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
    await tester.pumpAndSettle();

    await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_unselected_m3.png'));
  });

  testWidgetsWithLeakTracking('Navigation indicator scale transform', (WidgetTester tester) async {
    int selectedIndex = 0;

    Widget buildNavigationBar() {
      return MaterialApp(
        theme: ThemeData.light(),
        home: Scaffold(
          bottomNavigationBar: Center(
            child: NavigationBar(
              selectedIndex: selectedIndex,
              destinations: const <Widget>[
                NavigationDestination(
                  icon: Icon(Icons.ac_unit),
                  label: 'AC',
                ),
                NavigationDestination(
                  icon: Icon(Icons.access_alarm),
                  label: 'Alarm',
                ),
              ],
              onDestinationSelected: (int i) { },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildNavigationBar());
    await tester.pumpAndSettle();
    final Finder transformFinder = find.descendant(
      of: find.byType(NavigationIndicator),
      matching: find.byType(Transform),
    ).last;
    Matrix4 transform = tester.widget<Transform>(transformFinder).transform;
    expect(transform.getColumn(0)[0], 0.0);

    selectedIndex = 1;
    await tester.pumpWidget(buildNavigationBar());
    await tester.pump(const Duration(milliseconds: 100));
    transform = tester.widget<Transform>(transformFinder).transform;
    expect(transform.getColumn(0)[0], closeTo(0.7805849514007568, precisionErrorTolerance));

    await tester.pump(const Duration(milliseconds: 100));
    transform = tester.widget<Transform>(transformFinder).transform;
    expect(transform.getColumn(0)[0], closeTo(0.9473570239543915, precisionErrorTolerance));

    await tester.pumpAndSettle();
    transform = tester.widget<Transform>(transformFinder).transform;
    expect(transform.getColumn(0)[0], 1.0);
  });

  testWidgetsWithLeakTracking('Material3 - Navigation destination updates indicator color and shape', (WidgetTester tester) async {
    final ThemeData theme = ThemeData(useMaterial3: true);
    const Color color = Color(0xff0000ff);
    const ShapeBorder shape = RoundedRectangleBorder();

    Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
      return MaterialApp(
        theme: theme,
        home: Scaffold(
          bottomNavigationBar: NavigationBar(
            indicatorColor: indicatorColor,
            indicatorShape: indicatorShape,
            destinations: const <Widget>[
              NavigationDestination(
                icon: Icon(Icons.ac_unit),
                label: 'AC',
              ),
              NavigationDestination(
                icon: Icon(Icons.access_alarm),
                label: 'Alarm',
              ),
            ],
            onDestinationSelected: (int i) { },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildNavigationBar());

    // Test default indicator color and shape.
    expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondaryContainer);
    expect(_getIndicatorDecoration(tester)?.shape, const StadiumBorder());

    await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));

    // Test custom indicator color and shape.
    expect(_getIndicatorDecoration(tester)?.color, color);
    expect(_getIndicatorDecoration(tester)?.shape, shape);
  });

  group('Material 2', () {
    // These tests are only relevant for Material 2. Once Material 2
    // support is deprecated and the APIs are removed, these tests
    // can be deleted.

    testWidgetsWithLeakTracking('Material2 - Navigation destination updates indicator color and shape', (WidgetTester tester) async {
      final ThemeData theme = ThemeData(useMaterial3: false);
      const Color color = Color(0xff0000ff);
      const ShapeBorder shape = RoundedRectangleBorder();

      Widget buildNavigationBar({Color? indicatorColor, ShapeBorder? indicatorShape}) {
        return MaterialApp(
          theme: theme,
          home: Scaffold(
            bottomNavigationBar: NavigationBar(
              indicatorColor: indicatorColor,
              indicatorShape: indicatorShape,
              destinations: const <Widget>[
                NavigationDestination(
                  icon: Icon(Icons.ac_unit),
                  label: 'AC',
                ),
                NavigationDestination(
                  icon: Icon(Icons.access_alarm),
                  label: 'Alarm',
                ),
              ],
              onDestinationSelected: (int i) { },
            ),
          ),
        );
      }

      await tester.pumpWidget(buildNavigationBar());

      // Test default indicator color and shape.
      expect(_getIndicatorDecoration(tester)?.color, theme.colorScheme.secondary.withOpacity(0.24));
      expect(
        _getIndicatorDecoration(tester)?.shape,
        const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
      );

      await tester.pumpWidget(buildNavigationBar(indicatorColor: color, indicatorShape: shape));

      // Test custom indicator color and shape.
      expect(_getIndicatorDecoration(tester)?.color, color);
      expect(_getIndicatorDecoration(tester)?.shape, shape);
    });

    testWidgetsWithLeakTracking('Material2 - Navigation indicator renders ripple', (WidgetTester tester) async {
      // This is a regression test for https://github.com/flutter/flutter/issues/116751.
      int selectedIndex = 0;

      Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) {
        return MaterialApp(
          theme: ThemeData(useMaterial3: false),
          home: Scaffold(
            bottomNavigationBar: Center(
              child: NavigationBar(
              selectedIndex: selectedIndex,
              labelBehavior: labelBehavior,
              destinations: const <Widget>[
                NavigationDestination(
                  icon: Icon(Icons.ac_unit),
                  label: 'AC',
                ),
                NavigationDestination(
                  icon: Icon(Icons.access_alarm),
                  label: 'Alarm',
                ),
              ],
              onDestinationSelected: (int i) { },
            ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildWidget());

      final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
      await gesture.addPointer();
      await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
      await tester.pumpAndSettle();

      final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
      Offset indicatorCenter = const Offset(600, 33);
      const Size includedIndicatorSize = Size(64, 32);
      const Size excludedIndicatorSize = Size(74, 40);

      // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
      expect(
        inkFeatures,
        paints
          ..clipPath(
            pathMatcher: isPathThat(
              includes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
              ],
              excludes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
              ],
            ),
          )
          ..circle(
            x: indicatorCenter.dx,
            y: indicatorCenter.dy,
            radius: 35.0,
            color: const Color(0x0a000000),
          )
      );

      // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
      await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide));
      await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
      await tester.pumpAndSettle();

      indicatorCenter = const Offset(600, 40);

      expect(
        inkFeatures,
        paints
          ..clipPath(
            pathMatcher: isPathThat(
              includes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
              ],
              excludes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
              ],
            ),
          )
          ..circle(
            x: indicatorCenter.dx,
            y: indicatorCenter.dy,
            radius: 35.0,
            color: const Color(0x0a000000),
          )
      );

      // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
      await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
      await gesture.moveTo(tester.getCenter(find.byIcon(Icons.access_alarm)));
      await tester.pumpAndSettle();

      expect(
        inkFeatures,
        paints
          ..clipPath(
            pathMatcher: isPathThat(
              includes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
              ],
              excludes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
              ],
            ),
          )
          ..circle(
            x: indicatorCenter.dx,
            y: indicatorCenter.dy,
            radius: 35.0,
            color: const Color(0x0a000000),
          )
      );

      // Make sure ripple is shifted when selectedIndex changes.
      selectedIndex = 1;
      await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
      await tester.pumpAndSettle();
      indicatorCenter = const Offset(600, 33);

      expect(
        inkFeatures,
        paints
          ..clipPath(
            pathMatcher: isPathThat(
              includes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (includedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (includedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (includedIndicatorSize.height / 2)),
              ],
              excludes: <Offset>[
                // Left center.
                Offset(indicatorCenter.dx - (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Top center.
                Offset(indicatorCenter.dx, indicatorCenter.dy - (excludedIndicatorSize.height / 2)),
                // Right center.
                Offset(indicatorCenter.dx + (excludedIndicatorSize.width / 2), indicatorCenter.dy),
                // Bottom center.
                Offset(indicatorCenter.dx, indicatorCenter.dy + (excludedIndicatorSize.height / 2)),
              ],
            ),
          )
          ..circle(
            x: indicatorCenter.dx,
            y: indicatorCenter.dy,
            radius: 35.0,
            color: const Color(0x0a000000),
          )
      );
    });

    testWidgetsWithLeakTracking('Material2 - Navigation indicator ripple golden test', (WidgetTester tester) async {
      // This is a regression test for https://github.com/flutter/flutter/issues/117420.

      Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) {
        return MaterialApp(
          theme: ThemeData(useMaterial3: false),
          home: Scaffold(
            bottomNavigationBar: Center(
              child: NavigationBar(
                labelBehavior: labelBehavior,
                destinations: const <Widget>[
                  NavigationDestination(
                    icon: SizedBox(),
                    label: 'AC',
                  ),
                  NavigationDestination(
                    icon: SizedBox(),
                    label: 'Alarm',
                  ),
                ],
                onDestinationSelected: (int i) { },
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildWidget());

      final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
      await gesture.addPointer();
      await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
      await tester.pumpAndSettle();

      // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysShow` (default).
      await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysShow_m2.png'));

      // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.alwaysHide`.
      await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.alwaysHide));
      await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
      await tester.pumpAndSettle();

      await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_alwaysHide_m2.png'));

      // Test ripple when NavigationBar is using `NavigationDestinationLabelBehavior.onlyShowSelected`.
      await tester.pumpWidget(buildWidget(labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected));
      await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).first));
      await tester.pumpAndSettle();

      await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_selected_m2.png'));

      await gesture.moveTo(tester.getCenter(find.byType(NavigationDestination).last));
      await tester.pumpAndSettle();

      await expectLater(find.byType(NavigationBar), matchesGoldenFile('indicator_onlyShowSelected_unselected_m2.png'));
    });

    testWidgetsWithLeakTracking('Destination icon does not rebuild when tapped', (WidgetTester tester) async {
      // This is a regression test for https://github.com/flutter/flutter/issues/122811.

      Widget buildNavigationBar() {
        return MaterialApp(
          home: Scaffold(
            bottomNavigationBar: StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
                int selectedIndex = 0;
                return NavigationBar(
                  selectedIndex: selectedIndex,
                  destinations: const <Widget>[
                    NavigationDestination(
                      icon: IconWithRandomColor(icon: Icons.ac_unit),
                      label: 'AC',
                    ),
                    NavigationDestination(
                      icon: IconWithRandomColor(icon: Icons.access_alarm),
                      label: 'Alarm',
                    ),
                  ],
                  onDestinationSelected: (int i) {
                    setState(() {
                      selectedIndex = i;
                    });
                  },
                );
              }
            ),
          ),
        );
      }

      await tester.pumpWidget(buildNavigationBar());
      Icon icon = tester.widget<Icon>(find.byType(Icon).last);
      final Color initialColor = icon.color!;

      // Trigger a rebuild.
      await tester.tap(find.text('Alarm'));
      await tester.pumpAndSettle();

      // Icon color should be the same as before the rebuild.
      icon = tester.widget<Icon>(find.byType(Icon).last);
      expect(icon.color, initialColor);
    });
  });
}

Widget _buildWidget(Widget child, { bool? useMaterial3 }) {
  return MaterialApp(
    theme: ThemeData(useMaterial3: useMaterial3),
    home: Scaffold(
      bottomNavigationBar: Center(
        child: child,
      ),
    ),
  );
}

Material _getMaterial(WidgetTester tester) {
  return tester.firstWidget<Material>(
    find.descendant(of: find.byType(NavigationBar), matching: find.byType(Material)),
  );
}

ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) {
  return tester.firstWidget<Container>(
    find.descendant(
      of: find.byType(FadeTransition),
      matching: find.byType(Container),
    ),
  ).decoration as ShapeDecoration?;
}

class IconWithRandomColor extends StatelessWidget {
  const IconWithRandomColor({super.key, required this.icon});

  final IconData icon;

  @override
  Widget build(BuildContext context) {
    final Color randomColor = Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
    return Icon(icon, color: randomColor);
  }
}