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

import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/signals.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/run.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/pre_run_validator.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:test/fake.dart';

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart';
import '../../src/test_flutter_command_runner.dart';
import 'utils.dart';

void main() {
  group('Flutter Command', () {
    late FakeCache cache;
    late TestUsage usage;
    late FakeClock clock;
    late FakeProcessInfo processInfo;
    late MemoryFileSystem fileSystem;
    late Platform platform;
    late FileSystemUtils fileSystemUtils;
    late Logger logger;
    late FakeProcessManager processManager;
    late PreRunValidator preRunValidator;

    setUpAll(() {
      Cache.flutterRoot = '/path/to/sdk/flutter';
    });

    setUp(() {
      Cache.disableLocking();
      cache = FakeCache();
      usage = TestUsage();
      clock = FakeClock();
      processInfo = FakeProcessInfo();
      processInfo.maxRss = 10;
      fileSystem = MemoryFileSystem.test();
      platform = FakePlatform();
      fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform);
      logger = BufferLogger.test();
      processManager = FakeProcessManager.empty();
      preRunValidator = PreRunValidator(fileSystem: fileSystem);
    });

    tearDown(() {
      Cache.enableLocking();
    });

    testUsingContext('help text contains global options', () {
      final FakeDeprecatedCommand fake = FakeDeprecatedCommand();
      createTestCommandRunner(fake);
      expect(fake.usage, contains('Global options:\n'));
    });

    testUsingContext('honors shouldUpdateCache false', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
      await flutterCommand.run();

      expect(cache.artifacts, isEmpty);
      expect(flutterCommand.deprecated, isFalse);
      expect(flutterCommand.hidden, isFalse);
    },
    overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => cache,
    });

    testUsingContext('honors shouldUpdateCache true', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(shouldUpdateCache: true);
      await flutterCommand.run();
      // First call for universal, second for the rest
      expect(
        cache.artifacts,
        <Set<DevelopmentArtifact>>[
          <DevelopmentArtifact>{DevelopmentArtifact.universal},
          <DevelopmentArtifact>{},
        ],
      );
    },
    overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
      Cache: () => cache,
    });

    testUsingContext("throws toolExit if flutter_tools source dir doesn't exist", () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
      await expectToolExitLater(
        flutterCommand.run(),
        contains('Flutter SDK installation appears corrupted'),
      );
    },
    overrides: <Type, Generator>{
      Cache: () => cache,
      FileSystem: () => fileSystem,
      PreRunValidator: () => preRunValidator,
      ProcessManager: () => processManager,
    });

    testUsingContext('deprecated command should warn', () async {
      final FakeDeprecatedCommand flutterCommand = FakeDeprecatedCommand();
      final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
      await runner.run(<String>['deprecated']);

      expect(testLogger.warningText,
        contains('The "deprecated" command is deprecated and will be removed in '
            'a future version of Flutter.'));
      expect(flutterCommand.usage,
        contains('Deprecated. This command will be removed in a future version '
            'of Flutter.'));
      expect(flutterCommand.deprecated, isTrue);
      expect(flutterCommand.hidden, isTrue);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('uses the error handling file system', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          expect(globals.fs, isA<ErrorHandlingFileSystem>());
          return const FlutterCommandResult(ExitStatus.success);
        }
      );
      await flutterCommand.run();
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('finds the target file with default values', () async {
      globals.fs.file('lib/main.dart').createSync(recursive: true);
      final FakeTargetCommand fakeTargetCommand = FakeTargetCommand();
      final CommandRunner<void> runner = createTestCommandRunner(fakeTargetCommand);
      await runner.run(<String>['test']);

      expect(fakeTargetCommand.cachedTargetFile, 'lib/main.dart');
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('finds the target file with specified value', () async {
      globals.fs.file('lib/foo.dart').createSync(recursive: true);
      final FakeTargetCommand fakeTargetCommand = FakeTargetCommand();
      final CommandRunner<void> runner = createTestCommandRunner(fakeTargetCommand);
      await runner.run(<String>['test', '-t', 'lib/foo.dart']);

      expect(fakeTargetCommand.cachedTargetFile, 'lib/foo.dart');
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('throws tool exit if specified file does not exist', () async {
      final FakeTargetCommand fakeTargetCommand = FakeTargetCommand();
      final CommandRunner<void> runner = createTestCommandRunner(fakeTargetCommand);

      expect(() async => runner.run(<String>['test', '-t', 'lib/foo.dart']), throwsToolExit());
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    void testUsingCommandContext(String testName, dynamic Function() testBody) {
      testUsingContext(testName, testBody, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        ProcessInfo: () => processInfo,
        ProcessManager: () => processManager,
        SystemClock: () => clock,
        Usage: () => usage,
      });
    }

    testUsingCommandContext('reports command that results in success', () async {
      // Crash if called a third time which is unexpected.
      clock.times = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          return const FlutterCommandResult(ExitStatus.success);
        }
      );
      await flutterCommand.run();

      expect(usage.events, <TestUsageEvent>[
        const TestUsageEvent(
          'tool-command-result',
          'dummy',
          label: 'success',
        ),
        const TestUsageEvent(
          'tool-command-max-rss',
          'dummy',
          label: 'success',
          value: 10,
        ),
      ]);
    });

    testUsingCommandContext('reports command that results in warning', () async {
      // Crash if called a third time which is unexpected.
      clock.times = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          return const FlutterCommandResult(ExitStatus.warning);
        }
      );
      await flutterCommand.run();

      expect(usage.events, <TestUsageEvent>[
        const TestUsageEvent(
          'tool-command-result',
          'dummy',
          label: 'warning',
        ),
        const TestUsageEvent(
          'tool-command-max-rss',
          'dummy',
          label: 'warning',
          value: 10,
        ),
      ]);
    });

    testUsingCommandContext('reports command that results in error', () async {
      // Crash if called a third time which is unexpected.
      clock.times = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          throwToolExit('fail');
        },
      );
      await expectLater(
        () => flutterCommand.run(),
        throwsToolExit(),
      );
      expect(usage.events, <TestUsageEvent>[
        const TestUsageEvent(
          'tool-command-result',
          'dummy',
          label: 'fail',
        ),
        const TestUsageEvent(
          'tool-command-max-rss',
          'dummy',
          label: 'fail',
          value: 10,
        ),
      ]);
    });

    test('FlutterCommandResult.success()', () async {
      expect(FlutterCommandResult.success().exitStatus, ExitStatus.success);
    });

    test('FlutterCommandResult.warning()', () async {
      expect(FlutterCommandResult.warning().exitStatus, ExitStatus.warning);
    });

    testUsingContext('devToolsServerAddress returns parsed uri', () async {
      final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions(verboseHelp: false);
      await createTestCommandRunner(command).run(<String>[
        'dummy',
        '--${FlutterCommand.kDevToolsServerAddress}',
        'http://127.0.0.1:9105',
      ]);
      expect(command.devToolsServerAddress.toString(), equals('http://127.0.0.1:9105'));
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('devToolsServerAddress returns null for bad input', () async {
      final DummyFlutterCommand command = DummyFlutterCommand()..addDevToolsOptions(verboseHelp: false);
      final CommandRunner<void> runner = createTestCommandRunner(command);
      await runner.run(<String>[
        'dummy',
        '--${FlutterCommand.kDevToolsServerAddress}',
        'hello-world',
      ]);
      expect(command.devToolsServerAddress, isNull);

      await runner.run(<String>[
        'dummy',
        '--${FlutterCommand.kDevToolsServerAddress}',
        '',
      ]);
      expect(command.devToolsServerAddress, isNull);

      await runner.run(<String>[
        'dummy',
        '--${FlutterCommand.kDevToolsServerAddress}',
        '9101',
      ]);
      expect(command.devToolsServerAddress, isNull);

      await runner.run(<String>[
        'dummy',
        '--${FlutterCommand.kDevToolsServerAddress}',
        '127.0.0.1:9101',
      ]);
      expect(command.devToolsServerAddress, isNull);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    group('signals tests', () {
      late FakeIoProcessSignal mockSignal;
      late ProcessSignal signalUnderTest;
      late StreamController<io.ProcessSignal> signalController;

      setUp(() {
        mockSignal = FakeIoProcessSignal();
        signalUnderTest = ProcessSignal(mockSignal);
        signalController = StreamController<io.ProcessSignal>();
        mockSignal.stream = signalController.stream;
      });

      testUsingContext('reports command that is killed', () async {
        // Crash if called a third time which is unexpected.
        clock.times = <int>[1000, 2000];

        final Completer<void> completer = Completer<void>();
        setExitFunctionForTests((int exitCode) {
          expect(exitCode, 0);
          restoreExitFunction();
          completer.complete();
        });

        final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
          commandFunction: () async {
            final Completer<void> c = Completer<void>();
            await c.future;
            throw UnsupportedError('Unreachable');
          }
        );

        unawaited(flutterCommand.run());
        signalController.add(mockSignal);
        await completer.future;

        expect(usage.events, <TestUsageEvent>[
          const TestUsageEvent(
            'tool-command-result',
            'dummy',
            label: 'killed',
          ),
          const TestUsageEvent(
            'tool-command-max-rss',
            'dummy',
            label: 'killed',
            value: 10,
          ),
        ]);
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        ProcessManager: () => processManager,
        ProcessInfo: () => processInfo,
        Signals: () => FakeSignals(
          subForSigTerm: signalUnderTest,
          exitSignals: <ProcessSignal>[signalUnderTest],
        ),
        SystemClock: () => clock,
        Usage: () => usage,
      });

      testUsingContext('command release lock on kill signal', () async {
        clock.times = <int>[1000, 2000];
        final Completer<void> completer = Completer<void>();
        setExitFunctionForTests((int exitCode) {
          expect(exitCode, 0);
          restoreExitFunction();
          completer.complete();
        });
        final Completer<void> checkLockCompleter = Completer<void>();
        final DummyFlutterCommand flutterCommand =
            DummyFlutterCommand(commandFunction: () async {
          await globals.cache.lock();
          checkLockCompleter.complete();
          final Completer<void> c = Completer<void>();
          await c.future;
          throw UnsupportedError('Unreachable');
        });

        unawaited(flutterCommand.run());
        await checkLockCompleter.future;

        globals.cache.checkLockAcquired();

        signalController.add(mockSignal);
        await completer.future;
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        ProcessManager: () => processManager,
        ProcessInfo: () => processInfo,
        Signals: () => FakeSignals(
              subForSigTerm: signalUnderTest,
              exitSignals: <ProcessSignal>[signalUnderTest],
            ),
        Usage: () => usage,
      });
    });

    testUsingCommandContext('report execution timing by default', () async {
      // Crash if called a third time which is unexpected.
      clock.times = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
      await flutterCommand.run();

      expect(usage.timings, contains(
        const TestTimingEvent(
          'flutter',
          'dummy',
          Duration(milliseconds: 1000),
          label: 'fail',
        )));
    });

    testUsingCommandContext('no timing report without usagePath', () async {
      // Crash if called a third time which is unexpected.
      clock.times = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand =
          DummyFlutterCommand(noUsagePath: true);
      await flutterCommand.run();

      expect(usage.timings, isEmpty);
    });

    testUsingCommandContext('report additional FlutterCommandResult data', () async {
      // Crash if called a third time which is unexpected.
      clock.times = <int>[1000, 2000];

      final FlutterCommandResult commandResult = FlutterCommandResult(
        ExitStatus.success,
        // nulls should be cleaned up.
        timingLabelParts: <String?> ['blah1', 'blah2', null, 'blah3'],
        endTimeOverride: DateTime.fromMillisecondsSinceEpoch(1500),
      );

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async => commandResult
      );
      await flutterCommand.run();

      expect(usage.timings, contains(
        const TestTimingEvent(
          'flutter',
          'dummy',
          Duration(milliseconds: 500),
          label: 'success-blah1-blah2-blah3',
        )));
    });

    testUsingCommandContext('report failed execution timing too', () async {
      // Crash if called a third time which is unexpected.
      clock.times = <int>[1000, 2000];

      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
        commandFunction: () async {
          throwToolExit('fail');
        },
      );

      await expectLater(
        () => flutterCommand.run(),
        throwsToolExit(),
      );
      expect(usage.timings, contains(
        const TestTimingEvent(
          'flutter',
          'dummy',
          Duration(milliseconds: 1000),
          label: 'fail',
        ),
      ));
    });

    testUsingContext('reports null safety analytics when reportNullSafety is true', () async {
      globals.fs.file('lib/main.dart')
        ..createSync(recursive: true)
        ..writeAsStringSync('// @dart=2.12');
      globals.fs.file('pubspec.yaml')
        .writeAsStringSync('name: example\n');
      globals.fs.file('.dart_tool/package_config.json')
        ..createSync(recursive: true)
        ..writeAsStringSync(r'''
{
  "configVersion": 2,
  "packages": [
    {
      "name": "example",
      "rootUri": "../",
      "packageUri": "lib/",
      "languageVersion": "2.12"
    }
  ],
  "generated": "2020-12-02T19:30:53.862346Z",
  "generator": "pub",
  "generatorVersion": "2.12.0-76.0.dev"
}
''');
      final FakeReportingNullSafetyCommand command = FakeReportingNullSafetyCommand();
      final CommandRunner<void> runner = createTestCommandRunner(command);

      await runner.run(<String>['test']);

      expect(usage.events, containsAll(<TestUsageEvent>[
        const TestUsageEvent(
          NullSafetyAnalysisEvent.kNullSafetyCategory,
          'runtime-mode',
          label: 'NullSafetyMode.sound',
        ),
        TestUsageEvent(
          NullSafetyAnalysisEvent.kNullSafetyCategory,
          'stats',
          parameters: CustomDimensions.fromMap(<String, String>{
            'cd49': '1', 'cd50': '1',
          }),
        ),
        const TestUsageEvent(
          NullSafetyAnalysisEvent.kNullSafetyCategory,
          'language-version',
          label: '2.12',
        ),
      ]));
    }, overrides: <Type, Generator>{
      Pub: () => FakePub(),
      Usage: () => usage,
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('use packagesPath to generate BuildInfo', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(packagesPath: 'foo');
      final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
      expect(buildInfo.packagesPath, 'foo');
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('use fileSystemScheme to generate BuildInfo', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemScheme: 'foo');
      final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
      expect(buildInfo.fileSystemScheme, 'foo');
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('use fileSystemRoots to generate BuildInfo', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand(fileSystemRoots: <String>['foo', 'bar']);
      final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
      expect(buildInfo.fileSystemRoots, <String>['foo', 'bar']);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('includes initializeFromDill in BuildInfo', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand()..usesInitializeFromDillOption(hide: false);
      final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
      await runner.run(<String>['dummy', '--initialize-from-dill=/foo/bar.dill']);
      final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
      expect(buildInfo.initializeFromDill, '/foo/bar.dill');
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('includes assumeInitializeFromDillUpToDate in BuildInfo', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand()..usesInitializeFromDillOption(hide: false);
      final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
      await runner.run(<String>['dummy', '--assume-initialize-from-dill-up-to-date']);
      final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
      expect(buildInfo.assumeInitializeFromDillUpToDate, isTrue);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('unsets assumeInitializeFromDillUpToDate in BuildInfo when disabled', () async {
      final DummyFlutterCommand flutterCommand = DummyFlutterCommand()..usesInitializeFromDillOption(hide: false);
      final CommandRunner<void> runner = createTestCommandRunner(flutterCommand);
      await runner.run(<String>['dummy', '--no-assume-initialize-from-dill-up-to-date']);
      final BuildInfo buildInfo = await flutterCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
      expect(buildInfo.assumeInitializeFromDillUpToDate, isFalse);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('dds options', () async {
      final FakeDdsCommand ddsCommand = FakeDdsCommand();
      final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
      await runner.run(<String>['test', '--dds-port=1']);
      expect(ddsCommand.enableDds, isTrue);
      expect(ddsCommand.ddsPort, 1);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('dds options --dds', () async {
      final FakeDdsCommand ddsCommand = FakeDdsCommand();
      final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
      await runner.run(<String>['test', '--dds']);
      expect(ddsCommand.enableDds, isTrue);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('dds options --no-dds', () async {
      final FakeDdsCommand ddsCommand = FakeDdsCommand();
      final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
      await runner.run(<String>['test', '--no-dds']);
      expect(ddsCommand.enableDds, isFalse);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('dds options --disable-dds', () async {
      final FakeDdsCommand ddsCommand = FakeDdsCommand();
      final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
      await runner.run(<String>['test', '--disable-dds']);
      expect(ddsCommand.enableDds, isFalse);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('dds options --no-disable-dds', () async {
      final FakeDdsCommand ddsCommand = FakeDdsCommand();
      final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
      await runner.run(<String>['test', '--no-disable-dds']);
      expect(ddsCommand.enableDds, isTrue);
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    testUsingContext('dds options --dds --disable-dds', () async {
      final FakeDdsCommand ddsCommand = FakeDdsCommand();
      final CommandRunner<void> runner = createTestCommandRunner(ddsCommand);
      await runner.run(<String>['test', '--dds', '--disable-dds']);
      expect(() => ddsCommand.enableDds, throwsToolExit());
    }, overrides: <Type, Generator>{
      FileSystem: () => fileSystem,
      ProcessManager: () => processManager,
    });

    group('findTargetDevice', () {
      final FakeDevice device1 = FakeDevice('device1', 'device1');
      final FakeDevice device2 = FakeDevice('device2', 'device2');

      testUsingContext('no device found', () async {
        final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
        final Device? device = await flutterCommand.findTargetDevice();
        expect(device, isNull);
      });

      testUsingContext('finds single device', () async {
        testDeviceManager.addAttachedDevice(device1);
        final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
        final Device? device = await flutterCommand.findTargetDevice();
        expect(device, device1);
      });

      testUsingContext('finds multiple devices', () async {
        testDeviceManager.addAttachedDevice(device1);
        testDeviceManager.addAttachedDevice(device2);
        testDeviceManager.specifiedDeviceId = 'all';
        final DummyFlutterCommand flutterCommand = DummyFlutterCommand();
        final Device? device = await flutterCommand.findTargetDevice();
        expect(device, isNull);
        expect(testLogger.statusText, contains(UserMessages().flutterSpecifyDevice));
      });
    });

    group('--dart-define-from-file', () {

      late FlutterCommand dummyCommand;
      late CommandRunner<void> dummyCommandRunner;

      setUp(() {
        dummyCommand = DummyFlutterCommand()..usesDartDefineOption();
        dummyCommandRunner = createTestCommandRunner(dummyCommand);
      });

      testUsingContext('parses values from JSON files and includes them in defines list', () async {
        fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        await fileSystem.file('config1.json').writeAsString(
          '''
            {
              "kInt": 1,
              "kDouble": 1.1,
              "name": "denghaizhu",
              "title": "this is title from config json file",
              "nullValue": null,
              "containEqual": "sfadsfv=432f"
            }
          '''
        );
        await fileSystem.file('config2.json').writeAsString(
            '''
            {
              "body": "this is body from config json file"
            }
          '''
        );

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=config1.json',
          '--dart-define-from-file=config2.json',
        ]);

        final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
        expect(buildInfo.dartDefines, containsAll(const <String>[
          'kInt=1',
          'kDouble=1.1',
          'name=denghaizhu',
          'title=this is title from config json file',
          'nullValue=null',
          'containEqual=sfadsfv=432f',
          'body=this is body from config json file',
        ]));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('has values with identical keys from --dart-define take precedence', () async {
        fileSystem
          .file(fileSystem.path.join('lib', 'main.dart'))
          .createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        fileSystem.file('.env').writeAsStringSync('''
            MY_VALUE=VALUE_FROM_ENV_FILE
          ''');

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define=MY_VALUE=VALUE_FROM_COMMAND',
          '--dart-define-from-file=.env',
        ]);

        final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
        expect(buildInfo.dartDefines, containsAll(const <String>[
          'MY_VALUE=VALUE_FROM_ENV_FILE',
          'MY_VALUE=VALUE_FROM_COMMAND',
        ]));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('correctly parses a valid env file', () async {
        fileSystem
            .file(fileSystem.path.join('lib', 'main.dart'))
            .createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        await fileSystem.file('.env').writeAsString('''
            # comment
            kInt=1
            kDouble=1.1 # should be double

            name=piotrfleury
            title=this is title from config env file
            empty=

            doubleQuotes="double quotes 'value'#=" # double quotes
            singleQuotes='single quotes "value"#=' # single quotes
            backQuotes=`back quotes "value" '#=` # back quotes

            hashString="some-#-hash-string-value"

            # Play around with spaces around the equals sign.
            spaceBeforeEqual =value
            spaceAroundEqual = value
            spaceAfterEqual= value

          ''');
        await fileSystem.file('.env2').writeAsString('''
            # second comment

            body=this is body from config env file
          ''');

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=.env',
          '--dart-define-from-file=.env2',
        ]);

        final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
        expect(buildInfo.dartDefines, containsAll(const <String>[
          'kInt=1',
          'kDouble=1.1',
          'name=piotrfleury',
          'title=this is title from config env file',
          'empty=',
          "doubleQuotes=double quotes 'value'#=",
          'singleQuotes=single quotes "value"#=',
          'backQuotes=back quotes "value" \'#=',
          'hashString=some-#-hash-string-value',
          'spaceBeforeEqual=value',
          'spaceAroundEqual=value',
          'spaceAfterEqual=value',
          'body=this is body from config env file'
        ]));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('throws a ToolExit when the provided .env file is malformed', () async {
        fileSystem
            .file(fileSystem.path.join('lib', 'main.dart'))
            .createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        await fileSystem.file('.env').writeAsString('what is this');

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=.env',
        ]);

       expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug),
          throwsToolExit(message: 'Unable to parse file provided for '
          '--${FlutterOptions.kDartDefineFromFileOption}.\n'
          'Invalid property line: what is this'));

      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('throws a ToolExit when .env file contains a multiline value', () async {
        fileSystem
            .file(fileSystem.path.join('lib', 'main.dart'))
            .createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        await fileSystem.file('.env').writeAsString('''
            # single line value
            name=piotrfleury

            # multi-line value
            multiline = """ Welcome to .env demo
            a simple counter app with .env file support
            for more info, check out the README.md file
            Thanks! """ # This is the welcome message that will be displayed on the counter app

          ''');

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=.env',
        ]);
        expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug),
          throwsToolExit(message: 'Multi-line value is not supported: multiline = """ Welcome to .env demo'));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('works with mixed file formats',
          () async {
        fileSystem
            .file(fileSystem.path.join('lib', 'main.dart'))
            .createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        await fileSystem.file('.env').writeAsString('''
            kInt=1
            kDouble=1.1
            name=piotrfleury
            title=this is title from config env file
          ''');
        await fileSystem.file('config.json').writeAsString('''
            {
              "body": "this is body from config json file"
            }
          ''');

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=.env',
          '--dart-define-from-file=config.json',
        ]);

        final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
        expect(buildInfo.dartDefines, containsAll(const <String>[
          'kInt=1',
          'kDouble=1.1',
          'name=piotrfleury',
          'title=this is title from config env file',
          'body=this is body from config json file',
        ]));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('when files contain entries with duplicate keys, uses the value from the lattermost file', () async {
        fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        await fileSystem.file('config1.json').writeAsString(
            '''
            {
              "kInt": 1,
              "kDouble": 1.1,
              "name": "denghaizhu",
              "title": "this is title from config json file"
            }
          '''
        );
        await fileSystem.file('config2.json').writeAsString(
            '''
            {
              "kInt": "2"
            }
          '''
        );

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=config1.json',
          '--dart-define-from-file=config2.json',
        ]);
        final BuildInfo buildInfo = await dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug);
        expect(buildInfo.dartDefines, containsAll(const <String>[
          'kInt=2',
          'kDouble=1.1',
          'name=denghaizhu',
          'title=this is title from config json file'
        ]));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('throws a ToolExit when the argued path points to a directory', () async {
        fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        fileSystem.directory('config').createSync();

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=config',
        ]);
        expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug),
          throwsToolExit(message: 'Did not find the file passed to "--dart-define-from-file". Path: config'));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('throws a ToolExit when the given JSON file is malformed', () async {
        fileSystem.file(fileSystem.path.join('lib', 'main.dart')).createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        await fileSystem.file('config.json').writeAsString(
          '''
            {
              "kInt": 1Error json format
              "kDouble": 1.1,
              "name": "denghaizhu",
              "title": "this is title from config json file"
            }
          '''
        );

        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define-from-file=config.json',
        ]);
        expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug),
          throwsToolExit(message: 'Unable to parse the file at path "config.json" due to '
            'a formatting error. Ensure that the file contains valid JSON.\n'
            'Error details: FormatException: Missing expected digit (at line 2, character 25)'));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });

      testUsingContext('throws a ToolExit when the provided file does not exist', () async {
        fileSystem.directory('config').createSync();
        await dummyCommandRunner.run(<String>[
          'dummy',
          '--dart-define=k=v',
          '--dart-define-from-file=config']);
        expect(dummyCommand.getBuildInfo(forcedBuildMode: BuildMode.debug),
            throwsToolExit(message: 'Did not find the file passed to "--dart-define-from-file". Path: config'));
      }, overrides: <Type, Generator>{
        FileSystem: () => fileSystem,
        Logger: () => logger,
        FileSystemUtils: () => fileSystemUtils,
        Platform: () => platform,
        ProcessManager: () => processManager,
      });
    });

    group('--flavor', () {
      late _TestDeviceManager testDeviceManager;
      late Logger logger;
      late FileSystem fileSystem;

      setUp(() {
        logger = BufferLogger.test();
        testDeviceManager = _TestDeviceManager(logger: logger);
        fileSystem = MemoryFileSystem.test();
      });

      testUsingContext("tool exits when FLUTTER_APP_FLAVOR is already set in user's environment", () async {
        fileSystem.file('lib/main.dart').createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();

        final FakeDevice device = FakeDevice('name', 'id');
        testDeviceManager.devices = <Device>[device];
        final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates();
        final CommandRunner<void> runner =  createTestCommandRunner(command);

        expect(runner.run(<String>['run', '--no-pub', '--no-hot', '--flavor=strawberry']),
          throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set in the environment.'));

      }, overrides: <Type, Generator>{
        DeviceManager: () => testDeviceManager,
        Platform: () => FakePlatform(
          environment: <String, String>{
            'FLUTTER_APP_FLAVOR': 'I was already set'
          }
        ),
        Cache: () => Cache.test(processManager: FakeProcessManager.any()),
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      });

      testUsingContext('tool exits when FLUTTER_APP_FLAVOR is set in --dart-define or --dart-define-from-file', () async {
        fileSystem.file('lib/main.dart').createSync(recursive: true);
        fileSystem.file('pubspec.yaml').createSync();
        fileSystem.file('.packages').createSync();
        fileSystem.file('config.json')..createSync()..writeAsStringSync('{"FLUTTER_APP_FLAVOR": "strawberry"}');

        final FakeDevice device = FakeDevice('name', 'id');
        testDeviceManager.devices = <Device>[device];
        final _TestRunCommandThatOnlyValidates command = _TestRunCommandThatOnlyValidates();
        final CommandRunner<void> runner =  createTestCommandRunner(command);

        expect(runner.run(<String>['run', '--dart-define=FLUTTER_APP_FLAVOR=strawberry', '--no-pub', '--no-hot', '--flavor=strawberry']),
          throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file'));

        expect(runner.run(<String>['run', '--dart-define-from-file=config.json', '--no-pub', '--no-hot', '--flavor=strawberry']),
          throwsToolExit(message: 'FLUTTER_APP_FLAVOR is used by the framework and cannot be set using --dart-define or --dart-define-from-file'));
      }, overrides: <Type, Generator>{
        DeviceManager: () => testDeviceManager,
        Platform: () => FakePlatform(),
        Cache: () => Cache.test(processManager: FakeProcessManager.any()),
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      });
    });
  });
}

class FakeDeprecatedCommand extends FlutterCommand {
  @override
  String get description => 'A fake command';

  @override
  String get name => 'deprecated';

  @override
  bool get deprecated => true;

  @override
  Future<FlutterCommandResult> runCommand() async {
    return FlutterCommandResult.success();
  }
}

class FakeTargetCommand extends FlutterCommand {
  FakeTargetCommand() {
    usesTargetOption();
  }

  @override
  Future<FlutterCommandResult> runCommand() async {
    cachedTargetFile = targetFile;
    return FlutterCommandResult.success();
  }

  String? cachedTargetFile;

  @override
  String get description => '';

  @override
  String get name => 'test';
}

class FakeReportingNullSafetyCommand extends FlutterCommand {
  FakeReportingNullSafetyCommand() {
    argParser.addFlag('debug');
    argParser.addFlag('release');
    argParser.addFlag('jit-release');
    argParser.addFlag('profile');
  }

  @override
  String get description => 'test';

  @override
  String get name => 'test';

  @override
  bool get shouldRunPub => true;

  @override
  bool get reportNullSafety => true;

  @override
  Future<FlutterCommandResult> runCommand() async {
    return FlutterCommandResult.success();
  }
}

class FakeDdsCommand extends FlutterCommand {
  FakeDdsCommand() {
    addDdsOptions(verboseHelp: false);
  }

  @override
  String get description => 'test';

  @override
  String get name => 'test';

  @override
  Future<FlutterCommandResult> runCommand() async {
    return FlutterCommandResult.success();
  }
}

class FakeProcessInfo extends Fake implements ProcessInfo {
  @override
  int maxRss = 0;
}

class FakeIoProcessSignal extends Fake implements io.ProcessSignal {
  late Stream<io.ProcessSignal> stream;

  @override
  Stream<io.ProcessSignal> watch() => stream;
}

class FakeCache extends Fake implements Cache {
  List<Set<DevelopmentArtifact>> artifacts = <Set<DevelopmentArtifact>>[];

  @override
  Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts, {bool offline = false}) async {
    artifacts.add(requiredArtifacts.toSet());
  }

  @override
  void releaseLock() { }
}

class FakeSignals implements Signals {
  FakeSignals({
    required this.subForSigTerm,
    required List<ProcessSignal> exitSignals,
  }) : delegate = Signals.test(exitSignals: exitSignals);

  final ProcessSignal subForSigTerm;
  final Signals delegate;

  @override
  Object addHandler(ProcessSignal signal, SignalHandler handler) {
    if (signal == ProcessSignal.sigterm) {
      return delegate.addHandler(subForSigTerm, handler);
    }
    return delegate.addHandler(signal, handler);
  }

  @override
  Future<bool> removeHandler(ProcessSignal signal, Object token) =>
    delegate.removeHandler(signal, token);

  @override
  Stream<Object> get errors => delegate.errors;
}

class FakeClock extends Fake implements SystemClock {
  List<int> times = <int>[];

  @override
  DateTime now() {
    return DateTime.fromMillisecondsSinceEpoch(times.removeAt(0));
  }
}

class FakePub extends Fake implements Pub {
  @override
  Future<void> get({
    required PubContext context,
    required FlutterProject project,
    bool upgrade = false,
    bool offline = false,
    String? flutterRootOverride,
    bool checkUpToDate = false,
    bool shouldSkipThirdPartyGenerator = true,
    PubOutputMode outputMode = PubOutputMode.all,
  }) async { }
}

class _TestDeviceManager extends DeviceManager {
  _TestDeviceManager({required super.logger});
  List<Device> devices = <Device>[];

  @override
  List<DeviceDiscovery> get deviceDiscoverers {
    final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
    devices.forEach(discoverer.addDevice);
    return <DeviceDiscovery>[discoverer];
  }
}

class _TestRunCommandThatOnlyValidates extends RunCommand {
  @override
  Future<FlutterCommandResult> runCommand() async {
    return FlutterCommandResult.success();
  }
}
