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

import 'package:flutter/cupertino.dart';
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';
import '../widgets/semantics_tester.dart';

void main() {
  final ThemeData theme = ThemeData();

  testWidgetsWithLeakTracking('Switch can toggle on tap', (WidgetTester tester) async {
    final Key switchKey = UniqueKey();
    bool value = false;

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return MaterialApp(
              theme: theme,
              home: Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    key: switchKey,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );

    expect(value, isFalse);
    await tester.tap(find.byKey(switchKey));
    expect(value, isTrue);
  });

  testWidgetsWithLeakTracking('Switch size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
    final bool material3 = theme.useMaterial3;
    await tester.pumpWidget(
      Theme(
        data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.padded),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Material(
            child: Center(
              child: Switch(
                dragStartBehavior: DragStartBehavior.down,
                value: true,
                onChanged: (bool newValue) { },
              ),
            ),
          ),
        ),
      ),
    );

    // switch width = trackWidth - 2 * trackRadius + _kSwitchMinSize
    // M2 width = 33 - 2 * 7 + 40
    // M3 width = 52 - 2 * 16 + 40
    expect(tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 48.0) : const Size(59.0, 48.0));

    await tester.pumpWidget(
      Theme(
        data: theme.copyWith(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Material(
            child: Center(
              child: Switch(
                dragStartBehavior: DragStartBehavior.down,
                value: true,
                onChanged: (bool newValue) { },
              ),
            ),
          ),
        ),
      ),
    );

    expect(tester.getSize(find.byType(Switch)), material3 ? const Size(60.0, 40.0) : const Size(59.0, 40.0));
  });

  testWidgetsWithLeakTracking('Material2 - Switch does not get distorted upon changing constraints with parent', (WidgetTester tester) async {
    const double maxWidth = 300;
    const double maxHeight = 100;

    const ValueKey<String> boundaryKey = ValueKey<String>('switch container');

    Widget buildSwitch({required double width, required double height}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Scaffold(
          body: Directionality(
            textDirection: TextDirection.ltr,
            child: SizedBox(
              width: maxWidth,
              height: maxHeight,
              child: RepaintBoundary(
                key: boundaryKey,
                child: SizedBox(
                  width: width,
                  height: height,
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: true,
                    onChanged: (_) {},
                  ),
                ),
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(
      width: maxWidth,
      height: maxHeight,
    ));
    await expectLater(
      find.byKey(boundaryKey),
      matchesGoldenFile('m2_switch_test.big.on.png'),
    );

    await tester.pumpWidget(buildSwitch(
      width: 20,
      height: 10,
    ));
    await expectLater(
      find.byKey(boundaryKey),
      matchesGoldenFile('m2_switch_test.small.on.png'),
    );
  });

  testWidgetsWithLeakTracking('Material3 - Switch does not get distorted upon changing constraints with parent', (WidgetTester tester) async {
    const double maxWidth = 300;
    const double maxHeight = 100;

    const ValueKey<String> boundaryKey = ValueKey<String>('switch container');

    Widget buildSwitch({required double width, required double height}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: Scaffold(
          body: Directionality(
            textDirection: TextDirection.ltr,
            child: SizedBox(
              width: maxWidth,
              height: maxHeight,
              child: RepaintBoundary(
                key: boundaryKey,
                child: SizedBox(
                  width: width,
                  height: height,
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: true,
                    onChanged: (_) {},
                  ),
                ),
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(
      width: maxWidth,
      height: maxHeight,
    ));
    await expectLater(
      find.byKey(boundaryKey),
      matchesGoldenFile('m3_switch_test.big.on.png'),
    );

    await tester.pumpWidget(buildSwitch(
      width: 20,
      height: 10,
    ));
    await expectLater(
      find.byKey(boundaryKey),
      matchesGoldenFile('m3_switch_test.small.on.png'),
    );
  });

  testWidgetsWithLeakTracking('Switch can drag (LTR)', (WidgetTester tester) async {
    bool value = false;

    await tester.pumpWidget(
      Theme(
        data: theme,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(value, isFalse);

    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));

    expect(value, isFalse);

    await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));

    expect(value, isTrue);

    await tester.pump();
    await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));

    expect(value, isTrue);

    await tester.pump();
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));

    expect(value, isFalse);
  });

  testWidgetsWithLeakTracking('Switch can drag with dragStartBehavior', (WidgetTester tester) async {
    bool value = false;

    await tester.pumpWidget(
      Theme(
        data: theme,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(value, isFalse);
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
    expect(value, isFalse);

    await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));
    expect(value, isTrue);
    await tester.pump();
    await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));
    expect(value, isTrue);
    await tester.pump();
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
    expect(value, isFalse);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Material(
              child: Center(
                child: Switch(
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                ),
              ),
            );
          },
        ),
      ),
    );
    await tester.pumpAndSettle();
    final Rect switchRect = tester.getRect(find.byType(Switch));

    TestGesture gesture = await tester.startGesture(switchRect.center);
    // We have to execute the drag in two frames because the first update will
    // just set the start position.
    await gesture.moveBy(const Offset(20.0, 0.0));
    await gesture.moveBy(const Offset(20.0, 0.0));
    expect(value, isFalse);
    await gesture.up();
    expect(value, isTrue);
    await tester.pump();

    gesture = await tester.startGesture(switchRect.center);
    await gesture.moveBy(const Offset(20.0, 0.0));
    await gesture.moveBy(const Offset(20.0, 0.0));
    expect(value, isTrue);
    await gesture.up();
    expect(value, isTrue);
    await tester.pump();

    gesture = await tester.startGesture(switchRect.center);
    await gesture.moveBy(const Offset(-20.0, 0.0));
    await gesture.moveBy(const Offset(-20.0, 0.0));
    expect(value, isTrue);
    await gesture.up();
    expect(value, isFalse);
  });

  testWidgetsWithLeakTracking('Switch can drag (RTL)', (WidgetTester tester) async {
    bool value = false;

    await tester.pumpWidget(
      Theme(
        data: theme,
        child: Directionality(
          textDirection: TextDirection.rtl,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));

    expect(value, isFalse);

    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));

    expect(value, isTrue);

    await tester.pump();
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));

    expect(value, isTrue);

    await tester.pump();
    await tester.drag(find.byType(Switch), const Offset(30.0, 0.0));

    expect(value, isFalse);
  });

  testWidgetsWithLeakTracking('Material2 - Switch has default colors when enabled', (WidgetTester tester) async {
    bool value = false;
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Directionality(
          textDirection: TextDirection.rtl,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x52000000), // Black with 32% opacity
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: Colors.grey.shade50),
      reason: 'Inactive enabled switch should match these colors',
    );
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
    await tester.pump();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x802196f3),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: const Color(0xff2196f3)),
      reason: 'Active enabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material3 - Switch has default colors when enabled', (WidgetTester tester) async {
    final ThemeData theme = ThemeData(useMaterial3: true);
    final ColorScheme colors = theme.colorScheme;
    bool value = false;
    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: Directionality(
          textDirection: TextDirection.rtl,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..save()
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.surfaceVariant,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect(
          style: PaintingStyle.stroke,
          color: colors.outline,
          rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
        )
        ..rrect(color: colors.outline), // thumb color
      reason: 'Inactive enabled switch should match these colors',
    );
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
    await tester.pump();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..save()
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.primary,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: colors.onPrimary), // thumb color
      reason: 'Active enabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material2 - Switch has default colors when disabled', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: const Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                value: false,
                onChanged: null,
              ),
            ),
          )
        ),
      ),
    );

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.black12,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: Colors.grey.shade400),
      reason: 'Inactive disabled switch should match these colors',
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: const Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                value: true,
                onChanged: null,
              ),
            ),
          ),
        ),
      ),
    );

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.black12,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: Colors.grey.shade400),
      reason: 'Active disabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material3 - Inactive Switch has default colors when disabled', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final ColorScheme colors = themeData.colorScheme;

    await tester.pumpWidget(MaterialApp(
      theme: themeData,
      home: const Directionality(
        textDirection: TextDirection.rtl,
        child: Material(
          child: Center(
            child: Switch(
              value: false,
              onChanged: null,
            ),
          ),
        ),
      ),
    ));

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..save()
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.surfaceVariant.withOpacity(0.12),
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect(
          style: PaintingStyle.stroke,
          color: colors.onSurface.withOpacity(0.12),
          rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
        )
        ..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)), // thumb color
      reason: 'Inactive disabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material3 - Active Switch has default colors when disabled', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final ColorScheme colors = themeData.colorScheme;
    await tester.pumpWidget(MaterialApp(
      theme: themeData,
      home: const Directionality(
        textDirection: TextDirection.rtl,
        child: Material(
          child: Center(
            child: Switch(
              value: true,
              onChanged: null,
            ),
          ),
        ),
      ),
    ));

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..save()
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.onSurface.withOpacity(0.12),
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: colors.surface), // thumb color
      reason: 'Active disabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material2 - Switch default overlayColor resolves hovered/focused state', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    Finder findSwitch() {
      return find.byWidgetPredicate((Widget widget) => widget is Switch);
    }

    MaterialInkController? getSwitchMaterial(WidgetTester tester) {
      return Material.of(tester.element(findSwitch()));
    }
    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(useMaterial3: false),
      home: Scaffold(
        body: Switch(
          focusNode: focusNode,
          value: true,
          onChanged: (_) { },
        ),
      ),
    ));

    // Focused.
    focusNode.requestFocus();
    await tester.pumpAndSettle();

    expect(getSwitchMaterial(tester),
      paints
        ..circle(color: theme.focusColor)
    );

    // On both hovered and focused, the overlay color should show hovered overlay color.
    final Offset center = tester.getCenter(find.byType(Switch));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
    await gesture.moveTo(center);
    await tester.pumpAndSettle();

    expect(getSwitchMaterial(tester),
      paints..circle(color: theme.hoverColor)
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Material3 - Switch default overlayColor resolves hovered/focused state', (WidgetTester tester) async {
    final ThemeData theme = ThemeData(useMaterial3: true);
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;

    Finder findSwitch() {
      return find.byWidgetPredicate((Widget widget) => widget is Switch);
    }

    MaterialInkController? getSwitchMaterial(WidgetTester tester) {
      return Material.of(tester.element(findSwitch()));
    }
    await tester.pumpWidget(MaterialApp(
      theme: theme,
      home: Scaffold(
        body: Switch(
          focusNode: focusNode,
          value: true,
          onChanged: (_) { },
        ),
      ),
    ));

    // Focused.
    focusNode.requestFocus();
    await tester.pumpAndSettle();

    expect(getSwitchMaterial(tester),
      paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))
    );

    // On both hovered and focused, the overlay color should show hovered overlay color.
    final Offset center = tester.getCenter(find.byType(Switch));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
    await gesture.moveTo(center);
    await tester.pumpAndSettle();

    expect(getSwitchMaterial(tester),
      paints..circle(color: theme.colorScheme.primary.withOpacity(0.08))
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Material2 - Switch can be set color', (WidgetTester tester) async {
    bool value = false;
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Directionality(
          textDirection: TextDirection.rtl,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                    activeColor: Colors.red[500],
                    activeTrackColor: Colors.green[500],
                    inactiveThumbColor: Colors.yellow[500],
                    inactiveTrackColor: Colors.blue[500],
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.blue[500],
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: Colors.yellow[500]),
    );
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
    await tester.pump();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.green[500],
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: Colors.red[500]),
    );
  });

  testWidgetsWithLeakTracking('Material3 - Switch can be set color', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final ColorScheme colors = themeData.colorScheme;

    bool value = false;
    await tester.pumpWidget(
      MaterialApp(
        theme: themeData,
        home: Directionality(
          textDirection: TextDirection.rtl,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                    activeColor: Colors.red[500],
                    activeTrackColor: Colors.green[500],
                    inactiveThumbColor: Colors.yellow[500],
                    inactiveTrackColor: Colors.blue[500],
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: Colors.blue[500],
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect(
          style: PaintingStyle.stroke,
          color: colors.outline,
        )
        ..rrect(color: Colors.yellow[500]), // thumb color
    );
    await tester.drag(find.byType(Switch), const Offset(-30.0, 0.0));
    await tester.pump();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: Colors.green[500],
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: Colors.red[500]), // thumb color
    );
  });

  testWidgetsWithLeakTracking('Drag ends after animation completes', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/17773

    bool value = false;
    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: Directionality(
          textDirection: TextDirection.ltr,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(value, isFalse);

    final Rect switchRect = tester.getRect(find.byType(Switch));
    final TestGesture gesture = await tester.startGesture(switchRect.centerLeft);
    await tester.pump();
    await gesture.moveBy(Offset(switchRect.width, 0.0));
    await tester.pump();
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));

    expect(value, isTrue);
    expect(tester.hasRunningAnimations, false);
  });

  testWidgetsWithLeakTracking('can veto switch dragging result', (WidgetTester tester) async {
    bool value = false;

    await tester.pumpWidget(
      Theme(
        data: theme,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    dragStartBehavior: DragStartBehavior.down,
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = value || newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    // Move a little to the right, not past the middle.
    TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
    await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0));
    await tester.pump();
    await gesture.moveBy(const Offset(-kTouchSlop + 5.1, 0.0));
    await tester.pump();
    await gesture.up();
    await tester.pump();
    expect(value, isFalse);
    final ToggleableStateMixin state = tester.state<ToggleableStateMixin>(
      find.descendant(
        of: find.byType(Switch),
        matching: find.byWidgetPredicate(
          (Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch',
        ),
      ),
    );
    expect(state.position.value, lessThan(0.5));
    await tester.pump();
    await tester.pumpAndSettle();
    expect(value, isFalse);
    expect(state.position.value, 0);

    // Move past the middle.
    gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
    await gesture.moveBy(const Offset(kTouchSlop + 0.1, 0.0));
    await tester.pump();
    await gesture.up();
    await tester.pump();
    expect(value, isTrue);
    expect(state.position.value, greaterThan(0.5));

    await tester.pump();
    await tester.pumpAndSettle();
    expect(value, isTrue);
    expect(state.position.value, 1.0);

    // Now move back to the left, the revert animation should play.
    gesture = await tester.startGesture(tester.getRect(find.byType(Switch)).center);
    await gesture.moveBy(const Offset(-kTouchSlop - 0.1, 0.0));
    await tester.pump();
    await gesture.up();
    await tester.pump();
    expect(value, isTrue);
    expect(state.position.value, lessThan(0.5));

    await tester.pump();
    await tester.pumpAndSettle();
    expect(value, isTrue);
    expect(state.position.value, 1.0);
  });

  testWidgetsWithLeakTracking('switch has semantic events', (WidgetTester tester) async {
    dynamic semanticEvent;
    bool value = false;
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async {
      semanticEvent = message;
    });
    final SemanticsTester semanticsTester = SemanticsTester(tester);

    await tester.pumpWidget(
      Theme(
        data: theme,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Center(
                  child: Switch(
                    value: value,
                    onChanged: (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
    await tester.tap(find.byType(Switch));
    final RenderObject object = tester.firstRenderObject(find.byType(Switch));

    expect(value, true);
    expect(semanticEvent, <String, dynamic>{
      'type': 'tap',
      'nodeId': object.debugSemantics!.id,
      'data': <String, dynamic>{},
    });
    expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true);

    semanticsTester.dispose();
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
  });

  testWidgetsWithLeakTracking('switch sends semantic events from parent if fully merged', (WidgetTester tester) async {
    dynamic semanticEvent;
    bool value = false;
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async {
      semanticEvent = message;
    });
    final SemanticsTester semanticsTester = SemanticsTester(tester);

    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            void onChanged(bool newValue) {
              setState(() {
                value = newValue;
              });
            }
            return Material(
              child: MergeSemantics(
                child: ListTile(
                  leading: const Text('test'),
                  onTap: () {
                    onChanged(!value);
                  },
                  trailing: Switch(
                    value: value,
                    onChanged: onChanged,
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
    await tester.tap(find.byType(MergeSemantics));
    final RenderObject object = tester.firstRenderObject(find.byType(MergeSemantics));

    expect(value, true);
    expect(semanticEvent, <String, dynamic>{
      'type': 'tap',
      'nodeId': object.debugSemantics!.id,
      'data': <String, dynamic>{},
    });
    expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true);

    semanticsTester.dispose();
    tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
  });

  testWidgetsWithLeakTracking('Switch.adaptive', (WidgetTester tester) async {
    bool value = false;
    const Color activeTrackColor =  Color(0xffff1200);
    const Color inactiveTrackColor =  Color(0xffff12ff);
    const Color thumbColor = Color(0xffffff00);
    const Color focusColor = Color(0xff00ff00);

    Widget buildFrame(TargetPlatform platform) {
      return MaterialApp(
        theme: ThemeData(platform: platform),
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Material(
              child: Center(
                child: Switch.adaptive(
                  value: value,
                  activeColor: activeTrackColor,
                  inactiveTrackColor: inactiveTrackColor,
                  thumbColor: const MaterialStatePropertyAll<Color?>(thumbColor),
                  focusColor: focusColor,
                  onChanged: (bool newValue) {
                    setState(() {
                      value = newValue;
                    });
                  },
                ),
              ),
            );
          },
        ),
      );
    }

    for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
      value = false;
      await tester.pumpWidget(buildFrame(platform));
      expect(find.byType(CupertinoSwitch), findsOneWidget, reason: 'on ${platform.name}');

      final CupertinoSwitch adaptiveSwitch = tester.widget(find.byType(CupertinoSwitch));
      expect(adaptiveSwitch.activeColor, activeTrackColor, reason: 'on ${platform.name}');
      expect(adaptiveSwitch.trackColor, inactiveTrackColor, reason: 'on ${platform.name}');
      expect(adaptiveSwitch.thumbColor, thumbColor, reason: 'on ${platform.name}');
      expect(adaptiveSwitch.focusColor, focusColor, reason: 'on ${platform.name}');

      expect(value, isFalse, reason: 'on ${platform.name}');
      await tester.tap(find.byType(Switch));
      expect(value, isTrue, reason: 'on ${platform.name}');
    }

    for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
      value = false;
      await tester.pumpWidget(buildFrame(platform));
      await tester.pumpAndSettle(); // Finish the theme change animation.
      expect(find.byType(CupertinoSwitch), findsNothing);
      expect(value, isFalse, reason: 'on ${platform.name}');
      await tester.tap(find.byType(Switch));
      expect(value, isTrue, reason: 'on ${platform.name}');
    }
  });

  testWidgetsWithLeakTracking('Material2 - Switch is focusable and has correct focus color', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    bool value = true;
    Widget buildApp({bool enabled = true}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Material(
          child: Center(
            child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
              return Switch(
                value: value,
                onChanged: enabled ? (bool newValue) {
                  setState(() {
                    value = newValue;
                  });
                } : null,
                focusColor: Colors.orange[500],
                autofocus: true,
                focusNode: focusNode,
              );
            }),
          ),
        ),
      );
    }
    await tester.pumpWidget(buildApp());

    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x802196f3),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..circle(color: Colors.orange[500])
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: const Color(0xff2196f3)),
    );

    // Check the false value.
    value = false;
    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x52000000),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..circle(color: Colors.orange[500])
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: const Color(0xfffafafa)),
    );

    // Check what happens when disabled.
    value = false;
    await tester.pumpWidget(buildApp(enabled: false));
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isFalse);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x1f000000),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: const Color(0xffbdbdbd)),
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Material3 - Switch is focusable and has correct focus color', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final ColorScheme colors = themeData.colorScheme;
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    bool value = true;
    Widget buildApp({bool enabled = true}) {
      return MaterialApp(
        theme: themeData,
        home: Material(
          child: Center(
            child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
              return Switch(
                value: value,
                onChanged: enabled ? (bool newValue) {
                  setState(() {
                    value = newValue;
                  });
                } : null,
                focusColor: Colors.orange[500],
                autofocus: true,
                focusNode: focusNode,
              );
            }),
          ),
        ),
      );
    }
    await tester.pumpWidget(buildApp());

    // active, enabled switch
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.primary,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..circle(color: Colors.orange[500]),
    );

    // Check the false value: inactive enabled switch
    value = false;
    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
        Material.of(tester.element(find.byType(Switch))),
        paints
          ..rrect(
            style: PaintingStyle.fill,
            color: colors.surfaceVariant,
            rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
          )
          ..rrect(
            style: PaintingStyle.stroke,
            color: colors.outline,
            rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
          )
          ..circle(color: Colors.orange[500])
    );

    // Check what happens when disabled: inactive disabled switch.
    value = false;
    await tester.pumpWidget(buildApp(enabled: false));
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isFalse);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.surfaceVariant.withOpacity(0.12),
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect(
          style: PaintingStyle.stroke,
          color: colors.onSurface.withOpacity(0.12),
          rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
        )
        ..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)),
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Switch with splash radius set', (WidgetTester tester) async {
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    const double splashRadius = 30;
    Widget buildApp() {
      return MaterialApp(
        theme: theme,
        home: Material(
          child: Center(
            child: Switch(
              value: true,
              onChanged: (bool newValue) {},
              focusColor: Colors.orange[500],
              autofocus: true,
              splashRadius: splashRadius,
            ),
          ),
        ),
      );
    }
    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints..circle(color: Colors.orange[500], radius: splashRadius),
    );
  });

  testWidgetsWithLeakTracking('Material2 - Switch can be hovered and has correct hover color', (WidgetTester tester) async {
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    bool value = true;
    Widget buildApp({bool enabled = true}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Material(
          child: Center(
            child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
              return Switch(
                value: value,
                onChanged: enabled ? (bool newValue) {
                  setState(() {
                    value = newValue;
                  });
                } : null,
                hoverColor: Colors.orange[500],
              );
            }),
          ),
        ),
      );
    }
    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x802196f3),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: const Color(0xff2196f3)),
    );

    // Start hovering
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(Switch)));

    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x802196f3),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..circle(color: Colors.orange[500])
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: const Color(0xff2196f3)),
    );

    // Check what happens when disabled.
    await tester.pumpWidget(buildApp(enabled: false));
    await tester.pumpAndSettle();
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x1f000000),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: const Color(0xffbdbdbd)),
    );
  });

  testWidgetsWithLeakTracking('Material3 - Switch can be hovered and has correct hover color', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final ColorScheme colors = themeData.colorScheme;
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    bool value = true;
    Widget buildApp({bool enabled = true}) {
      return MaterialApp(
        theme: themeData,
        home: Material(
          child: Center(
            child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
              return Switch(
                value: value,
                onChanged: enabled ? (bool newValue) {
                  setState(() {
                    value = newValue;
                  });
                } : null,
                hoverColor: Colors.orange[500],
              );
            }),
          ),
        ),
      );
    }

    // active enabled switch
    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: colors.primary,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: colors.onPrimary),
    );

    // Start hovering
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(Switch)));

    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: colors.primary,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..circle(color: Colors.orange[500]),
    );

    // Check what happens for disabled active switch
    await tester.pumpWidget(buildApp(enabled: false));
    await tester.pumpAndSettle();
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: colors.onSurface.withOpacity(0.12),
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: colors.surface.withOpacity(1.0)),
    );
  });

  testWidgetsWithLeakTracking('Switch can be toggled by keyboard shortcuts', (WidgetTester tester) async {
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    bool value = true;
    Widget buildApp({bool enabled = true}) {
      return MaterialApp(
        theme: theme,
        home: Material(
          child: Center(
            child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
              return Switch(
                value: value,
                onChanged: enabled ? (bool newValue) {
                  setState(() {
                    value = newValue;
                  });
                } : null,
                focusColor: Colors.orange[500],
                autofocus: true,
              );
            }),
          ),
        ),
      );
    }
    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();
    await tester.sendKeyEvent(LogicalKeyboardKey.enter);
    await tester.pumpAndSettle();
    // On web, switches don't respond to the enter key.
    expect(value, kIsWeb ? isTrue : isFalse);
    await tester.sendKeyEvent(LogicalKeyboardKey.enter);
    await tester.pumpAndSettle();
    expect(value, isTrue);
    await tester.sendKeyEvent(LogicalKeyboardKey.space);
    await tester.pumpAndSettle();
    expect(value, isFalse);
    await tester.sendKeyEvent(LogicalKeyboardKey.space);
    await tester.pumpAndSettle();
    expect(value, isTrue);
  });

  testWidgetsWithLeakTracking('Switch changes mouse cursor when hovered', (WidgetTester tester) async {
    // Test Switch.adaptive() constructor
    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: Switch.adaptive(
                  mouseCursor: SystemMouseCursors.text,
                  value: true,
                  onChanged: (_) {},
                ),
              ),
            ),
          ),
        ),
      ),
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
    await gesture.addPointer(location: tester.getCenter(find.byType(Switch)));

    await tester.pump();

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);

    // Test Switch() constructor
    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: Switch(
                  mouseCursor: SystemMouseCursors.text,
                  value: true,
                  onChanged: (_) {},
                ),
              ),
            ),
          ),
        ),
      ),
    );

    await gesture.moveTo(tester.getCenter(find.byType(Switch)));
    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);

    // Test default cursor
    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: Switch(
                  value: true,
                  onChanged: (_) {},
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);

    // Test default cursor when disabled
    await tester.pumpWidget(
      MaterialApp(
        theme: theme,
        home: const Scaffold(
          body: Align(
            alignment: Alignment.topLeft,
            child: Material(
              child: MouseRegion(
                cursor: SystemMouseCursors.forbidden,
                child: Switch(
                  value: true,
                  onChanged: null,
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);

    await tester.pumpAndSettle();
  });

  testWidgetsWithLeakTracking('Material switch should not recreate its render object when disabled', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/61247.
    bool value = true;
    bool enabled = true;
    late StateSetter stateSetter;
    await tester.pumpWidget(
      Theme(
        data: theme,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              stateSetter = setState;
              return Material(
                child: Center(
                  child: Switch(
                    value: value,
                    onChanged: !enabled ? null : (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );

    final ToggleableStateMixin oldSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));

    stateSetter(() { value = false; });
    await tester.pump();
    // Disable the switch when the implicit animation begins.
    stateSetter(() { enabled = false; });
    await tester.pump();

    final ToggleableStateMixin updatedSwitchState = tester.state(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch'));

    expect(updatedSwitchState.isInteractive, false);
    expect(updatedSwitchState, oldSwitchState);
    expect(updatedSwitchState.position.isCompleted, false);
    expect(updatedSwitchState.position.isDismissed, false);
  });

  testWidgetsWithLeakTracking('Material2 - Switch thumb color resolves in active/enabled states', (WidgetTester tester) async {
    const Color activeEnabledThumbColor = Color(0xFF000001);
    const Color activeDisabledThumbColor = Color(0xFF000002);
    const Color inactiveEnabledThumbColor = Color(0xFF000003);
    const Color inactiveDisabledThumbColor = Color(0xFF000004);

    Color getThumbColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        if (states.contains(MaterialState.selected)) {
          return activeDisabledThumbColor;
        }
        return inactiveDisabledThumbColor;
      }
      if (states.contains(MaterialState.selected)) {
        return activeEnabledThumbColor;
      }
      return inactiveEnabledThumbColor;
    }

    final MaterialStateProperty<Color> thumbColor =
      MaterialStateColor.resolveWith(getThumbColor);

    Widget buildSwitch({required bool enabled, required bool active}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                thumbColor: thumbColor,
                value: active,
                onChanged: enabled ? (_) { } : null,
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(enabled: false, active: false));

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.black12,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: inactiveDisabledThumbColor),
      reason: 'Inactive disabled switch should default track and custom thumb color',
    );

    await tester.pumpWidget(buildSwitch(enabled: false, active: true));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.black12,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: activeDisabledThumbColor),
      reason: 'Active disabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: true, active: false));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x52000000), // Black with 32% opacity,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: inactiveEnabledThumbColor),
      reason: 'Inactive enabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: false, active: false));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.black12,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: inactiveDisabledThumbColor),
      reason: 'Inactive disabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material3 - Switch thumb color resolves in active/enabled states', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final ColorScheme colors = themeData.colorScheme;
    const Color activeEnabledThumbColor = Color(0xFF000001);
    const Color activeDisabledThumbColor = Color(0xFF000002);
    const Color inactiveEnabledThumbColor = Color(0xFF000003);
    const Color inactiveDisabledThumbColor = Color(0xFF000004);

    Color getThumbColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        if (states.contains(MaterialState.selected)) {
          return activeDisabledThumbColor;
        }
        return inactiveDisabledThumbColor;
      }
      if (states.contains(MaterialState.selected)) {
        return activeEnabledThumbColor;
      }
      return inactiveEnabledThumbColor;
    }

    final MaterialStateProperty<Color> thumbColor = MaterialStateColor.resolveWith(getThumbColor);

    Widget buildSwitch({required bool enabled, required bool active}) {
      return Theme(
        data: themeData,
        child: Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                thumbColor: thumbColor,
                value: active,
                onChanged: enabled ? (_) { } : null,
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(enabled: false, active: false));

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.surfaceVariant.withOpacity(0.12),
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect(
          style: PaintingStyle.stroke,
          color: colors.onSurface.withOpacity(0.12),
          rrect: RRect.fromLTRBR(5.0, 9.0, 55.0, 39.0, const Radius.circular(16.0)),
        )
        ..rrect(color: inactiveDisabledThumbColor),
      reason: 'Inactive disabled switch should default track and custom thumb color',
    );

    await tester.pumpWidget(buildSwitch(enabled: false, active: true));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.onSurface.withOpacity(0.12),
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: activeDisabledThumbColor),
      reason: 'Active disabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: true, active: false));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.surfaceVariant,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: inactiveEnabledThumbColor),
      reason: 'Inactive enabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: true, active: true));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.primary,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: activeEnabledThumbColor),
      reason: 'Active enabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material2 - Switch thumb color resolves in hovered/focused states', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    const Color hoveredThumbColor = Color(0xFF000001);
    const Color focusedThumbColor = Color(0xFF000002);

    Color getThumbColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return hoveredThumbColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusedThumbColor;
      }
      return Colors.transparent;
    }

    final MaterialStateProperty<Color> thumbColor =
      MaterialStateColor.resolveWith(getThumbColor);

    Widget buildSwitch() {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Material(
          child: Center(
            child: Switch(
              focusNode: focusNode,
              autofocus: true,
              value: true,
              thumbColor: thumbColor,
              onChanged: (_) { },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch());
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x802196f3),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..circle() // Radial reaction
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: focusedThumbColor),
      reason: 'Inactive disabled switch should default track and custom thumb color',
    );

    // Start hovering
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(Switch)));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: const Color(0x802196f3),
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..circle()
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: hoveredThumbColor),
      reason: 'Inactive disabled switch should default track and custom thumb color',
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Material3 - Switch thumb color resolves in hovered/focused states', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final ColorScheme colors = themeData.colorScheme;
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    const Color hoveredThumbColor = Color(0xFF000001);
    const Color focusedThumbColor = Color(0xFF000002);

    Color getThumbColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return hoveredThumbColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusedThumbColor;
      }
      return Colors.transparent;
    }

    final MaterialStateProperty<Color> thumbColor = MaterialStateColor.resolveWith(getThumbColor);

    Widget buildSwitch() {
      return MaterialApp(
        theme: themeData,
        home: Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                focusNode: focusNode,
                autofocus: true,
                value: true,
                thumbColor: thumbColor,
                onChanged: (_) { },
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch());
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.primary,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..circle(color: colors.primary.withOpacity(0.12))
        ..rrect(color: focusedThumbColor),
      reason: 'active enabled switch should default track and custom thumb color',
    );

    // Start hovering
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(Switch)));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          style: PaintingStyle.fill,
          color: colors.primary,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..circle(color: colors.primary.withOpacity(0.08))
        ..rrect(color: hoveredThumbColor),
      reason: 'active enabled switch should default track and custom thumb color',
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Material2 - Track color resolves in active/enabled states', (WidgetTester tester) async {
    const Color activeEnabledTrackColor = Color(0xFF000001);
    const Color activeDisabledTrackColor = Color(0xFF000002);
    const Color inactiveEnabledTrackColor = Color(0xFF000003);
    const Color inactiveDisabledTrackColor = Color(0xFF000004);

    Color getTrackColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        if (states.contains(MaterialState.selected)) {
          return activeDisabledTrackColor;
        }
        return inactiveDisabledTrackColor;
      }
      if (states.contains(MaterialState.selected)) {
        return activeEnabledTrackColor;
      }
      return inactiveEnabledTrackColor;
    }

    final MaterialStateProperty<Color> trackColor =
      MaterialStateColor.resolveWith(getTrackColor);

    Widget buildSwitch({required bool enabled, required bool active}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Material(
          child: Center(
            child: Switch(
              trackColor: trackColor,
              value: active,
              onChanged: enabled ? (_) { } : null,
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(enabled: false, active: false));

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: inactiveDisabledTrackColor,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          ),
      reason: 'Inactive disabled switch track should use this value',
    );

    await tester.pumpWidget(buildSwitch(enabled: false, active: true));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: activeDisabledTrackColor,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          ),
      reason: 'Active disabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: true, active: false));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: inactiveEnabledTrackColor,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          ),
      reason: 'Inactive enabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: false, active: false));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: inactiveDisabledTrackColor,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          ),
      reason: 'Inactive disabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material3 - Track color resolves in active/enabled states', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    const Color activeEnabledTrackColor = Color(0xFF000001);
    const Color activeDisabledTrackColor = Color(0xFF000002);
    const Color inactiveEnabledTrackColor = Color(0xFF000003);
    const Color inactiveDisabledTrackColor = Color(0xFF000004);

    Color getTrackColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        if (states.contains(MaterialState.selected)) {
          return activeDisabledTrackColor;
        }
        return inactiveDisabledTrackColor;
      }
      if (states.contains(MaterialState.selected)) {
        return activeEnabledTrackColor;
      }
      return inactiveEnabledTrackColor;
    }

    final MaterialStateProperty<Color> trackColor =
    MaterialStateColor.resolveWith(getTrackColor);

    Widget buildSwitch({required bool enabled, required bool active}) {
      return Theme(
        data: themeData,
        child: Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                trackColor: trackColor,
                value: active,
                onChanged: enabled ? (_) { } : null,
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(enabled: false, active: false));

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: inactiveDisabledTrackColor,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        ),
      reason: 'Inactive disabled switch track should use this value',
    );

    await tester.pumpWidget(buildSwitch(enabled: false, active: true));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: activeDisabledTrackColor,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        ),
      reason: 'Active disabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: true, active: false));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: inactiveEnabledTrackColor,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        ),
      reason: 'Inactive enabled switch should match these colors',
    );

    await tester.pumpWidget(buildSwitch(enabled: true, active: true));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: activeEnabledTrackColor,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        ),
      reason: 'Active enabled switch should match these colors',
    );
  });

  testWidgetsWithLeakTracking('Material2 - Switch track color resolves in hovered/focused states', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    const Color hoveredTrackColor = Color(0xFF000001);
    const Color focusedTrackColor = Color(0xFF000002);

    Color getTrackColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return hoveredTrackColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusedTrackColor;
      }
      return Colors.transparent;
    }

    final MaterialStateProperty<Color> trackColor =
      MaterialStateColor.resolveWith(getTrackColor);

    Widget buildSwitch() {
      return MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                focusNode: focusNode,
                autofocus: true,
                value: true,
                trackColor: trackColor,
                onChanged: (_) { },
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch());
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: focusedTrackColor,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          ),
      reason: 'Inactive enabled switch should match these colors',
    );

    // Start hovering
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(Switch)));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: hoveredTrackColor,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          ),
      reason: 'Inactive enabled switch should match these colors',
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Material3 - Switch track color resolves in hovered/focused states', (WidgetTester tester) async {
    final ThemeData themeData = ThemeData(useMaterial3: true);
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    const Color hoveredTrackColor = Color(0xFF000001);
    const Color focusedTrackColor = Color(0xFF000002);

    Color getTrackColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.hovered)) {
        return hoveredTrackColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusedTrackColor;
      }
      return Colors.transparent;
    }

    final MaterialStateProperty<Color> trackColor =
    MaterialStateColor.resolveWith(getTrackColor);

    Widget buildSwitch() {
      return Theme(
        data: themeData,
        child: Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                focusNode: focusNode,
                autofocus: true,
                value: true,
                trackColor: trackColor,
                onChanged: (_) { },
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch());
    await tester.pumpAndSettle();
    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: focusedTrackColor,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        ),
      reason: 'Active enabled switch should match these colors',
    );

    // Start hovering
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(Switch)));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: hoveredTrackColor,
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        ),
      reason: 'Active enabled switch should match these colors',
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Material2 - Switch thumb color is blended against surface color', (WidgetTester tester) async {
    final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60);
    final ThemeData theme = ThemeData.light(useMaterial3: false);

    Color getThumbColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return activeDisabledThumbColor;
      }
      return Colors.black;
    }

    final MaterialStateProperty<Color> thumbColor =
      MaterialStateColor.resolveWith(getThumbColor);

    Widget buildSwitch({required bool enabled, required bool active}) {
      return Directionality(
        textDirection: TextDirection.rtl,
        child: Theme(
          data: theme,
          child: Material(
            child: Center(
              child: Switch(
                thumbColor: thumbColor,
                value: active,
                onChanged: enabled ? (_) { } : null,
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(enabled: false, active: true));

    final Color expectedThumbColor = Color.alphaBlend(activeDisabledThumbColor, theme.colorScheme.surface);

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
            color: Colors.black12,
            rrect: RRect.fromLTRBR(13.0, 17.0, 46.0, 31.0, const Radius.circular(7.0)),
          )
        ..rrect(color: const Color(0x33000000))
        ..rrect(color: const Color(0x24000000))
        ..rrect(color: const Color(0x1f000000))
        ..rrect(color: expectedThumbColor),
      reason: 'Active disabled thumb color should be blended on top of surface color',
    );
  });

  testWidgetsWithLeakTracking('Material3 - Switch thumb color is blended against surface color', (WidgetTester tester) async {
    final Color activeDisabledThumbColor = Colors.blue.withOpacity(.60);
    final ThemeData theme = ThemeData(useMaterial3: true);
    final ColorScheme colors = theme.colorScheme;

    Color getThumbColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return activeDisabledThumbColor;
      }
      return Colors.black;
    }

    final MaterialStateProperty<Color> thumbColor =
    MaterialStateColor.resolveWith(getThumbColor);

    Widget buildSwitch({required bool enabled, required bool active}) {
      return Directionality(
        textDirection: TextDirection.rtl,
        child: Theme(
          data: theme,
          child: Material(
            child: Center(
              child: Switch(
                thumbColor: thumbColor,
                value: active,
                onChanged: enabled ? (_) { } : null,
              ),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(enabled: false, active: true));

    final Color expectedThumbColor = Color.alphaBlend(activeDisabledThumbColor, theme.colorScheme.surface);

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect(
          color: colors.onSurface.withOpacity(0.12),
          rrect: RRect.fromLTRBR(4.0, 8.0, 56.0, 40.0, const Radius.circular(16.0)),
        )
        ..rrect()
        ..rrect(color: expectedThumbColor),
      reason: 'Active disabled thumb color should be blended on top of surface color',
    );
  });

  testWidgetsWithLeakTracking('Switch overlay color resolves in active/pressed/focused/hovered states', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;

    const Color activeThumbColor = Color(0xFF000000);
    const Color inactiveThumbColor = Color(0xFF000010);
    const Color activePressedOverlayColor = Color(0xFF000001);
    const Color inactivePressedOverlayColor = Color(0xFF000002);
    const Color hoverOverlayColor = Color(0xFF000003);
    const Color focusOverlayColor = Color(0xFF000004);
    const Color hoverColor = Color(0xFF000005);
    const Color focusColor = Color(0xFF000006);

    Color? getOverlayColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        if (states.contains(MaterialState.selected)) {
          return activePressedOverlayColor;
        }
        return inactivePressedOverlayColor;
      }
      if (states.contains(MaterialState.hovered)) {
        return hoverOverlayColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusOverlayColor;
      }
      return null;
    }
    const double splashRadius = 24.0;

    Widget buildSwitch({bool active = false, bool focused = false, bool useOverlay = true}) {
      return MaterialApp(
        theme: theme,
        home: Scaffold(
          body: Switch(
            focusNode: focusNode,
            autofocus: focused,
            value: active,
            onChanged: (_) { },
            thumbColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
              if (states.contains(MaterialState.selected)) {
                return activeThumbColor;
              }
              return inactiveThumbColor;
            }),
            overlayColor: useOverlay ? MaterialStateProperty.resolveWith(getOverlayColor) : null,
            hoverColor: hoverColor,
            focusColor: focusColor,
            splashRadius: splashRadius,
          ),
        ),
      );
    }

    // test inactive Switch, and overlayColor is set to null.
    await tester.pumpWidget(buildSwitch(useOverlay: false));
    await tester.press(find.byType(Switch));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect()
        ..circle(
          color: inactiveThumbColor.withAlpha(kRadialReactionAlpha),
          radius: splashRadius,
        ),
      reason: 'Default inactive pressed Switch should have overlay color from thumbColor',
    );

    // test active Switch, and overlayColor is set to null.
    await tester.pumpWidget(buildSwitch(active: true, useOverlay: false));
    await tester.press(find.byType(Switch));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect()
        ..circle(
          color: activeThumbColor.withAlpha(kRadialReactionAlpha),
          radius: splashRadius,
        ),
      reason: 'Default active pressed Switch should have overlay color from thumbColor',
    );

    // test inactive Switch with an overlayColor
    await tester.pumpWidget(buildSwitch());
    await tester.press(find.byType(Switch));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect()
        ..circle(
          color: inactivePressedOverlayColor,
          radius: splashRadius,
        ),
      reason: 'Inactive pressed Switch should have overlay color: $inactivePressedOverlayColor',
    );

    // test active Switch with an overlayColor
    await tester.pumpWidget(buildSwitch(active: true));
    await tester.press(find.byType(Switch));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect()
        ..circle(
          color: activePressedOverlayColor,
          radius: splashRadius,
        ),
      reason: 'Active pressed Switch should have overlay color: $activePressedOverlayColor',
    );

    await tester.pumpWidget(buildSwitch(focused: true));
    await tester.pumpAndSettle();

    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect()
        ..circle(
          color: focusOverlayColor,
          radius: splashRadius,
        ),
      reason: 'Focused Switch should use overlay color $focusOverlayColor over $focusColor',
    );

    // Start hovering
    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(Switch)));
    await tester.pumpAndSettle();

    expect(
      Material.of(tester.element(find.byType(Switch))),
      paints
        ..rrect()
        ..circle(
          color: hoverOverlayColor,
          radius: splashRadius,
        ),
      reason: 'Hovered Switch should use overlay color $hoverOverlayColor over $hoverColor',
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
    Widget buildSwitch(bool show) {
      return MaterialApp(
        theme: theme,
        home: Material(
          child: Center(
            child: show ? Switch(value: true, onChanged: (_) { }) : Container(),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildSwitch(true));
    final Offset center = tester.getCenter(find.byType(Switch));
    // Put a pointer down on the screen.
    final TestGesture gesture = await tester.startGesture(center);
    await tester.pump();
    // While the pointer is down, the widget disappears.
    await tester.pumpWidget(buildSwitch(false));
    expect(find.byType(Switch), findsNothing);
    // Release pointer after widget disappeared.
    await gesture.up();
  });

  testWidgetsWithLeakTracking('disabled switch shows tooltip', (WidgetTester tester) async {
    const String longPressTooltip = 'long press tooltip';
    const String tapTooltip = 'tap tooltip';
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Tooltip(
            message: longPressTooltip,
            child: Switch(
              onChanged: null,
              value: true,
            ),
          ),
        ),
      )
    );

    // Default tooltip shows up after long pressed.
    final Finder tooltip0 = find.byType(Tooltip);
    expect(find.text(longPressTooltip), findsNothing);

    await tester.tap(tooltip0);
    await tester.pump(const Duration(milliseconds: 10));
    expect(find.text(longPressTooltip), findsNothing);

    final TestGesture gestureLongPress = await tester.startGesture(tester.getCenter(tooltip0));
    await tester.pump();
    await tester.pump(kLongPressTimeout);
    await gestureLongPress.up();
    await tester.pump();

    expect(find.text(longPressTooltip), findsOneWidget);

    // Tooltip shows up after tapping when set triggerMode to TooltipTriggerMode.tap.
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Tooltip(
            triggerMode: TooltipTriggerMode.tap,
            message: tapTooltip,
            child: Switch(
              onChanged: null,
              value: true,
            ),
          ),
        ),
      )
    );

    await tester.pump(const Duration(days: 1));
    await tester.pumpAndSettle();
    expect(find.text(tapTooltip), findsNothing);
    expect(find.text(longPressTooltip), findsNothing);

    final Finder tooltip1 = find.byType(Tooltip);
    await tester.tap(tooltip1);
    await tester.pump(const Duration(milliseconds: 10));
    expect(find.text(tapTooltip), findsOneWidget);
  });

  group('with image', () {
    late ui.Image image;

    setUp(() async {
      image = await createTestImage(width: 100, height: 100);
    });

    testWidgetsWithLeakTracking('thumb image shows up', (WidgetTester tester) async {
      imageCache.clear();
      final _TestImageProvider provider1 = _TestImageProvider();
      final _TestImageProvider provider2 = _TestImageProvider();

      expect(provider1.loadCallCount, 0);
      expect(provider2.loadCallCount, 0);

      bool value1 = true;
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(platform: TargetPlatform.android),
          home: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Material(
                child: Switch(
                  activeThumbImage: provider1,
                  inactiveThumbImage: provider2,
                  value: value1,
                  onChanged: (bool val) {
                    setState(() {
                      value1 = val;
                    });
                  },
                ),
              );
            }
          )
        )
      );

      expect(provider1.loadCallCount, 1);
      expect(provider2.loadCallCount, 0);
      expect(imageCache.liveImageCount, 1);
      await tester.tap(find.byType(Switch));
      await tester.pumpAndSettle();
      expect(provider1.loadCallCount, 1);
      expect(provider2.loadCallCount, 1);
      expect(imageCache.liveImageCount, 2);
    });

    testWidgetsWithLeakTracking('do not crash when imageProvider completes after Switch is disposed', (WidgetTester tester) async {
      final DelayedImageProvider imageProvider = DelayedImageProvider(image);

      await tester.pumpWidget(
        MaterialApp(
          theme: theme,
          home: Material(
            child: Center(
              child: Switch(
                value: true,
                onChanged: null,
                inactiveThumbImage: imageProvider,
              ),
            ),
          ),
        ),
      );

      expect(find.byType(Switch), findsOneWidget);

      // Dispose the switch by taking down the tree.
      await tester.pumpWidget(Container());
      expect(find.byType(Switch), findsNothing);

      imageProvider.complete();
      expect(tester.takeException(), isNull);
    });

    testWidgetsWithLeakTracking('do not crash when previous imageProvider completes after Switch is disposed', (WidgetTester tester) async {
      final DelayedImageProvider imageProvider1 = DelayedImageProvider(image);
      final DelayedImageProvider imageProvider2 = DelayedImageProvider(image);

      Future<void> buildSwitch(ImageProvider imageProvider) {
        return tester.pumpWidget(
          MaterialApp(
            theme: theme,
            home: Material(
              child: Center(
                child: Switch(
                  value: true,
                  onChanged: null,
                  inactiveThumbImage: imageProvider,
                ),
              ),
            ),
          ),
        );
      }

      await buildSwitch(imageProvider1);
      expect(find.byType(Switch), findsOneWidget);
      // Replace the ImageProvider.
      await buildSwitch(imageProvider2);
      expect(find.byType(Switch), findsOneWidget);

      // Dispose the switch by taking down the tree.
      await tester.pumpWidget(Container());
      expect(find.byType(Switch), findsNothing);

      // Completing the replaced ImageProvider shouldn't crash.
      imageProvider1.complete();
      expect(tester.takeException(), isNull);

      imageProvider2.complete();
      expect(tester.takeException(), isNull);
    });
  });

  group('Switch M3 only tests', () {
    testWidgetsWithLeakTracking('M3 Switch has a 300-millisecond animation in total', (WidgetTester tester) async {
      final ThemeData theme = ThemeData(useMaterial3: true);
      bool value = false;
      await tester.pumpWidget(
        MaterialApp(
          theme: theme,
          home: Directionality(
            textDirection: TextDirection.rtl,
            child: StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
                return Material(
                  child: Center(
                    child: Switch(
                      value: value,
                      onChanged: (bool newValue) {
                        setState(() {
                          value = newValue;
                        });
                      },
                    ),
                  ),
                );
              },
            ),
          ),
        ),
      );
      expect(value, isFalse);

      final Rect switchRect = tester.getRect(find.byType(Switch));
      final TestGesture gesture = await tester.startGesture(switchRect.centerLeft);
      await tester.pump();
      await gesture.up();
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 200)); // M2 animation duration
      expect(tester.hasRunningAnimations, true);
      await tester.pump(const Duration(milliseconds: 101));
      expect(tester.hasRunningAnimations, false);
    });

    testWidgetsWithLeakTracking('M3 Switch has a stadium shape in the middle of the track', (WidgetTester tester) async {
      final ThemeData theme = ThemeData(useMaterial3: true, colorSchemeSeed: Colors.deepPurple);
      bool value = false;
      await tester.pumpWidget(
        MaterialApp(
          theme: theme,
          home: Directionality(
            textDirection: TextDirection.ltr,
            child: StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
                return Material(
                  child: Center(
                    child: Switch(
                      value: value,
                      onChanged: (bool newValue) {
                        setState(() {
                          value = newValue;
                        });
                      },
                    ),
                  ),
                );
              },
            ),
          ),
        ),
      );
      expect(value, isFalse);

      final Rect switchRect = tester.getRect(find.byType(Switch));
      final TestGesture gesture = await tester.startGesture(switchRect.centerLeft);
      await tester.pump();
      await gesture.up();
      await tester.pump();
      // After 33 milliseconds, the switch thumb moves to the middle
      // and has a stadium shape with a size of (34x22).
      await tester.pump(const Duration(milliseconds: 33));
      expect(tester.hasRunningAnimations, true);

      await expectLater(
        find.byType(Switch),
        matchesGoldenFile('switch_test.m3.transition.png'),
      );
    });

    testWidgetsWithLeakTracking('M3 Switch thumb bounces in the end of the animation', (WidgetTester tester) async {
      final ThemeData theme = ThemeData(useMaterial3: true);
      bool value = false;
      await tester.pumpWidget(
        MaterialApp(
          theme: theme,
          home: Directionality(
            textDirection: TextDirection.ltr,
            child: StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
                return Material(
                  child: Center(
                    child: Switch(
                      value: value,
                      onChanged: (bool newValue) {
                        setState(() {
                          value = newValue;
                        });
                      },
                    ),
                  ),
                );
              },
            ),
          ),
        ),
      );
      expect(value, isFalse);

      final Rect switchRect = tester.getRect(find.byType(Switch));
      final TestGesture gesture = await tester.startGesture(switchRect.centerLeft);
      await tester.pump();
      await gesture.up();
      await tester.pump();
      // The value on y axis is greater than 1 when t > 0.375
      // 300 * 0.375 = 112.5
      await tester.pump(const Duration(milliseconds: 113));
      final ToggleableStateMixin state = tester.state<ToggleableStateMixin>(
        find.descendant(
          of: find.byType(Switch),
          matching: find.byWidgetPredicate(
                (Widget widget) => widget.runtimeType.toString() == '_MaterialSwitch',
          ),
        ),
      );
      expect(tester.hasRunningAnimations, true);
      expect(state.position.value, greaterThan(1));
    });

    testWidgets('Switch thumb shows correct pressed color - M3', (WidgetTester tester) async {
      final ThemeData themeData = ThemeData(useMaterial3: true);
      final ColorScheme colors = themeData.colorScheme;
      Widget buildApp({bool enabled = true, bool value = true}) {
        return MaterialApp(
          theme: themeData,
          home: Material(
            child: Center(
              child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
                return Switch(
                  value: value,
                  onChanged: enabled ? (bool newValue) {
                    setState(() {
                      value = newValue;
                    });
                  } : null,
                );
              }),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildApp());
      await tester.press(find.byType(Switch));
      await tester.pumpAndSettle();

      expect(Material.of(tester.element(find.byType(Switch))),
        paints..rrect(
          color: colors.primary, // track color
          style: PaintingStyle.fill,
        )..rrect(
          color: Colors.transparent, // track outline color
          style: PaintingStyle.stroke,
        )..rrect(color: colors.primaryContainer, rrect: RRect.fromLTRBR(26.0, 10.0, 54.0, 38.0, const Radius.circular(14.0))),
      );

      await tester.pumpWidget(Container());
      await tester.pumpWidget(buildApp(value: false));
      await tester.press(find.byType(Switch));
      await tester.pumpAndSettle();

      expect(Material.of(tester.element(find.byType(Switch))),
        paints..rrect(
          color: colors.surfaceVariant, // track color
          style: PaintingStyle.fill
        )..rrect(
          color: colors.outline, // track outline color
          style: PaintingStyle.stroke,
        )..rrect(color: colors.onSurfaceVariant),
      );

      await tester.pumpWidget(Container());
      await tester.pumpWidget(buildApp(enabled: false));
      await tester.press(find.byType(Switch));
      await tester.pumpAndSettle();

      expect(Material.of(tester.element(find.byType(Switch))),
        paints..rrect(
          color: colors.onSurface.withOpacity(0.12), // track color
          style: PaintingStyle.fill,
        )..rrect(
          color: Colors.transparent, // track outline color
          style: PaintingStyle.stroke,
        )..rrect(color: colors.surface.withOpacity(1.0)),
      );

      await tester.pumpWidget(Container());
      await tester.pumpWidget(buildApp(enabled: false, value: false));
      await tester.press(find.byType(Switch));
      await tester.pumpAndSettle();

      expect(Material.of(tester.element(find.byType(Switch))),
        paints..rrect(
          color: colors.surfaceVariant.withOpacity(0.12), // track color
          style: PaintingStyle.fill,
        )..rrect(
          color: colors.onSurface.withOpacity(0.12), // track outline color
          style: PaintingStyle.stroke,
        )..rrect(color: Color.alphaBlend(colors.onSurface.withOpacity(0.38), colors.surface)),
      );
    }, variant: TargetPlatformVariant.mobile());

    testWidgetsWithLeakTracking('Track outline color resolves in active/enabled states', (WidgetTester tester) async {
      const Color activeEnabledTrackOutlineColor = Color(0xFF000001);
      const Color activeDisabledTrackOutlineColor = Color(0xFF000002);
      const Color inactiveEnabledTrackOutlineColor = Color(0xFF000003);
      const Color inactiveDisabledTrackOutlineColor = Color(0xFF000004);

      Color getTrackOutlineColor(Set<MaterialState> states) {
        if (states.contains(MaterialState.disabled)) {
          if (states.contains(MaterialState.selected)) {
            return activeDisabledTrackOutlineColor;
          }
          return inactiveDisabledTrackOutlineColor;
        }
        if (states.contains(MaterialState.selected)) {
          return activeEnabledTrackOutlineColor;
        }
        return inactiveEnabledTrackOutlineColor;
      }

      final MaterialStateProperty<Color> trackOutlineColor = MaterialStateColor.resolveWith(getTrackOutlineColor);

      Widget buildSwitch({required bool enabled, required bool active}) {
        return Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                trackOutlineColor: trackOutlineColor,
                value: active,
                onChanged: enabled ? (_) { } : null,
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildSwitch(enabled: false, active: false));

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(color: inactiveDisabledTrackOutlineColor, style: PaintingStyle.stroke),
        reason: 'Inactive disabled switch track outline should use this value',
      );

      await tester.pumpWidget(buildSwitch(enabled: false, active: true));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(color: activeDisabledTrackOutlineColor, style: PaintingStyle.stroke),
        reason: 'Active disabled switch track outline should match these colors',
      );

      await tester.pumpWidget(buildSwitch(enabled: true, active: false));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(color: inactiveEnabledTrackOutlineColor),
        reason: 'Inactive enabled switch track outline should match these colors',
      );

      await tester.pumpWidget(buildSwitch(enabled: true, active: true));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(color: activeEnabledTrackOutlineColor),
        reason: 'Active enabled switch track outline should match these colors',
      );
    });

    testWidgetsWithLeakTracking('Switch track outline color resolves in hovered/focused states', (WidgetTester tester) async {
      final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
      tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
      const Color hoveredTrackOutlineColor = Color(0xFF000001);
      const Color focusedTrackOutlineColor = Color(0xFF000002);

      Color getTrackOutlineColor(Set<MaterialState> states) {
        if (states.contains(MaterialState.hovered)) {
          return hoveredTrackOutlineColor;
        }
        if (states.contains(MaterialState.focused)) {
          return focusedTrackOutlineColor;
        }
        return Colors.transparent;
      }

      final MaterialStateProperty<Color> trackOutlineColor = MaterialStateColor.resolveWith(getTrackOutlineColor);

      Widget buildSwitch() {
        return Directionality(
          textDirection: TextDirection.rtl,
          child: Material(
            child: Center(
              child: Switch(
                focusNode: focusNode,
                autofocus: true,
                value: true,
                trackOutlineColor: trackOutlineColor,
                onChanged: (_) { },
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildSwitch());
      await tester.pumpAndSettle();
      expect(focusNode.hasPrimaryFocus, isTrue);
      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(color: focusedTrackOutlineColor, style: PaintingStyle.stroke),
        reason: 'Active enabled switch track outline should match this color',
      );

      // Start hovering
      final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
      await gesture.addPointer();
      await gesture.moveTo(tester.getCenter(find.byType(Switch)));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(color: hoveredTrackOutlineColor, style: PaintingStyle.stroke),
        reason: 'Active enabled switch track outline should match this color',
      );

      focusNode.dispose();
    });

    testWidgetsWithLeakTracking('Track outline width resolves in active/enabled states', (WidgetTester tester) async {
      const double activeEnabledTrackOutlineWidth = 1.0;
      const double activeDisabledTrackOutlineWidth = 2.0;
      const double inactiveEnabledTrackOutlineWidth = 3.0;
      const double inactiveDisabledTrackOutlineWidth = 4.0;

      double getTrackOutlineWidth(Set<MaterialState> states) {
        if (states.contains(MaterialState.disabled)) {
          if (states.contains(MaterialState.selected)) {
            return activeDisabledTrackOutlineWidth;
          }
          return inactiveDisabledTrackOutlineWidth;
        }
        if (states.contains(MaterialState.selected)) {
          return activeEnabledTrackOutlineWidth;
        }
        return inactiveEnabledTrackOutlineWidth;
      }

      final MaterialStateProperty<double> trackOutlineWidth = MaterialStateProperty.resolveWith(getTrackOutlineWidth);

      Widget buildSwitch({required bool enabled, required bool active}) {
        return MaterialApp(
          theme: ThemeData(useMaterial3: true),
          home: Material(
            child: Center(
              child: Switch(
                trackOutlineWidth: trackOutlineWidth,
                value: active,
                onChanged: enabled ? (_) { } : null,
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildSwitch(enabled: false, active: false));

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(strokeWidth: inactiveDisabledTrackOutlineWidth, style: PaintingStyle.stroke),
        reason: 'Inactive disabled switch track outline width should be 4.0',
      );

      await tester.pumpWidget(buildSwitch(enabled: false, active: true));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(strokeWidth: activeDisabledTrackOutlineWidth, style: PaintingStyle.stroke),
        reason: 'Active disabled switch track outline width should be 2.0',
      );

      await tester.pumpWidget(buildSwitch(enabled: true, active: false));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(strokeWidth: inactiveEnabledTrackOutlineWidth, style: PaintingStyle.stroke),
        reason: 'Inactive enabled switch track outline width should be 3.0',
      );

      await tester.pumpWidget(buildSwitch(enabled: true, active: true));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(strokeWidth: activeEnabledTrackOutlineWidth, style: PaintingStyle.stroke),
        reason: 'Active enabled switch track outline width should be 1.0',
      );
    });

    testWidgetsWithLeakTracking('Switch track outline width resolves in hovered/focused states', (WidgetTester tester) async {
      final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
      tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
      const double hoveredTrackOutlineWidth = 4.0;
      const double focusedTrackOutlineWidth = 6.0;

      double getTrackOutlineWidth(Set<MaterialState> states) {
        if (states.contains(MaterialState.hovered)) {
          return hoveredTrackOutlineWidth;
        }
        if (states.contains(MaterialState.focused)) {
          return focusedTrackOutlineWidth;
        }
        return 8.0;
      }

      final MaterialStateProperty<double> trackOutlineWidth = MaterialStateProperty.resolveWith(getTrackOutlineWidth);

      Widget buildSwitch() {
        return MaterialApp(
          theme: ThemeData(useMaterial3: true),
          home: Material(
            child: Center(
              child: Switch(
                focusNode: focusNode,
                autofocus: true,
                value: true,
                trackOutlineWidth: trackOutlineWidth,
                onChanged: (_) { },
              ),
            ),
          ),
        );
      }

      await tester.pumpWidget(buildSwitch());
      await tester.pumpAndSettle();
      expect(focusNode.hasPrimaryFocus, isTrue);
      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(strokeWidth: focusedTrackOutlineWidth, style: PaintingStyle.stroke),
        reason: 'Active enabled switch track outline width should be 6.0',
      );

      // Start hovering
      final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
      await gesture.addPointer();
      await gesture.moveTo(tester.getCenter(find.byType(Switch)));
      await tester.pumpAndSettle();

      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints..rrect(style: PaintingStyle.fill)
          ..rrect(strokeWidth: hoveredTrackOutlineWidth, style: PaintingStyle.stroke),
        reason: 'Active enabled switch track outline width should be 4.0',
      );

      focusNode.dispose();
    });

    testWidgetsWithLeakTracking('Switch can set icon - M3', (WidgetTester tester) async {
      final ThemeData themeData = ThemeData(
        useMaterial3: true,
        colorSchemeSeed: const Color(0xff6750a4),
        brightness: Brightness.light);

      MaterialStateProperty<Icon?> thumbIcon(Icon? activeIcon, Icon? inactiveIcon) {
        return MaterialStateProperty.resolveWith<Icon?>((Set<MaterialState> states) {
          if (states.contains(MaterialState.selected)) {
            return activeIcon;
          }
          return inactiveIcon;
        });
      }
      Widget buildSwitch({required bool enabled, required bool active, Icon? activeIcon, Icon? inactiveIcon}) {
        return Theme(
          data: themeData,
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Material(
              child: Center(
                child: Switch(
                  thumbIcon: thumbIcon(activeIcon, inactiveIcon),
                  value: active,
                  onChanged: enabled ? (_) {} : null,
                ),
              ),
            ),
          ),
        );
      }

      // active icon shows when switch is on.
      await tester.pumpWidget(buildSwitch(enabled: true, active: true, activeIcon: const Icon(Icons.close)));
      await tester.pumpAndSettle();
      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints
          ..rrect()..rrect()
          ..paragraph(offset: const Offset(32.0, 16.0)),
      );

      // inactive icon shows when switch is off.
      await tester.pumpWidget(buildSwitch(enabled: true, active: false, inactiveIcon: const Icon(Icons.close)));
      await tester.pumpAndSettle();
      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints
          ..rrect()..rrect()
          ..rrect()
          ..paragraph(offset: const Offset(12.0, 16.0)),
      );

      // active icon doesn't show when switch is off.
      await tester.pumpWidget(buildSwitch(enabled: true, active: false, activeIcon: const Icon(Icons.check)));
      await tester.pumpAndSettle();
      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints
          ..rrect()..rrect()..rrect()
      );

      // inactive icon doesn't show when switch is on.
      await tester.pumpWidget(buildSwitch(enabled: true, active: true, inactiveIcon: const Icon(Icons.check)));
      await tester.pumpAndSettle();
      expect(
          Material.of(tester.element(find.byType(Switch))),
          paints
            ..rrect()..rrect()..restore(),
      );

      // without icon
      await tester.pumpWidget(buildSwitch(enabled: true, active: false));
      expect(
        Material.of(tester.element(find.byType(Switch))),
        paints
          ..rrect()..rrect()..rrect()..restore(),
      );
    });
  });

  testWidgetsWithLeakTracking('Switch.adaptive(Cupertino) is focusable and has correct focus color', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch.adaptive');
    tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    bool value = true;
    const Color focusColor = Color(0xffff0000);

    Widget buildApp({bool enabled = true}) {
      return MaterialApp(
        theme: ThemeData(platform: TargetPlatform.iOS),
        home: Material(
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return Center(
                child: Switch.adaptive(
                  value: value,
                  onChanged: enabled ? (bool newValue) {
                    setState(() {
                      value = newValue;
                    });
                  } : null,
                  focusColor: focusColor,
                  focusNode: focusNode,
                  autofocus: true,
                ),
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();

    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      find.byType(CupertinoSwitch),
      paints
        ..rrect(color: const Color(0xff34c759))
        ..rrect(color: focusColor)
        ..clipRRect()
        ..rrect(color: const Color(0x26000000))
        ..rrect(color: const Color(0x0f000000))
        ..rrect(color: const Color(0x0a000000))
        ..rrect(color: const Color(0xffffffff)),
    );

    // Check the false value.
    value = false;
    await tester.pumpWidget(buildApp());
    await tester.pumpAndSettle();

    expect(focusNode.hasPrimaryFocus, isTrue);
    expect(
      find.byType(CupertinoSwitch),
      paints
        ..rrect(color: const Color(0x28787880))
        ..rrect(color: focusColor)
        ..clipRRect()
        ..rrect(color: const Color(0x26000000))
        ..rrect(color: const Color(0x0f000000))
        ..rrect(color: const Color(0x0a000000))
        ..rrect(color: const Color(0xffffffff)),
    );

    // Check what happens when disabled.
    value = false;
    await tester.pumpWidget(buildApp(enabled: false));
    await tester.pumpAndSettle();

    expect(focusNode.hasPrimaryFocus, isFalse);
    expect(
      find.byType(CupertinoSwitch),
      paints
        ..rrect(color: const Color(0x28787880))
        ..clipRRect()
        ..rrect(color: const Color(0x26000000))
        ..rrect(color: const Color(0x0f000000))
        ..rrect(color: const Color(0x0a000000))
        ..rrect(color: const Color(0xffffffff)),
    );

    focusNode.dispose();
  });

  testWidgetsWithLeakTracking('Switch.onFocusChange callback', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode(debugLabel: 'Switch');
    bool focused = false;
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: Center(
          child: Switch(
            value: true,
            focusNode: focusNode,
            onFocusChange: (bool value) {
              focused = value;
            },
            onChanged:(bool newValue) {},
          ),
        ),
      ),
    ));

    focusNode.requestFocus();
    await tester.pump();
    expect(focused, isTrue);
    expect(focusNode.hasFocus, isTrue);

    focusNode.unfocus();
    await tester.pump();
    expect(focused, isFalse);
    expect(focusNode.hasFocus, isFalse);

    focusNode.dispose();
  });
}

