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

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

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

void main() {
  test('copyWith, ==, hashCode basics', () {
    expect(const NavigationBarThemeData(), const NavigationBarThemeData().copyWith());
    expect(const NavigationBarThemeData().hashCode, const NavigationBarThemeData().copyWith().hashCode);
  });

  test('NavigationBarThemeData lerp special cases', () {
    expect(NavigationBarThemeData.lerp(null, null, 0), null);
    const NavigationBarThemeData data = NavigationBarThemeData();
    expect(identical(NavigationBarThemeData.lerp(data, data, 0.5), data), true);
  });

  testWidgetsWithLeakTracking('Default debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const NavigationBarThemeData().debugFillProperties(builder);

    final List<String> description = builder.properties
        .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
        .map((DiagnosticsNode node) => node.toString())
        .toList();

    expect(description, <String>[]);
  });

  testWidgetsWithLeakTracking('Custom debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const NavigationBarThemeData(
      height: 200.0,
      backgroundColor: Color(0x00000099),
      elevation: 20.0,
      indicatorColor: Color(0x00000098),
      indicatorShape: CircleBorder(),
      labelTextStyle: MaterialStatePropertyAll<TextStyle>(TextStyle(fontSize: 7.0)),
      iconTheme: MaterialStatePropertyAll<IconThemeData>(IconThemeData(color: Color(0x00000097))),
      labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
      overlayColor: MaterialStatePropertyAll<Color>(Color(0x00000096)),
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
        .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
        .map((DiagnosticsNode node) => node.toString())
        .toList();

    expect(description[0], 'height: 200.0');
    expect(description[1], 'backgroundColor: Color(0x00000099)');
    expect(description[2], 'elevation: 20.0');
    expect(description[3], 'indicatorColor: Color(0x00000098)');
    expect(description[4], 'indicatorShape: CircleBorder(BorderSide(width: 0.0, style: none))');
    expect(description[5], 'labelTextStyle: MaterialStatePropertyAll(TextStyle(inherit: true, size: 7.0))');
    // Ignore instance address for IconThemeData.
    expect(description[6].contains('iconTheme: MaterialStatePropertyAll(IconThemeData'), isTrue);
    expect(description[6].contains('(color: Color(0x00000097))'), isTrue);
    expect(description[7], 'labelBehavior: NavigationDestinationLabelBehavior.alwaysHide');
    expect(description[8], 'overlayColor: MaterialStatePropertyAll(Color(0x00000096))');
  });

  testWidgetsWithLeakTracking('NavigationBarThemeData values are used when no NavigationBar properties are specified', (WidgetTester tester) async {
    const double height = 200.0;
    const Color backgroundColor = Color(0x00000001);
    const double elevation = 42.0;
    const Color indicatorColor = Color(0x00000002);
    const ShapeBorder indicatorShape = CircleBorder();
    const double selectedIconSize = 25.0;
    const double unselectedIconSize = 23.0;
    const Color selectedIconColor = Color(0x00000003);
    const Color unselectedIconColor = Color(0x00000004);
    const double selectedIconOpacity = 0.99;
    const double unselectedIconOpacity = 0.98;
    const double selectedLabelFontSize = 13.0;
    const double unselectedLabelFontSize = 11.0;
    const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          bottomNavigationBar: NavigationBarTheme(
            data: NavigationBarThemeData(
              height: height,
              backgroundColor: backgroundColor,
              elevation: elevation,
              indicatorColor: indicatorColor,
              indicatorShape: indicatorShape,
              iconTheme: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
                if (states.contains(MaterialState.selected)) {
                  return const IconThemeData(
                    size: selectedIconSize,
                    color: selectedIconColor,
                    opacity: selectedIconOpacity,
                  );
                }
                return const IconThemeData(
                  size: unselectedIconSize,
                  color: unselectedIconColor,
                  opacity: unselectedIconOpacity,
                );
              }),
              labelTextStyle: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
                if (states.contains(MaterialState.selected)) {
                  return const TextStyle(fontSize: selectedLabelFontSize);
                }
                return const TextStyle(fontSize: unselectedLabelFontSize);
              }),
              labelBehavior: labelBehavior,
            ),
            child: NavigationBar(
              destinations: _destinations(),
            ),
          ),
        ),
      ),
    );

    expect(_barHeight(tester), height);
    expect(_barMaterial(tester).color, backgroundColor);
    expect(_barMaterial(tester).elevation, elevation);
    expect(_indicator(tester)?.color, indicatorColor);
    expect(_indicator(tester)?.shape, indicatorShape);
    expect(_selectedIconTheme(tester).size, selectedIconSize);
    expect(_selectedIconTheme(tester).color, selectedIconColor);
    expect(_selectedIconTheme(tester).opacity, selectedIconOpacity);
    expect(_unselectedIconTheme(tester).size, unselectedIconSize);
    expect(_unselectedIconTheme(tester).color, unselectedIconColor);
    expect(_unselectedIconTheme(tester).opacity, unselectedIconOpacity);
    expect(_selectedLabelStyle(tester).fontSize, selectedLabelFontSize);
    expect(_unselectedLabelStyle(tester).fontSize, unselectedLabelFontSize);
    expect(_labelBehavior(tester), labelBehavior);
  });

  testWidgetsWithLeakTracking('NavigationBar values take priority over NavigationBarThemeData values when both properties are specified', (WidgetTester tester) async {
    const double height = 200.0;
    const Color backgroundColor = Color(0x00000001);
    const double elevation = 42.0;
    const NavigationDestinationLabelBehavior labelBehavior = NavigationDestinationLabelBehavior.alwaysShow;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          bottomNavigationBar: NavigationBarTheme(
            data: const NavigationBarThemeData(
              height: 100.0,
              elevation: 18.0,
              backgroundColor: Color(0x00000099),
              labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
            ),
            child: NavigationBar(
              height: height,
              elevation: elevation,
              backgroundColor: backgroundColor,
              labelBehavior: labelBehavior,
              destinations: _destinations(),
            ),
          ),
        ),
      ),
    );

    expect(_barHeight(tester), height);
    expect(_barMaterial(tester).color, backgroundColor);
    expect(_barMaterial(tester).elevation, elevation);
    expect(_labelBehavior(tester), labelBehavior);
  });

  testWidgetsWithLeakTracking('Custom label style renders ink ripple properly', (WidgetTester tester) async {
    Widget buildWidget({ NavigationDestinationLabelBehavior? labelBehavior }) {
      return MaterialApp(
        theme: ThemeData(
          navigationBarTheme: const NavigationBarThemeData(
            labelTextStyle: MaterialStatePropertyAll<TextStyle>(
              TextStyle(fontSize: 25, color: Color(0xff0000ff)),
            ),
          ),
          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();

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

  testWidgetsWithLeakTracking('NavigationBar respects NavigationBarTheme.overlayColor in active/pressed/hovered states', (WidgetTester tester) async {
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    const Color hoverColor = Color(0xff0000ff);
    const Color focusColor = Color(0xff00ffff);
    const Color pressedColor = Color(0xffff00ff);
    final MaterialStateProperty<Color?> overlayColor = MaterialStateProperty.resolveWith<Color>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.hovered)) {
          return hoverColor;
        }
        if (states.contains(MaterialState.focused)) {
          return focusColor;
        }
        if (states.contains(MaterialState.pressed)) {
          return pressedColor;
        }
        return Colors.transparent;
    });

    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(navigationBarTheme: NavigationBarThemeData(overlayColor: overlayColor)),
      home: Scaffold(
        bottomNavigationBar: RepaintBoundary(
          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 TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(NavigationIndicator).last));
    await tester.pumpAndSettle();

    final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');

    // Test hovered state.
    expect(
      inkFeatures,
      kIsWeb
        ? (paints..rrect()..rrect()..circle(color: hoverColor))
        : (paints..circle(color: hoverColor)),
    );

    await gesture.down(tester.getCenter(find.byType(NavigationIndicator).last));
    await tester.pumpAndSettle();

    // Test pressed state.
    expect(
      inkFeatures,
      kIsWeb
        ? (paints..circle()..circle()..circle(color: pressedColor))
        : (paints..circle()..circle(color: pressedColor)),
    );

    await gesture.up();
    await tester.pumpAndSettle();

    // Press tab to focus the navigation bar.
    await tester.sendKeyEvent(LogicalKeyboardKey.tab);
    await tester.pumpAndSettle();

    // Test focused state.
    expect(
      inkFeatures,
      kIsWeb ? (paints..circle()..circle(color: focusColor)) : (paints..circle()..circle(color: focusColor)),
    );
  });
}

