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

import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Radio group control test', (WidgetTester tester) async {
    final UniqueKey key0 = UniqueKey();
    final UniqueKey key1 = UniqueKey();

    await tester.pumpWidget(
      Material(
        child: TestRadioGroup<int>(
          child: Column(
            children: <Widget>[
              Radio<int>(key: key0, value: 0),
              Radio<int>(key: key1, value: 1),
            ],
          ),
        ),
      ),
    );
    expect(
      tester.getSemantics(find.byKey(key0)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
    );
    expect(
      tester.getSemantics(find.byKey(key1)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
    );

    await tester.tap(find.byKey(key0));
    await tester.pumpAndSettle();
    expect(
      tester.getSemantics(find.byKey(key0)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: true, isEnabled: true),
    );
    expect(
      tester.getSemantics(find.byKey(key1)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
    );

    await tester.tap(find.byKey(key1));
    await tester.pumpAndSettle();
    expect(
      tester.getSemantics(find.byKey(key0)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
    );
    expect(
      tester.getSemantics(find.byKey(key1)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: true, isEnabled: true),
    );
  });

  testWidgets('Radio group can have disabled radio', (WidgetTester tester) async {
    final UniqueKey key0 = UniqueKey();
    final UniqueKey key1 = UniqueKey();

    await tester.pumpWidget(
      Material(
        child: TestRadioGroup<int>(
          child: Column(
            children: <Widget>[
              Radio<int>(key: key0, value: 0, enabled: false),
              Radio<int>(key: key1, value: 1),
            ],
          ),
        ),
      ),
    );
    expect(
      tester.getSemantics(find.byKey(key0)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: false),
    );
    expect(
      tester.getSemantics(find.byKey(key1)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
    );

    await tester.tap(find.byKey(key0));
    await tester.pumpAndSettle();
    // Can't be select because the radio is disabled.
    expect(
      tester.getSemantics(find.byKey(key0)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: false),
    );
    expect(
      tester.getSemantics(find.byKey(key1)),
      containsSemantics(isInMutuallyExclusiveGroup: true, isChecked: false, isEnabled: true),
    );
  });

  testWidgets('Radio group will not merge up', (WidgetTester tester) async {
    await tester.pumpWidget(
      Material(
        child: Semantics(
          container: true,
          child: Column(
            children: <Widget>[
              Checkbox(value: true, onChanged: (bool? value) {}),
              const TestRadioGroup<int>(
                child: Column(children: <Widget>[Radio<int>(value: 0), Radio<int>(value: 1)]),
              ),
              Checkbox(value: true, onChanged: (bool? value) {}),
            ],
          ),
        ),
      ),
    );
    final SemanticsNode radioGroup = tester.getSemantics(find.byType(RadioGroup<int>));
    expect(radioGroup.childrenCount, 2);
  });

  testWidgets('Radio group can use arrow key', (WidgetTester tester) async {
    final UniqueKey key0 = UniqueKey();
    final UniqueKey key1 = UniqueKey();
    final UniqueKey key2 = UniqueKey();
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: TestRadioGroup<int>(
            child: Column(
              children: <Widget>[
                Radio<int>(key: key0, focusNode: focusNode, value: 0),
                Radio<int>(key: key1, value: 1),
                Radio<int>(key: key2, value: 2),
              ],
            ),
          ),
        ),
      ),
    );

    final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
      find.byType(TestRadioGroup<int>),
    );

    await tester.tap(find.byKey(key0));
    focusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(state.groupValue, 0);
    expect(focusNode.hasFocus, isTrue);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
    await tester.pumpAndSettle();
    expect(state.groupValue, 1);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
    await tester.pumpAndSettle();
    expect(state.groupValue, 2);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
    await tester.pumpAndSettle();
    // Wrap around
    expect(state.groupValue, 0);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
    await tester.pumpAndSettle();
    // Wrap around
    expect(state.groupValue, 2);

    await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
    await tester.pumpAndSettle();
    // Wrap around
    expect(state.groupValue, 1);
  });

  testWidgets('Radio group can tab in and out', (WidgetTester tester) async {
    final UniqueKey key0 = UniqueKey();
    final UniqueKey key1 = UniqueKey();
    final UniqueKey key2 = UniqueKey();
    final FocusNode radio0 = FocusNode();
    addTearDown(radio0.dispose);
    final FocusNode radio1 = FocusNode();
    addTearDown(radio1.dispose);
    final FocusNode textFieldBefore = FocusNode();
    addTearDown(textFieldBefore.dispose);
    final FocusNode textFieldAfter = FocusNode();
    addTearDown(textFieldAfter.dispose);
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Column(
            children: <Widget>[
              TextField(focusNode: textFieldBefore),
              TestRadioGroup<int>(
                child: Column(
                  children: <Widget>[
                    Radio<int>(key: key0, focusNode: radio0, value: 0),
                    Radio<int>(key: key1, focusNode: radio1, value: 1),
                    Radio<int>(key: key2, value: 2),
                  ],
                ),
              ),
              TextField(focusNode: textFieldAfter),
            ],
          ),
        ),
      ),
    );

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

    await tester.sendKeyEvent(LogicalKeyboardKey.tab);
    await tester.pump();
    // If no selected radio, focus the first.
    expect(textFieldBefore.hasFocus, isFalse);
    expect(radio0.hasFocus, isTrue);

    // tab out the radio group.
    await tester.sendKeyEvent(LogicalKeyboardKey.tab);
    await tester.pump();
    expect(radio0.hasFocus, isFalse);
    expect(radio1.hasFocus, isFalse);
    expect(textFieldAfter.hasFocus, isTrue);

    // Select the radio 1
    await tester.tap(find.byKey(key1));
    await tester.pump();
    final TestRadioGroupState<int> state = tester.state<TestRadioGroupState<int>>(
      find.byType(TestRadioGroup<int>),
    );
    expect(state.groupValue, 1);
    // focus textFieldAfter again.
    textFieldAfter.requestFocus();
    await tester.pump();
    expect(textFieldAfter.hasFocus, isTrue);

    // shift+tab in the radio again.
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
    await tester.sendKeyEvent(LogicalKeyboardKey.tab);
    await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
    await tester.pump();
    // Should focus selected radio
    expect(radio0.hasFocus, isFalse);
    expect(radio1.hasFocus, isTrue);
    expect(textFieldAfter.hasFocus, isFalse);
  });
}

class TestRadioGroup<T> extends StatefulWidget {
  const TestRadioGroup({super.key, required this.child});

  final Widget child;

  @override
  State<StatefulWidget> createState() => TestRadioGroupState<T>();
}

class TestRadioGroupState<T> extends State<TestRadioGroup<T>> {
  T? groupValue;

  @override
  Widget build(BuildContext context) {
    return RadioGroup<T>(
      onChanged: (T? newValue) {
        setState(() {
          groupValue = newValue;
        });
      },
      groupValue: groupValue,
      child: widget.child,
    );
  }
}