class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
  DelayedImageProvider(this.image);

  final ui.Image image;

  final Completer<ImageInfo> _completer = Completer<ImageInfo>();

  @override
  Future<DelayedImageProvider> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<DelayedImageProvider>(this);
  }

  @override
  ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
    return OneFrameImageStreamCompleter(_completer.future);
  }

  Future<void> complete() async {
    _completer.complete(ImageInfo(image: image));
  }

  @override
  String toString() => '${describeIdentity(this)}()';
}

class _TestImageProvider extends ImageProvider<Object> {
  _TestImageProvider({ImageStreamCompleter? streamCompleter}) {
    _streamCompleter = streamCompleter
        ?? OneFrameImageStreamCompleter(_completer.future);
  }

  final Completer<ImageInfo> _completer = Completer<ImageInfo>();
  late ImageStreamCompleter _streamCompleter;

  bool get loadCalled => _loadCallCount > 0;
  int get loadCallCount => _loadCallCount;
  int _loadCallCount = 0;

  @override
  Future<Object> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<_TestImageProvider>(this);
  }

  @override
  void resolveStreamForKey(ImageConfiguration configuration, ImageStream stream, Object key, ImageErrorListener handleError) {
    super.resolveStreamForKey(configuration, stream, key, handleError);
  }

  @override
  ImageStreamCompleter load(Object key, DecoderCallback decode) {
    _loadCallCount += 1;
    return _streamCompleter;
  }

  void complete(ui.Image image) {
    _completer.complete(ImageInfo(image: image));
  }

  void fail(Object exception, StackTrace? stackTrace) {
    _completer.completeError(exception, stackTrace);
  }

  @override
  String toString() => '${describeIdentity(this)}()';
}