List<NavigationDestination> _destinations() {
  return const <NavigationDestination>[
    NavigationDestination(
      icon: Icon(Icons.favorite_border),
      selectedIcon: Icon(Icons.favorite),
      label: 'Abc',
    ),
    NavigationDestination(
      icon: Icon(Icons.star_border),
      selectedIcon: Icon(Icons.star),
      label: 'Def',
    ),
  ];
}

double _barHeight(WidgetTester tester) {
  return tester.getRect(
    find.byType(NavigationBar),
  ).height;
}

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

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

IconThemeData _selectedIconTheme(WidgetTester tester) {
  return _iconTheme(tester, Icons.favorite);
}

IconThemeData _unselectedIconTheme(WidgetTester tester) {
  return _iconTheme(tester, Icons.star_border);
}

IconThemeData _iconTheme(WidgetTester tester, IconData icon) {
  return tester.firstWidget<IconTheme>(
    find.ancestor(
      of: find.byIcon(icon),
      matching: find.byType(IconTheme),
    ),
  ).data;
}

TextStyle _selectedLabelStyle(WidgetTester tester) {
  return tester.widget<RichText>(
    find.descendant(
      of: find.text('Abc'),
      matching: find.byType(RichText),
    ),
  ).text.style!;
}

TextStyle _unselectedLabelStyle(WidgetTester tester) {
  return tester.widget<RichText>(
    find.descendant(
      of: find.text('Def'),
      matching: find.byType(RichText),
    ),
  ).text.style!;
}

NavigationDestinationLabelBehavior _labelBehavior(WidgetTester tester) {
  if (_opacityAboveLabel('Abc').evaluate().isNotEmpty && _opacityAboveLabel('Def').evaluate().isNotEmpty) {
    return _labelOpacity(tester, 'Abc') == 1
        ? NavigationDestinationLabelBehavior.onlyShowSelected
        : NavigationDestinationLabelBehavior.alwaysHide;
  } else {
    return NavigationDestinationLabelBehavior.alwaysShow;
  }
}

Finder _opacityAboveLabel(String text) {
  return find.ancestor(
    of: find.text(text),
    matching: find.byType(Opacity),
  );
}

// Only valid when labelBehavior != alwaysShow.
double _labelOpacity(WidgetTester tester, String text) {
  final Opacity opacityWidget = tester.widget<Opacity>(
    find.ancestor(
      of: find.text(text),
      matching: find.byType(Opacity),
    ),
  );
  return opacityWidget.opacity;
}
