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

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

class TestIcon extends StatefulWidget {
  const TestIcon({ super.key });

  @override
  TestIconState createState() => TestIconState();
}

class TestIconState extends State<TestIcon> {
  late IconThemeData iconTheme;

  @override
  Widget build(BuildContext context) {
    iconTheme = IconTheme.of(context);
    return const Icon(Icons.add);
  }
}

class TestText extends StatefulWidget {
  const TestText(this.text, { super.key });

  final String text;

  @override
  TestTextState createState() => TestTextState();
}

class TestTextState extends State<TestText> {
  late TextStyle textStyle;

  @override
  Widget build(BuildContext context) {
    textStyle = DefaultTextStyle.of(context).style;
    return Text(widget.text);
  }
}

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

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

  test('ListTileThemeData defaults', () {
    const ListTileThemeData themeData = ListTileThemeData();
    expect(themeData.dense, null);
    expect(themeData.shape, null);
    expect(themeData.style, null);
    expect(themeData.selectedColor, null);
    expect(themeData.iconColor, null);
    expect(themeData.textColor, null);
    expect(themeData.titleTextStyle, null);
    expect(themeData.subtitleTextStyle, null);
    expect(themeData.leadingAndTrailingTextStyle, null);
    expect(themeData.contentPadding, null);
    expect(themeData.tileColor, null);
    expect(themeData.selectedTileColor, null);
    expect(themeData.horizontalTitleGap, null);
    expect(themeData.minVerticalPadding, null);
    expect(themeData.minLeadingWidth, null);
    expect(themeData.enableFeedback, null);
    expect(themeData.mouseCursor, null);
    expect(themeData.visualDensity, null);
    expect(themeData.titleAlignment, null);
  });

  testWidgetsWithLeakTracking('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const ListTileThemeData().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('ListTileThemeData implements debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    const ListTileThemeData(
      dense: true,
      shape: StadiumBorder(),
      style: ListTileStyle.drawer,
      selectedColor: Color(0x00000001),
      iconColor: Color(0x00000002),
      textColor: Color(0x00000003),
      titleTextStyle: TextStyle(color: Color(0x00000004)),
      subtitleTextStyle: TextStyle(color: Color(0x00000005)),
      leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)),
      contentPadding: EdgeInsets.all(100),
      tileColor: Color(0x00000007),
      selectedTileColor: Color(0x00000008),
      horizontalTitleGap: 200,
      minVerticalPadding: 300,
      minLeadingWidth: 400,
      enableFeedback: true,
      mouseCursor: MaterialStateMouseCursor.clickable,
      visualDensity: VisualDensity.comfortable,
      titleAlignment: ListTileTitleAlignment.top,
    ).debugFillProperties(builder);

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

    expect(
      description,
      equalsIgnoringHashCodes(<String>[
        'dense: true',
        'shape: StadiumBorder(BorderSide(width: 0.0, style: none))',
        'style: drawer',
        'selectedColor: Color(0x00000001)',
        'iconColor: Color(0x00000002)',
        'textColor: Color(0x00000003)',
        'titleTextStyle: TextStyle(inherit: true, color: Color(0x00000004))',
        'subtitleTextStyle: TextStyle(inherit: true, color: Color(0x00000005))',
        'leadingAndTrailingTextStyle: TextStyle(inherit: true, color: Color(0x00000006))',
        'contentPadding: EdgeInsets.all(100.0)',
        'tileColor: Color(0x00000007)',
        'selectedTileColor: Color(0x00000008)',
        'horizontalTitleGap: 200.0',
        'minVerticalPadding: 300.0',
        'minLeadingWidth: 400.0',
        'enableFeedback: true',
        'mouseCursor: MaterialStateMouseCursor(clickable)',
        'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)',
        'titleAlignment: ListTileTitleAlignment.top',
      ]),
    );
  });

  testWidgetsWithLeakTracking('ListTileTheme backwards compatibility constructor', (WidgetTester tester) async {
    late ListTileThemeData theme;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: ListTileTheme(
            dense: true,
            shape: const StadiumBorder(),
            style: ListTileStyle.drawer,
            selectedColor: const Color(0x00000001),
            iconColor: const Color(0x00000002),
            textColor: const Color(0x00000003),
            contentPadding: const EdgeInsets.all(100),
            tileColor: const Color(0x00000004),
            selectedTileColor: const Color(0x00000005),
            horizontalTitleGap: 200,
            minVerticalPadding: 300,
            minLeadingWidth: 400,
            enableFeedback: true,
            mouseCursor: MaterialStateMouseCursor.clickable,
            child: Center(
              child: Builder(
                builder: (BuildContext context) {
                  theme = ListTileTheme.of(context);
                  return const Placeholder();
                },
              ),
            ),
          ),
        ),
      ),
    );

    expect(theme.dense, true);
    expect(theme.shape, const StadiumBorder());
    expect(theme.style, ListTileStyle.drawer);
    expect(theme.selectedColor, const Color(0x00000001));
    expect(theme.iconColor, const Color(0x00000002));
    expect(theme.textColor, const Color(0x00000003));
    expect(theme.contentPadding, const EdgeInsets.all(100));
    expect(theme.tileColor, const Color(0x00000004));
    expect(theme.selectedTileColor, const Color(0x00000005));
    expect(theme.horizontalTitleGap, 200);
    expect(theme.minVerticalPadding, 300);
    expect(theme.minLeadingWidth, 400);
    expect(theme.enableFeedback, true);
    expect(theme.mouseCursor, MaterialStateMouseCursor.clickable);
  });

  testWidgetsWithLeakTracking('ListTileTheme', (WidgetTester tester) async {
    final Key listTileKey = UniqueKey();
    final Key titleKey = UniqueKey();
    final Key subtitleKey = UniqueKey();
    final Key leadingKey = UniqueKey();
    final Key trailingKey = UniqueKey();
    late ThemeData theme;

    Widget buildFrame({
      bool enabled = true,
      bool dense = false,
      bool selected = false,
      ShapeBorder? shape,
      Color? selectedColor,
      Color? iconColor,
      Color? textColor,
    }) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Material(
          child: Center(
            child: ListTileTheme(
              data: ListTileThemeData(
                dense: dense,
                shape: shape,
                selectedColor: selectedColor,
                iconColor: iconColor,
                textColor: textColor,
                minVerticalPadding: 25.0,
                mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
                  if (states.contains(MaterialState.disabled)) {
                    return SystemMouseCursors.forbidden;
                  }

                  return SystemMouseCursors.click;
                }),
                visualDensity: VisualDensity.compact,
                titleAlignment: ListTileTitleAlignment.bottom,
              ),
              child: Builder(
                builder: (BuildContext context) {
                  theme = Theme.of(context);
                  return ListTile(
                    key: listTileKey,
                    enabled: enabled,
                    selected: selected,
                    leading: TestIcon(key: leadingKey),
                    trailing: TestIcon(key: trailingKey),
                    title: TestText('title', key: titleKey),
                    subtitle: TestText('subtitle', key: subtitleKey),
                  );
                },
              ),
            ),
          ),
        ),
      );
    }

    const Color green = Color(0xFF00FF00);
    const Color red = Color(0xFFFF0000);
    const ShapeBorder roundedShape = RoundedRectangleBorder(
      borderRadius: BorderRadius.all(Radius.circular(4.0)),
    );

    Color iconColor(Key key) => tester.state<TestIconState>(find.byKey(key)).iconTheme.color!;
    Color textColor(Key key) => tester.state<TestTextState>(find.byKey(key)).textStyle.color!;
    ShapeBorder inkWellBorder() => tester.widget<InkWell>(find.descendant(of: find.byType(ListTile), matching: find.byType(InkWell))).customBorder!;

    // A selected ListTile's leading, trailing, and text get the primary color by default
    await tester.pumpWidget(buildFrame(selected: true));
    await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate
    expect(iconColor(leadingKey), theme.primaryColor);
    expect(iconColor(trailingKey), theme.primaryColor);
    expect(textColor(titleKey), theme.primaryColor);
    expect(textColor(subtitleKey), theme.primaryColor);

    // A selected ListTile's leading, trailing, and text get the ListTileTheme's selectedColor
    await tester.pumpWidget(buildFrame(selected: true, selectedColor: green));
    await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate
    expect(iconColor(leadingKey), green);
    expect(iconColor(trailingKey), green);
    expect(textColor(titleKey), green);
    expect(textColor(subtitleKey), green);

    // An unselected ListTile's leading and trailing get the ListTileTheme's iconColor
    // An unselected ListTile's title texts get the ListTileTheme's textColor
    await tester.pumpWidget(buildFrame(iconColor: red, textColor: green));
    await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate
    expect(iconColor(leadingKey), red);
    expect(iconColor(trailingKey), red);
    expect(textColor(titleKey), green);
    expect(textColor(subtitleKey), green);

    // If the item is disabled it's rendered with the theme's disabled color.
    await tester.pumpWidget(buildFrame(enabled: false));
    await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate
    expect(iconColor(leadingKey), theme.disabledColor);
    expect(iconColor(trailingKey), theme.disabledColor);
    expect(textColor(titleKey), theme.disabledColor);
    expect(textColor(subtitleKey), theme.disabledColor);

    // If the item is disabled it's rendered with the theme's disabled color.
    // Even if it's selected.
    await tester.pumpWidget(buildFrame(enabled: false, selected: true));
    await tester.pump(const Duration(milliseconds: 300)); // DefaultTextStyle changes animate
    expect(iconColor(leadingKey), theme.disabledColor);
    expect(iconColor(trailingKey), theme.disabledColor);
    expect(textColor(titleKey), theme.disabledColor);
    expect(textColor(subtitleKey), theme.disabledColor);

    // A selected ListTile's InkWell gets the ListTileTheme's shape
    await tester.pumpWidget(buildFrame(selected: true, shape: roundedShape));
    expect(inkWellBorder(), roundedShape);

    // Cursor updates when hovering disabled ListTile
    await tester.pumpWidget(buildFrame(enabled: false));
    final Offset listTile = tester.getCenter(find.byKey(titleKey));
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(listTile);
    await tester.pumpAndSettle();
    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);

    // VisualDensity is respected
    final RenderBox box = tester.renderObject(find.byKey(listTileKey));
    expect(box.size, equals(const Size(800, 80.0)));

    // titleAlignment is respected.
    final Offset titleOffset = tester.getTopLeft(find.text('title'));
    final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
    final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
    expect(leadingOffset.dy - titleOffset.dy, 6);
    expect(trailingOffset.dy - titleOffset.dy, 6);
  });

  testWidgetsWithLeakTracking('ListTileTheme colors are applied to leading and trailing text widgets', (WidgetTester tester) async {
    final Key leadingKey = UniqueKey();
    final Key trailingKey = UniqueKey();

    const Color selectedColor = Colors.orange;
    const Color defaultColor = Colors.black;

    late ThemeData theme;
    Widget buildFrame({
      bool enabled = true,
      bool selected = false,
    }) {
      return MaterialApp(
        home: Material(
          child: Center(
            child: ListTileTheme(
              data: const ListTileThemeData(
                selectedColor: selectedColor,
                textColor: defaultColor,
              ),
              child: Builder(
                builder: (BuildContext context) {
                  theme = Theme.of(context);
                  return ListTile(
                    enabled: enabled,
                    selected: selected,
                    leading: TestText('leading', key: leadingKey),
                    title: const TestText('title'),
                    trailing: TestText('trailing', key: trailingKey),
                  );
                },
              ),
            ),
          ),
        ),
      );
    }

    Color textColor(Key key) => tester.state<TestTextState>(find.byKey(key)).textStyle.color!;

    await tester.pumpWidget(buildFrame());
    // Enabled color should use ListTileTheme.textColor.
    expect(textColor(leadingKey), defaultColor);
    expect(textColor(trailingKey), defaultColor);

    await tester.pumpWidget(buildFrame(selected: true));
    // Wait for text color to animate.
    await tester.pumpAndSettle();
    // Selected color should use ListTileTheme.selectedColor.
    expect(textColor(leadingKey), selectedColor);
    expect(textColor(trailingKey), selectedColor);

    await tester.pumpWidget(buildFrame(enabled: false));
    // Wait for text color to animate.
    await tester.pumpAndSettle();
    // Disabled color should be ThemeData.disabledColor.
    expect(textColor(leadingKey), theme.disabledColor);
    expect(textColor(trailingKey), theme.disabledColor);
  });

  testWidgetsWithLeakTracking(
    "Material3 - ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle",
    (WidgetTester tester) async {
      const TextStyle titleTextStyle = TextStyle(
        fontSize: 23.0,
        color: Color(0xffff0000),
        fontStyle: FontStyle.italic,
      );
      const TextStyle subtitleTextStyle = TextStyle(
        fontSize: 20.0,
        color: Color(0xff00ff00),
        fontStyle: FontStyle.italic,
      );
      const TextStyle leadingAndTrailingTextStyle = TextStyle(
        fontSize: 18.0,
        color: Color(0xff0000ff),
        fontStyle: FontStyle.italic,
      );

    final ThemeData theme = ThemeData(
        useMaterial3: true,
        listTileTheme: const ListTileThemeData(
        titleTextStyle: titleTextStyle,
        subtitleTextStyle: subtitleTextStyle,
        leadingAndTrailingTextStyle: leadingAndTrailingTextStyle,
      ),
    );

    Widget buildFrame() {
      return MaterialApp(
        theme: theme,
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return const ListTile(
                  leading: TestText('leading'),
                  title: TestText('title'),
                  subtitle: TestText('subtitle'),
                  trailing: TestText('trailing'),
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());
    final RenderParagraph leading = _getTextRenderObject(tester, 'leading');
    expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
    expect(leading.text.style!.color, leadingAndTrailingTextStyle.color);
    expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
    final RenderParagraph title = _getTextRenderObject(tester, 'title');
    expect(title.text.style!.fontSize, titleTextStyle.fontSize);
    expect(title.text.style!.color, titleTextStyle.color);
    expect(title.text.style!.fontStyle, titleTextStyle.fontStyle);
    final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle');
    expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize);
    expect(subtitle.text.style!.color, subtitleTextStyle.color);
    expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle);
    final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing');
    expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
    expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color);
    expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
  });

  testWidgetsWithLeakTracking(
    "Material2 - ListTile respects ListTileTheme's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle",
    (WidgetTester tester) async {
      const TextStyle titleTextStyle = TextStyle(
        fontSize: 23.0,
        color: Color(0xffff0000),
        fontStyle: FontStyle.italic,
      );
      const TextStyle subtitleTextStyle = TextStyle(
        fontSize: 20.0,
        color: Color(0xff00ff00),
        fontStyle: FontStyle.italic,
      );
      const TextStyle leadingAndTrailingTextStyle = TextStyle(
        fontSize: 18.0,
        color: Color(0xff0000ff),
        fontStyle: FontStyle.italic,
      );

    final ThemeData theme = ThemeData(
        useMaterial3: false,
        listTileTheme: const ListTileThemeData(
        titleTextStyle: titleTextStyle,
        subtitleTextStyle: subtitleTextStyle,
        leadingAndTrailingTextStyle: leadingAndTrailingTextStyle,
      ),
    );

    Widget buildFrame() {
      return MaterialApp(
        theme: theme,
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return const ListTile(
                  leading: TestText('leading'),
                  title: TestText('title'),
                  subtitle: TestText('subtitle'),
                  trailing: TestText('trailing'),
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());
    final RenderParagraph leading = _getTextRenderObject(tester, 'leading');
    expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
    expect(leading.text.style!.color, leadingAndTrailingTextStyle.color);
    expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
    final RenderParagraph title = _getTextRenderObject(tester, 'title');
    expect(title.text.style!.fontSize, titleTextStyle.fontSize);
    expect(title.text.style!.color, titleTextStyle.color);
    expect(title.text.style!.fontStyle, titleTextStyle.fontStyle);
    final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle');
    expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize);
    expect(subtitle.text.style!.color, subtitleTextStyle.color);
    expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle);
    final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing');
    expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
    expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color);
    expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
  });

  testWidgetsWithLeakTracking(
    "Material3 - ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties",
    (WidgetTester tester) async {
      final ThemeData theme = ThemeData(
        useMaterial3: true,
        listTileTheme: const ListTileThemeData(
          titleTextStyle: TextStyle(fontSize: 20.0),
          subtitleTextStyle: TextStyle(fontSize: 17.5),
          leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0),
        ),
      );
      const TextStyle titleTextStyle = TextStyle(
        fontSize: 23.0,
        color: Color(0xffff0000),
        fontStyle: FontStyle.italic,
      );
      const TextStyle subtitleTextStyle = TextStyle(
        fontSize: 20.0,
        color: Color(0xff00ff00),
        fontStyle: FontStyle.italic,
      );
      const TextStyle leadingAndTrailingTextStyle = TextStyle(
        fontSize: 18.0,
        color: Color(0xff0000ff),
        fontStyle: FontStyle.italic,
      );

      Widget buildFrame() {
        return MaterialApp(
          theme: theme,
          home: Material(
            child: Center(
              child: Builder(
                builder: (BuildContext context) {
                  return const ListTile(
                    titleTextStyle: titleTextStyle,
                    subtitleTextStyle: subtitleTextStyle,
                    leadingAndTrailingTextStyle: leadingAndTrailingTextStyle,
                    leading: TestText('leading'),
                    title: TestText('title'),
                    subtitle: TestText('subtitle'),
                    trailing: TestText('trailing'),
                  );
                },
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildFrame());
      final RenderParagraph leading = _getTextRenderObject(tester, 'leading');
      expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
      expect(leading.text.style!.color, leadingAndTrailingTextStyle.color);
      expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
      final RenderParagraph title = _getTextRenderObject(tester, 'title');
      expect(title.text.style!.fontSize, titleTextStyle.fontSize);
      expect(title.text.style!.color, titleTextStyle.color);
      expect(title.text.style!.fontStyle, titleTextStyle.fontStyle);
      final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle');
      expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize);
      expect(subtitle.text.style!.color, subtitleTextStyle.color);
      expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle);
      final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing');
      expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
      expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color);
      expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
  });

  testWidgetsWithLeakTracking(
    "Material2 - ListTile's titleTextStyle, subtitleTextStyle & leadingAndTrailingTextStyle are overridden by ListTile properties",
    (WidgetTester tester) async {
      final ThemeData theme = ThemeData(
        useMaterial3: false,
        listTileTheme: const ListTileThemeData(
          titleTextStyle: TextStyle(fontSize: 20.0),
          subtitleTextStyle: TextStyle(fontSize: 17.5),
          leadingAndTrailingTextStyle: TextStyle(fontSize: 15.0),
        ),
      );
      const TextStyle titleTextStyle = TextStyle(
        fontSize: 23.0,
        color: Color(0xffff0000),
        fontStyle: FontStyle.italic,
      );
      const TextStyle subtitleTextStyle = TextStyle(
        fontSize: 20.0,
        color: Color(0xff00ff00),
        fontStyle: FontStyle.italic,
      );
      const TextStyle leadingAndTrailingTextStyle = TextStyle(
        fontSize: 18.0,
        color: Color(0xff0000ff),
        fontStyle: FontStyle.italic,
      );

      Widget buildFrame() {
        return MaterialApp(
          theme: theme,
          home: Material(
            child: Center(
              child: Builder(
                builder: (BuildContext context) {
                  return const ListTile(
                    titleTextStyle: titleTextStyle,
                    subtitleTextStyle: subtitleTextStyle,
                    leadingAndTrailingTextStyle: leadingAndTrailingTextStyle,
                    leading: TestText('leading'),
                    title: TestText('title'),
                    subtitle: TestText('subtitle'),
                    trailing: TestText('trailing'),
                  );
                },
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildFrame());
      final RenderParagraph leading = _getTextRenderObject(tester, 'leading');
      expect(leading.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
      expect(leading.text.style!.color, leadingAndTrailingTextStyle.color);
      expect(leading.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
      final RenderParagraph title = _getTextRenderObject(tester, 'title');
      expect(title.text.style!.fontSize, titleTextStyle.fontSize);
      expect(title.text.style!.color, titleTextStyle.color);
      expect(title.text.style!.fontStyle, titleTextStyle.fontStyle);
      final RenderParagraph subtitle = _getTextRenderObject(tester, 'subtitle');
      expect(subtitle.text.style!.fontSize, subtitleTextStyle.fontSize);
      expect(subtitle.text.style!.color, subtitleTextStyle.color);
      expect(subtitle.text.style!.fontStyle, subtitleTextStyle.fontStyle);
      final RenderParagraph trailing = _getTextRenderObject(tester, 'trailing');
      expect(trailing.text.style!.fontSize, leadingAndTrailingTextStyle.fontSize);
      expect(trailing.text.style!.color, leadingAndTrailingTextStyle.color);
      expect(trailing.text.style!.fontStyle, leadingAndTrailingTextStyle.fontStyle);
  });

  testWidgetsWithLeakTracking("ListTile respects ListTileTheme's tileColor & selectedTileColor", (WidgetTester tester) async {
    late ListTileThemeData theme;
    bool isSelected = false;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: ListTileTheme(
            data: ListTileThemeData(
              tileColor: Colors.green.shade500,
              selectedTileColor: Colors.red.shade500,
            ),
            child: Center(
              child: StatefulBuilder(
                builder: (BuildContext context, StateSetter setState) {
                  theme = ListTileTheme.of(context);
                  return ListTile(
                    selected: isSelected,
                    onTap: () {
                      setState(()=> isSelected = !isSelected);
                    },
                    title: const Text('Title'),
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );

    expect(find.byType(Material), paints..rect(color: theme.tileColor));

    // Tap on tile to change isSelected.
    await tester.tap(find.byType(ListTile));
    await tester.pumpAndSettle();

    expect(find.byType(Material), paints..rect(color: theme.selectedTileColor));
  });

  testWidgetsWithLeakTracking("ListTileTheme's tileColor & selectedTileColor are overridden by ListTile properties", (WidgetTester tester) async {
    bool isSelected = false;
    final Color tileColor = Colors.green.shade500;
    final Color selectedTileColor = Colors.red.shade500;

    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: ListTileTheme(
            data: const ListTileThemeData(
              selectedTileColor: Colors.green,
              tileColor: Colors.red,
            ),
            child: Center(
              child: StatefulBuilder(
                builder: (BuildContext context, StateSetter setState) {
                  return ListTile(
                    tileColor: tileColor,
                    selectedTileColor: selectedTileColor,
                    selected: isSelected,
                    onTap: () {
                      setState(()=> isSelected = !isSelected);
                    },
                    title: const Text('Title'),
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );

    expect(find.byType(Material), paints..rect(color: tileColor));

    // Tap on tile to change isSelected.
    await tester.tap(find.byType(ListTile));
    await tester.pumpAndSettle();

    expect(find.byType(Material), paints..rect(color: selectedTileColor));
  });

  testWidgetsWithLeakTracking('ListTile uses ListTileTheme shape in a drawer', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/106303

    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
    final ShapeBorder shapeBorder =  RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0));

    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(
        listTileTheme: ListTileThemeData(shape: shapeBorder),
      ),
      home: Scaffold(
        key: scaffoldKey,
        drawer: const Drawer(
          child: ListTile(),
        ),
        body: Container(),
      ),
    ));
    await tester.pumpAndSettle();

    scaffoldKey.currentState!.openDrawer();
    // Start drawer animation.
    await tester.pump();

    final ShapeBorder? inkWellBorder = tester.widget<InkWell>(
      find.descendant(
        of: find.byType(ListTile),
        matching: find.byType(InkWell),
    )).customBorder;
    // Test shape.
    expect(inkWellBorder, shapeBorder);
  });

  testWidgetsWithLeakTracking('ListTile respects MaterialStateColor LisTileTheme.textColor', (WidgetTester tester) async {
    bool enabled = false;
    bool selected = false;
    const Color defaultColor = Colors.blue;
    const Color selectedColor = Colors.green;
    const Color disabledColor = Colors.red;

    final ThemeData theme = ThemeData(
      listTileTheme: ListTileThemeData(
        textColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
          if (states.contains(MaterialState.disabled)) {
            return disabledColor;
          }
          if (states.contains(MaterialState.selected)) {
            return selectedColor;
          }
          return defaultColor;
        }),
      ),
    );
    Widget buildFrame() {
      return MaterialApp(
        theme: theme,
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return ListTile(
                  enabled: enabled,
                  selected: selected,
                  title: const TestText('title'),
                  subtitle: const TestText('subtitle') ,
                );
              },
            ),
          ),
        ),
      );
    }

    // Test disabled state.
    await tester.pumpWidget(buildFrame());
    RenderParagraph title = _getTextRenderObject(tester, 'title');
    expect(title.text.style!.color, disabledColor);

    // Test enabled state.
    enabled = true;
    await tester.pumpWidget(buildFrame());
    await tester.pumpAndSettle();
    title = _getTextRenderObject(tester, 'title');
    expect(title.text.style!.color, defaultColor);

    // Test selected state.
    selected = true;
    await tester.pumpWidget(buildFrame());
    await tester.pumpAndSettle();
    title = _getTextRenderObject(tester, 'title');
    expect(title.text.style!.color, selectedColor);
  });

  testWidgetsWithLeakTracking('ListTile respects MaterialStateColor LisTileTheme.iconColor', (WidgetTester tester) async {
    bool enabled = false;
    bool selected = false;
    const Color defaultColor = Colors.blue;
    const Color selectedColor = Colors.green;
    const Color disabledColor = Colors.red;
    final Key leadingKey = UniqueKey();

    final ThemeData theme = ThemeData(
      listTileTheme: ListTileThemeData(
        iconColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
          if (states.contains(MaterialState.disabled)) {
            return disabledColor;
          }
          if (states.contains(MaterialState.selected)) {
            return selectedColor;
          }
          return defaultColor;
        }),
      ),
    );
    Widget buildFrame() {
      return MaterialApp(
        theme: theme,
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return ListTile(
                  enabled: enabled,
                  selected: selected,
                  leading: TestIcon(key: leadingKey),
                );
              },
            ),
          ),
        ),
      );
    }

    Color iconColor(Key key) => tester.state<TestIconState>(find.byKey(key)).iconTheme.color!;

    // Test disabled state.
    await tester.pumpWidget(buildFrame());
    expect(iconColor(leadingKey), disabledColor);

    // Test enabled state.
    enabled = true;
    await tester.pumpWidget(buildFrame());
    await tester.pumpAndSettle();
    expect(iconColor(leadingKey), defaultColor);

    // Test selected state.
    selected = true;
    await tester.pumpWidget(buildFrame());
    await tester.pumpAndSettle();
    expect(iconColor(leadingKey), selectedColor);
  });

  testWidgetsWithLeakTracking('ListTileThemeData copyWith overrides all properties', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/119734

    const ListTileThemeData original = ListTileThemeData(
      dense: true,
      shape: StadiumBorder(),
      style: ListTileStyle.drawer,
      selectedColor: Color(0x00000001),
      iconColor: Color(0x00000002),
      textColor: Color(0x00000003),
      titleTextStyle: TextStyle(color: Color(0x00000004)),
      subtitleTextStyle: TextStyle(color: Color(0x00000005)),
      leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)),
      contentPadding: EdgeInsets.all(100),
      tileColor: Color(0x00000007),
      selectedTileColor: Color(0x00000008),
      horizontalTitleGap: 200,
      minVerticalPadding: 300,
      minLeadingWidth: 400,
      enableFeedback: true,
      titleAlignment: ListTileTitleAlignment.bottom,
    );

    final ListTileThemeData copy = original.copyWith(
      dense: false,
      shape: const RoundedRectangleBorder(),
      style: ListTileStyle.list,
      selectedColor: const Color(0x00000009),
      iconColor: const Color(0x0000000A),
      textColor: const Color(0x0000000B),
      titleTextStyle: const TextStyle(color: Color(0x0000000C)),
      subtitleTextStyle: const TextStyle(color: Color(0x0000000D)),
      leadingAndTrailingTextStyle: const TextStyle(color: Color(0x0000000E)),
      contentPadding: const EdgeInsets.all(500),
      tileColor: const Color(0x0000000F),
      selectedTileColor: const Color(0x00000010),
      horizontalTitleGap: 600,
      minVerticalPadding: 700,
      minLeadingWidth: 800,
      enableFeedback: false,
      titleAlignment: ListTileTitleAlignment.top,
    );

    expect(copy.dense, false);
    expect(copy.shape, const RoundedRectangleBorder());
    expect(copy.style, ListTileStyle.list);
    expect(copy.selectedColor, const Color(0x00000009));
    expect(copy.iconColor, const Color(0x0000000A));
    expect(copy.textColor, const Color(0x0000000B));
    expect(copy.titleTextStyle, const TextStyle(color: Color(0x0000000C)));
    expect(copy.subtitleTextStyle, const TextStyle(color: Color(0x0000000D)));
    expect(copy.leadingAndTrailingTextStyle, const TextStyle(color: Color(0x0000000E)));
    expect(copy.contentPadding, const EdgeInsets.all(500));
    expect(copy.tileColor, const Color(0x0000000F));
    expect(copy.selectedTileColor, const Color(0x00000010));
    expect(copy.horizontalTitleGap, 600);
    expect(copy.minVerticalPadding, 700);
    expect(copy.minLeadingWidth, 800);
    expect(copy.enableFeedback, false);
    expect(copy.titleAlignment, ListTileTitleAlignment.top);
  });

  testWidgetsWithLeakTracking('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', (WidgetTester tester) async {
    final Key leadingKey = GlobalKey();
    final Key trailingKey = GlobalKey();
    const String titleText = '\nHeadline Text\n';
    const String subtitleText = '\nSupporting Text\n';

    Widget buildFrame({ ListTileTitleAlignment? alignment }) {
      return MaterialApp(
        theme: ThemeData(
          useMaterial3: true,
          listTileTheme: const ListTileThemeData(
            titleAlignment: ListTileTitleAlignment.center,
          ),
        ),
        home: Material(
          child: Center(
            child: ListTile(
              titleAlignment: ListTileTitleAlignment.top,
              leading: SizedBox(key: leadingKey, width: 24.0, height: 24.0),
              title: const Text(titleText),
              subtitle: const Text(subtitleText),
              trailing: SizedBox(key: trailingKey, width: 24.0, height: 24.0),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());
    final Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
    final Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
    final Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
    expect(leadingOffset.dy - tileOffset.dy, 8.0);
    expect(trailingOffset.dy - tileOffset.dy, 8.0);
  });

  testWidgetsWithLeakTracking('ListTileTheme.merge supports all properties', (WidgetTester tester) async {
    Widget buildFrame() {
      return MaterialApp(
        theme: ThemeData(
          listTileTheme: const ListTileThemeData(
            dense: true,
            shape: StadiumBorder(),
            style: ListTileStyle.drawer,
            selectedColor: Color(0x00000001),
            iconColor: Color(0x00000002),
            textColor: Color(0x00000003),
            titleTextStyle: TextStyle(color: Color(0x00000004)),
            subtitleTextStyle: TextStyle(color: Color(0x00000005)),
            leadingAndTrailingTextStyle: TextStyle(color: Color(0x00000006)),
            contentPadding: EdgeInsets.all(100),
            tileColor: Color(0x00000007),
            selectedTileColor: Color(0x00000008),
            horizontalTitleGap: 200,
            minVerticalPadding: 300,
            minLeadingWidth: 400,
            enableFeedback: true,
            titleAlignment: ListTileTitleAlignment.bottom,
            mouseCursor: MaterialStateMouseCursor.textable,
            visualDensity: VisualDensity.comfortable,
          ),
        ),
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return ListTileTheme.merge(
                  dense: false,
                  shape: const RoundedRectangleBorder(),
                  style: ListTileStyle.list,
                  selectedColor: const Color(0x00000009),
                  iconColor: const Color(0x0000000A),
                  textColor: const Color(0x0000000B),
                  titleTextStyle: const TextStyle(color: Color(0x0000000C)),
                  subtitleTextStyle: const TextStyle(color: Color(0x0000000D)),
                  leadingAndTrailingTextStyle: const TextStyle(color: Color(0x0000000E)),
                  contentPadding: const EdgeInsets.all(500),
                  tileColor: const Color(0x0000000F),
                  selectedTileColor: const Color(0x00000010),
                  horizontalTitleGap: 600,
                  minVerticalPadding: 700,
                  minLeadingWidth: 800,
                  enableFeedback: false,
                  titleAlignment: ListTileTitleAlignment.top,
                  mouseCursor: MaterialStateMouseCursor.clickable,
                  visualDensity: VisualDensity.compact,
                  child: const ListTile(),
                );
              }
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());
    final ListTileThemeData theme = ListTileTheme.of(tester.element(find.byType(ListTile)));
    expect(theme.dense, false);
    expect(theme.shape, const RoundedRectangleBorder());
    expect(theme.style, ListTileStyle.list);
    expect(theme.selectedColor, const Color(0x00000009));
    expect(theme.iconColor, const Color(0x0000000A));
    expect(theme.textColor, const Color(0x0000000B));
    expect(theme.titleTextStyle, const TextStyle(color: Color(0x0000000C)));
    expect(theme.subtitleTextStyle, const TextStyle(color: Color(0x0000000D)));
    expect(theme.leadingAndTrailingTextStyle, const TextStyle(color: Color(0x0000000E)));
    expect(theme.contentPadding, const EdgeInsets.all(500));
    expect(theme.tileColor, const Color(0x0000000F));
    expect(theme.selectedTileColor, const Color(0x00000010));
    expect(theme.horizontalTitleGap, 600);
    expect(theme.minVerticalPadding, 700);
    expect(theme.minLeadingWidth, 800);
    expect(theme.enableFeedback, false);
    expect(theme.titleAlignment, ListTileTitleAlignment.top);
    expect(theme.mouseCursor, MaterialStateMouseCursor.clickable);
    expect(theme.visualDensity, VisualDensity.compact);
  });
}

RenderParagraph _getTextRenderObject(WidgetTester tester, String text) {
  return tester.renderObject(find.descendant(
    of: find.byType(ListTile),
    matching: find.text(text),
  ));
}
