// 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.

// @dart = 2.8

import 'dart:async';
import 'dart:typed_data';

import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/custom_devices.dart';
import 'package:flutter_tools/src/custom_devices/custom_device_config.dart';
import 'package:flutter_tools/src/custom_devices/custom_devices_config.dart';
import 'package:flutter_tools/src/runner/flutter_command_runner.dart';
import 'package:meta/meta.dart';

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fakes.dart';

const String linuxFlutterRoot = '/flutter';
const String windowsFlutterRoot = r'C:\flutter';

const String defaultConfigLinux1 = r'''
{
  "$schema": "file:///flutter/packages/flutter_tools/static/custom-devices.schema.json",
  "custom-devices": [
    {
      "id": "pi",
      "label": "Raspberry Pi",
      "sdkNameAndVersion": "Raspberry Pi 4 Model B+",
      "platform": "linux-arm64",
      "enabled": false,
      "ping": [
        "ping",
        "-w",
        "1",
        "-c",
        "1",
        "raspberrypi"
      ],
      "pingSuccessRegex": null,
      "postBuild": null,
      "install": [
        "scp",
        "-r",
        "-o",
        "BatchMode=yes",
        "${localPath}",
        "pi@raspberrypi:/tmp/${appName}"
      ],
      "uninstall": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "pi@raspberrypi",
        "rm -rf \"/tmp/${appName}\""
      ],
      "runDebug": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "pi@raspberrypi",
        "flutter-pi \"/tmp/${appName}\""
      ],
      "forwardPort": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "-o",
        "ExitOnForwardFailure=yes",
        "-L",
        "127.0.0.1:${hostPort}:127.0.0.1:${devicePort}",
        "pi@raspberrypi",
        "echo 'Port forwarding success'; read"
      ],
      "forwardPortSuccessRegex": "Port forwarding success",
      "screenshot": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "pi@raspberrypi",
        "fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \\n\\t'"
      ]
    }
  ]
}
''';
const String defaultConfigLinux2 = r'''
{
  "custom-devices": [
    {
      "id": "pi",
      "label": "Raspberry Pi",
      "sdkNameAndVersion": "Raspberry Pi 4 Model B+",
      "platform": "linux-arm64",
      "enabled": false,
      "ping": [
        "ping",
        "-w",
        "1",
        "-c",
        "1",
        "raspberrypi"
      ],
      "pingSuccessRegex": null,
      "postBuild": null,
      "install": [
        "scp",
        "-r",
        "-o",
        "BatchMode=yes",
        "${localPath}",
        "pi@raspberrypi:/tmp/${appName}"
      ],
      "uninstall": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "pi@raspberrypi",
        "rm -rf \"/tmp/${appName}\""
      ],
      "runDebug": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "pi@raspberrypi",
        "flutter-pi \"/tmp/${appName}\""
      ],
      "forwardPort": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "-o",
        "ExitOnForwardFailure=yes",
        "-L",
        "127.0.0.1:${hostPort}:127.0.0.1:${devicePort}",
        "pi@raspberrypi",
        "echo 'Port forwarding success'; read"
      ],
      "forwardPortSuccessRegex": "Port forwarding success",
      "screenshot": [
        "ssh",
        "-o",
        "BatchMode=yes",
        "pi@raspberrypi",
        "fbgrab /tmp/screenshot.png && cat /tmp/screenshot.png | base64 | tr -d ' \\n\\t'"
      ]
    }
  ],
  "$schema": "file:///flutter/packages/flutter_tools/static/custom-devices.schema.json"
}
''';

final Platform linuxPlatform = FakePlatform(
  environment: <String, String>{
    'FLUTTER_ROOT': linuxFlutterRoot,
    'HOME': '/',
  }
);

final Platform windowsPlatform = FakePlatform(
  operatingSystem: 'windows',
  environment: <String, String>{
    'FLUTTER_ROOT': windowsFlutterRoot,
  }
);

class FakeTerminal implements Terminal {
  factory FakeTerminal({Platform platform}) {
    return FakeTerminal._private(
        stdio: FakeStdio(),
        platform: platform
    );
  }

  FakeTerminal._private({
    this.stdio,
    Platform platform
  }) :
    terminal = AnsiTerminal(
      stdio: stdio,
      platform: platform
    );

  final FakeStdio stdio;
  final AnsiTerminal terminal;

  void simulateStdin(String line) {
    stdio.simulateStdin(line);
  }

  @override
  set usesTerminalUi(bool value) => terminal.usesTerminalUi = value;

  @override
  bool get usesTerminalUi => terminal.usesTerminalUi;

  @override
  String bolden(String message) => terminal.bolden(message);

  @override
  String clearScreen() => terminal.clearScreen();

  @override
  String color(String message, TerminalColor color) => terminal.color(message, color);

  @override
  Stream<String> get keystrokes => terminal.keystrokes;

  @override
  Future<String> promptForCharInput(
    List<String> acceptedCharacters, {
    Logger logger,
    String prompt,
    int defaultChoiceIndex,
    bool displayAcceptedCharacters = true
  }) => terminal.promptForCharInput(
      acceptedCharacters,
      logger: logger,
      prompt: prompt,
      defaultChoiceIndex: defaultChoiceIndex,
      displayAcceptedCharacters: displayAcceptedCharacters
    );

  @override
  bool get singleCharMode => terminal.singleCharMode;
  @override
  set singleCharMode(bool value) => terminal.singleCharMode = value;

  @override
  bool get stdinHasTerminal => terminal.stdinHasTerminal;

  @override
  String get successMark => terminal.successMark;

  @override
  bool get supportsColor => terminal.supportsColor;

  @override
  bool get supportsEmoji => terminal.supportsEmoji;

  @override
  String get warningMark => terminal.warningMark;

  @override
  int get preferredStyle => terminal.preferredStyle;
}

class FakeCommandRunner extends FlutterCommandRunner {
  FakeCommandRunner({
    @required Platform platform,
    @required FileSystem fileSystem,
    @required Logger logger,
    UserMessages userMessages
  }) : _platform = platform,
       _fileSystem = fileSystem,
       _logger = logger,
       _userMessages = userMessages ?? UserMessages(),
       assert(platform != null),
       assert(fileSystem != null),
       assert(logger != null);

  final Platform _platform;
  final FileSystem _fileSystem;
  final Logger _logger;
  final UserMessages _userMessages;

  @override
  Future<void> runCommand(ArgResults topLevelResults) async {
    final Logger logger = (topLevelResults['verbose'] as bool) ? VerboseLogger(_logger) : _logger;

    return context.run<void>(
      overrides: <Type, Generator>{
        Logger: () => logger
      },
      body: () {
        Cache.flutterRoot ??= Cache.defaultFlutterRoot(
          platform: _platform,
          fileSystem: _fileSystem,
          userMessages: _userMessages,
        );
        // For compatibility with tests that set this to a relative path.
        Cache.flutterRoot = _fileSystem.path.normalize(_fileSystem.path.absolute(Cache.flutterRoot));
        return super.runCommand(topLevelResults);
      }
    );
  }
}

/// May take platform, logger, processManager and fileSystem from context if
/// not explicitly specified.
CustomDevicesCommand createCustomDevicesCommand({
  CustomDevicesConfig Function(FileSystem, Logger) config,
  Terminal Function(Platform) terminal,
  Platform platform,
  FileSystem fileSystem,
  ProcessManager processManager,
  Logger logger,
  PrintFn usagePrintFn,
  bool featureEnabled = false
}) {
  platform ??= FakePlatform();
  processManager ??= FakeProcessManager.any();
  fileSystem ??= MemoryFileSystem.test();
  usagePrintFn ??= print;
  logger ??= BufferLogger.test();

  return CustomDevicesCommand.test(
    customDevicesConfig: config != null
      ? config(fileSystem, logger)
      : CustomDevicesConfig.test(
        platform: platform,
        fileSystem: fileSystem,
        directory: fileSystem.directory('/'),
        logger: logger
      ),
    operatingSystemUtils: FakeOperatingSystemUtils(
      hostPlatform: platform.isLinux ? HostPlatform.linux_x64
        : platform.isWindows ? HostPlatform.windows_x64
        : platform.isMacOS ? HostPlatform.darwin_x64
        : throw FallThroughError()
    ),
    terminal: terminal != null
      ? terminal(platform)
      : FakeTerminal(platform: platform),
    platform: platform,
    featureFlags: TestFeatureFlags(areCustomDevicesEnabled: featureEnabled),
    processManager: processManager,
    fileSystem: fileSystem,
    logger: logger,
    usagePrintFn: usagePrintFn,
  );
}

/// May take platform, logger, processManager and fileSystem from context if
/// not explicitly specified.
CommandRunner<void> createCustomDevicesCommandRunner({
  CustomDevicesConfig Function(FileSystem, Logger) config,
  Terminal Function(Platform) terminal,
  Platform platform,
  FileSystem fileSystem,
  ProcessManager processManager,
  Logger logger,
  PrintFn usagePrintFn,
  bool featureEnabled = false,
}) {
  platform ??= FakePlatform();
  fileSystem ??= MemoryFileSystem.test();
  logger ??= BufferLogger.test();

  return FakeCommandRunner(
    platform: platform,
    fileSystem: fileSystem,
    logger: logger
  )..addCommand(
    createCustomDevicesCommand(
      config: config,
      terminal: terminal,
      platform: platform,
      fileSystem: fileSystem,
      processManager: processManager,
      logger: logger,
      usagePrintFn: usagePrintFn,
      featureEnabled: featureEnabled
    )
  );
}

FakeTerminal createFakeTerminalForAddingSshDevice({
  @required Platform platform,
  @required String id,
  @required String label,
  @required String sdkNameAndVersion,
  @required String enabled,
  @required String hostname,
  @required String username,
  @required String runDebug,
  @required String usePortForwarding,
  @required String screenshot,
  @required String apply
}) {
  return FakeTerminal(platform: platform)
    ..simulateStdin(id)
    ..simulateStdin(label)
    ..simulateStdin(sdkNameAndVersion)
    ..simulateStdin(enabled)
    ..simulateStdin(hostname)
    ..simulateStdin(username)
    ..simulateStdin(runDebug)
    ..simulateStdin(usePortForwarding)
    ..simulateStdin(screenshot)
    ..simulateStdin(apply);
}

void main() {
  const String featureNotEnabledMessage = 'Custom devices feature must be enabled. Enable using `flutter config --enable-custom-devices`.';

  setUpAll(() {
    Cache.disableLocking();
  });

  group('linux', () {
    setUp(() {
      Cache.flutterRoot = linuxFlutterRoot;
    });

    testUsingContext(
      'custom-devices command shows config file in help when feature is enabled',
      () async {
        final BufferLogger logger = BufferLogger.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          logger: logger,
          usagePrintFn: (Object o) => logger.printStatus(o.toString()),
          featureEnabled: true
        );
        await expectLater(
          runner.run(const <String>['custom-devices', '--help']),
          completes
        );
        expect(
          logger.statusText,
          contains('Makes changes to the config file at "/.flutter_custom_devices.json".')
        );
      }
    );

    testUsingContext(
      'running custom-devices command without arguments prints usage',
      () async {
        final BufferLogger logger = BufferLogger.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          logger: logger,
          usagePrintFn: (Object o) => logger.printStatus(o.toString()),
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices']),
          completes
        );
        expect(
          logger.statusText,
          contains('Makes changes to the config file at "/.flutter_custom_devices.json".')
        );
      }
    );

    // test behaviour with disabled feature
    testUsingContext(
      'custom-devices add command fails when feature is not enabled',
      () async {
        final CommandRunner<void> runner = createCustomDevicesCommandRunner();
        expect(
          runner.run(const <String>['custom-devices', 'add']),
          throwsToolExit(message: featureNotEnabledMessage),
        );
      }
    );

    testUsingContext(
      'custom-devices delete command fails when feature is not enabled',
      () async {
        final CommandRunner<void> runner = createCustomDevicesCommandRunner();
        expect(
          runner.run(const <String>['custom-devices', 'delete', '-d', 'testid']),
          throwsToolExit(message: featureNotEnabledMessage),
        );
      }
    );

    testUsingContext(
      'custom-devices list command fails when feature is not enabled',
      () async {
        final CommandRunner<void> runner = createCustomDevicesCommandRunner();
        expect(
          runner.run(const <String>['custom-devices', 'list']),
          throwsToolExit(message: featureNotEnabledMessage),
        );
      }
    );

    testUsingContext(
      'custom-devices reset command fails when feature is not enabled',
      () async {
        final CommandRunner<void> runner = createCustomDevicesCommandRunner();
        expect(
          runner.run(const <String>['custom-devices', 'reset']),
          throwsToolExit(message: featureNotEnabledMessage),
        );
      }
    );

    // test add command
    testUsingContext(
      'custom-devices add command correctly adds ssh device config on linux',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          terminal: (Platform platform) => createFakeTerminalForAddingSshDevice(
            platform: platform,
            id: 'testid',
            label: 'testlabel',
            sdkNameAndVersion: 'testsdknameandversion',
            enabled: 'y',
            hostname: 'testhostname',
            username: 'testuser',
            runDebug: 'testrundebug',
            usePortForwarding: 'y',
            screenshot: 'testscreenshot',
            apply: 'y'
          ),
          fileSystem: fs,
          processManager: FakeProcessManager.any(),
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'add', '--no-check']),
          completes
        );

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test()
        );

        expect(
          config.devices,
          contains(
            CustomDeviceConfig(
              id: 'testid',
              label: 'testlabel',
              sdkNameAndVersion: 'testsdknameandversion',
              enabled: true,
              pingCommand: const <String>[
                'ping',
                '-c', '1',
                '-w', '1',
                'testhostname'
              ],
              postBuildCommand: null, // ignore: avoid_redundant_argument_values
              installCommand: const <String>[
                'scp',
                '-r',
                '-o', 'BatchMode=yes',
                r'${localPath}',
                r'testuser@testhostname:/tmp/${appName}'
              ],
              uninstallCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                r'rm -rf "/tmp/${appName}"'
              ],
              runDebugCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                'testrundebug'
              ],
              forwardPortCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-o', 'ExitOnForwardFailure=yes',
                '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}',
                'testuser@testhostname',
                "echo 'Port forwarding success'; read"
              ],
              forwardPortSuccessRegex: RegExp('Port forwarding success'),
              screenshotCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                'testscreenshot'
              ],
            )
          )
        );
      }
    );

    testUsingContext(
      'custom-devices add command correctly adds ipv4 ssh device config',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          terminal: (Platform platform) => createFakeTerminalForAddingSshDevice(
            platform: platform,
            id: 'testid',
            label: 'testlabel',
            sdkNameAndVersion: 'testsdknameandversion',
            enabled: 'y',
            hostname: '192.168.178.1',
            username: 'testuser',
            runDebug: 'testrundebug',
            usePortForwarding: 'y',
            screenshot: 'testscreenshot',
            apply: 'y',
          ),
          processManager: FakeProcessManager.any(),
          fileSystem: fs,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'add', '--no-check']),
          completes
        );

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test()
        );

        expect(
          config.devices,
          contains(
            CustomDeviceConfig(
              id: 'testid',
              label: 'testlabel',
              sdkNameAndVersion: 'testsdknameandversion',
              enabled: true,
              pingCommand: const <String>[
                'ping',
                '-c', '1',
                '-w', '1',
                '192.168.178.1'
              ],
              postBuildCommand: null, // ignore: avoid_redundant_argument_values
              installCommand: const <String>[
                'scp',
                '-r',
                '-o', 'BatchMode=yes',
                r'${localPath}',
                r'testuser@192.168.178.1:/tmp/${appName}'
              ],
              uninstallCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@192.168.178.1',
                r'rm -rf "/tmp/${appName}"'
              ],
              runDebugCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@192.168.178.1',
                'testrundebug'
              ],
              forwardPortCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-o', 'ExitOnForwardFailure=yes',
                '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}',
                'testuser@192.168.178.1',
                "echo 'Port forwarding success'; read"
              ],
              forwardPortSuccessRegex: RegExp('Port forwarding success'),
              screenshotCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@192.168.178.1',
                'testscreenshot'
              ]
            )
          )
        );
      }
    );

    testUsingContext(
      'custom-devices add command correctly adds ipv6 ssh device config',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          terminal: (Platform platform) => createFakeTerminalForAddingSshDevice(
            platform: platform,
            id: 'testid',
            label: 'testlabel',
            sdkNameAndVersion: 'testsdknameandversion',
            enabled: 'y',
            hostname: '::1',
            username: 'testuser',
            runDebug: 'testrundebug',
            usePortForwarding: 'y',
            screenshot: 'testscreenshot',
            apply: 'y',
          ),
          fileSystem: fs,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'add', '--no-check']),
          completes
        );

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test()
        );

        expect(
          config.devices,
          contains(
            CustomDeviceConfig(
              id: 'testid',
              label: 'testlabel',
              sdkNameAndVersion: 'testsdknameandversion',
              enabled: true,
              pingCommand: const <String>[
                'ping',
                '-6',
                '-c', '1',
                '-w', '1',
                '::1'
              ],
              postBuildCommand: null, // ignore: avoid_redundant_argument_values
              installCommand: const <String>[
                'scp',
                '-r',
                '-o', 'BatchMode=yes',
                '-6',
                r'${localPath}',
                r'testuser@[::1]:/tmp/${appName}'
              ],
              uninstallCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-6',
                'testuser@[::1]',
                r'rm -rf "/tmp/${appName}"'
              ],
              runDebugCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-6',
                'testuser@[::1]',
                'testrundebug'
              ],
              forwardPortCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-o', 'ExitOnForwardFailure=yes',
                '-6',
                '-L', r'[::1]:${hostPort}:[::1]:${devicePort}',
                'testuser@[::1]',
                "echo 'Port forwarding success'; read"
              ],
              forwardPortSuccessRegex: RegExp('Port forwarding success'),
              screenshotCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-6',
                'testuser@[::1]',
                'testscreenshot'
              ]
            )
          )
        );
      }
    );

    testUsingContext(
      'custom-devices add command correctly adds non-forwarding ssh device config',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          terminal: (Platform platform) => createFakeTerminalForAddingSshDevice(
            platform: platform,
            id: 'testid',
            label: 'testlabel',
            sdkNameAndVersion: 'testsdknameandversion',
            enabled: 'y',
            hostname: 'testhostname',
            username: 'testuser',
            runDebug: 'testrundebug',
            usePortForwarding: 'n',
            screenshot: 'testscreenshot',
            apply: 'y',
          ),
          fileSystem: fs,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'add', '--no-check']),
          completes
        );

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test()
        );

        expect(
          config.devices,
          contains(
            const CustomDeviceConfig(
              id: 'testid',
              label: 'testlabel',
              sdkNameAndVersion: 'testsdknameandversion',
              enabled: true,
              pingCommand: <String>[
                'ping',
                '-c', '1',
                '-w', '1',
                'testhostname'
              ],
              postBuildCommand: null, // ignore: avoid_redundant_argument_values
              installCommand: <String>[
                'scp',
                '-r',
                '-o', 'BatchMode=yes',
                r'${localPath}',
                r'testuser@testhostname:/tmp/${appName}'
              ],
              uninstallCommand: <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                r'rm -rf "/tmp/${appName}"'
              ],
              runDebugCommand: <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                'testrundebug'
              ],
              screenshotCommand: <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                'testscreenshot'
              ]
            )
          )
        );
      }
    );

    testUsingContext(
      'custom-devices add command correctly adds non-screenshotting ssh device config',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          terminal: (Platform platform) => createFakeTerminalForAddingSshDevice(
            platform: platform,
            id: 'testid',
            label: 'testlabel',
            sdkNameAndVersion: 'testsdknameandversion',
            enabled: 'y',
            hostname: 'testhostname',
            username: 'testuser',
            runDebug: 'testrundebug',
            usePortForwarding: 'y',
            screenshot: '',
            apply: 'y',
          ),
          fileSystem: fs,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'add', '--no-check']),
          completes
        );

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test()
        );

        expect(
          config.devices,
          contains(
            CustomDeviceConfig(
              id: 'testid',
              label: 'testlabel',
              sdkNameAndVersion: 'testsdknameandversion',
              enabled: true,
              pingCommand: const <String>[
                'ping',
                '-c', '1',
                '-w', '1',
                'testhostname'
              ],
              postBuildCommand: null, // ignore: avoid_redundant_argument_values
              installCommand: const <String>[
                'scp',
                '-r',
                '-o', 'BatchMode=yes',
                r'${localPath}',
                r'testuser@testhostname:/tmp/${appName}'
              ],
              uninstallCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                r'rm -rf "/tmp/${appName}"'
              ],
              runDebugCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                'testrundebug'
              ],
              forwardPortCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-o', 'ExitOnForwardFailure=yes',
                '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}',
                'testuser@testhostname',
                "echo 'Port forwarding success'; read"
              ],
              forwardPortSuccessRegex: RegExp('Port forwarding success'),
            )
          )
        );
      }
    );

    testUsingContext(
      'custom-devices delete command deletes device and creates backup',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test(),
        );

        config.add(CustomDeviceConfig.exampleUnix.copyWith(id: 'testid'));

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          config: (_, __) => config,
          fileSystem: fs,
          featureEnabled: true
        );

        final Uint8List contentsBefore = fs.file('.flutter_custom_devices.json').readAsBytesSync();

        await expectLater(
          runner.run(const <String>['custom-devices', 'delete', '-d', 'testid']),
          completes
        );
        expect(fs.file('/.flutter_custom_devices.json.bak'), exists);
        expect(config.devices, hasLength(0));

        final Uint8List backupContents = fs.file('.flutter_custom_devices.json.bak').readAsBytesSync();
        expect(contentsBefore, equals(backupContents));
      }
    );

    testUsingContext(
      'custom-devices delete command without device argument throws tool exit',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test(),
        );
        config.add(CustomDeviceConfig.exampleUnix.copyWith(id: 'testid2'));
        final Uint8List contentsBefore = fs.file('.flutter_custom_devices.json').readAsBytesSync();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          featureEnabled: true
        );
        await expectLater(
          runner.run(const <String>['custom-devices', 'delete']),
          throwsToolExit()
        );

        final Uint8List contentsAfter = fs.file('.flutter_custom_devices.json').readAsBytesSync();
        expect(contentsBefore, equals(contentsAfter));
        expect(fs.file('.flutter_custom_devices.json.bak').existsSync(), isFalse);
      }
    );

    testUsingContext(
      'custom-devices delete command throws tool exit with invalid device id',
      () async {
        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          featureEnabled: true
        );
        await expectLater(
          runner.run(const <String>['custom-devices', 'delete', '-d', 'testid']),
          throwsToolExit(message: 'Couldn\'t find device with id "testid" in config at "/.flutter_custom_devices.json"')
        );
      }
    );

    testUsingContext(
      'custom-devices list command throws tool exit when config contains errors',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();
        final BufferLogger logger = BufferLogger.test();

        fs.file('.flutter_custom_devices.json').writeAsStringSync('{"custom-devices": {}}');

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          fileSystem: fs,
          logger: logger,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'list']),
          throwsToolExit(message: 'Could not list custom devices.')
        );
        expect(
          logger.errorText,
          contains("Could not load custom devices config. config['custom-devices'] is not a JSON array.")
        );
      }
    );

    testUsingContext(
      'custom-devices list command prints message when no devices found',
      () async {
        final BufferLogger logger = BufferLogger.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          logger: logger,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'list']),
          completes
        );
        expect(
          logger.statusText,
          contains('No custom devices found in "/.flutter_custom_devices.json"')
        );
      }
    );

    testUsingContext(
      'custom-devices list command lists all devices',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();
        final BufferLogger logger = BufferLogger.test();

        CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: logger,
        )..add(
          CustomDeviceConfig.exampleUnix.copyWith(id: 'testid', label: 'testlabel', enabled: true)
        )..add(
          CustomDeviceConfig.exampleUnix.copyWith(id: 'testid2', label: 'testlabel2', enabled: false)
        );

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          logger: logger,
          fileSystem: fs,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'list']),
          completes
        );
        expect(
          logger.statusText,
          contains('List of custom devices in "/.flutter_custom_devices.json":')
        );
        expect(
          logger.statusText,
          contains('id: testid, label: testlabel, enabled: true')
        );
        expect(
          logger.statusText,
          contains('id: testid2, label: testlabel2, enabled: false')
        );
      }
    );

    testUsingContext(
      'custom-devices reset correctly backs up the config file',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();
        final BufferLogger logger = BufferLogger.test();

        CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: logger,
        )..add(
          CustomDeviceConfig.exampleUnix.copyWith(id: 'testid', label: 'testlabel', enabled: true)
        )..add(
          CustomDeviceConfig.exampleUnix.copyWith(id: 'testid2', label: 'testlabel2', enabled: false)
        );

        final Uint8List contentsBefore = fs.file('.flutter_custom_devices.json').readAsBytesSync();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          logger: logger,
          fileSystem: fs,
          featureEnabled: true
        );
        await expectLater(
          runner.run(const <String>['custom-devices', 'reset']),
          completes
        );
        expect(
          logger.statusText,
          contains(
            'Successfully resetted the custom devices config file and created a '
            'backup at "/.flutter_custom_devices.json.bak".'
          )
        );

        final Uint8List backupContents = fs.file('.flutter_custom_devices.json.bak').readAsBytesSync();
        expect(contentsBefore, equals(backupContents));
        expect(
          fs.file('.flutter_custom_devices.json').readAsStringSync(),
          anyOf(equals(defaultConfigLinux1), equals(defaultConfigLinux2))
        );
      }
    );

    testUsingContext(
      "custom-devices reset outputs correct msg when config file didn't exist",
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test();
        final BufferLogger logger = BufferLogger.test();

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          logger: logger,
          fileSystem: fs,
          featureEnabled: true
        );
        await expectLater(
          runner.run(const <String>['custom-devices', 'reset']),
          completes
        );
        expect(
          logger.statusText,
          contains(
            'Successfully resetted the custom devices config file.'
          )
        );

        expect(fs.file('.flutter_custom_devices.json.bak'), isNot(exists));
        expect(
          fs.file('.flutter_custom_devices.json').readAsStringSync(),
          anyOf(equals(defaultConfigLinux1), equals(defaultConfigLinux2))
        );
      }
    );
  });

  group('windows', () {
    setUp(() {
      Cache.flutterRoot = windowsFlutterRoot;
    });

    testUsingContext(
      'custom-devices add command correctly adds ssh device config on windows',
      () async {
        final MemoryFileSystem fs = MemoryFileSystem.test(style: FileSystemStyle.windows);

        final CommandRunner<void> runner = createCustomDevicesCommandRunner(
          terminal: (Platform platform) => createFakeTerminalForAddingSshDevice(
            platform: platform,
            id: 'testid',
            label: 'testlabel',
            sdkNameAndVersion: 'testsdknameandversion',
            enabled: 'y',
            hostname: 'testhostname',
            username: 'testuser',
            runDebug: 'testrundebug',
            usePortForwarding: 'y',
            screenshot: 'testscreenshot',
            apply: 'y',
          ),
          fileSystem: fs,
          platform: windowsPlatform,
          featureEnabled: true
        );

        await expectLater(
          runner.run(const <String>['custom-devices', 'add', '--no-check']),
          completes
        );

        final CustomDevicesConfig config = CustomDevicesConfig.test(
          fileSystem: fs,
          directory: fs.directory('/'),
          logger: BufferLogger.test()
        );

        expect(
          config.devices,
          contains(
            CustomDeviceConfig(
              id: 'testid',
              label: 'testlabel',
              sdkNameAndVersion: 'testsdknameandversion',
              enabled: true,
              pingCommand: const <String>[
                'ping',
                '-n', '1',
                '-w', '500',
                'testhostname'
              ],
              pingSuccessRegex: RegExp(r'[<=]\d+ms'),
              postBuildCommand: null, // ignore: avoid_redundant_argument_values
              installCommand: const <String>[
                'scp',
                '-r',
                '-o', 'BatchMode=yes',
                r'${localPath}',
                r'testuser@testhostname:/tmp/${appName}'
              ],
              uninstallCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                r'rm -rf "/tmp/${appName}"'
              ],
              runDebugCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                'testrundebug'
              ],
              forwardPortCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                '-o', 'ExitOnForwardFailure=yes',
                '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}',
                'testuser@testhostname',
                "echo 'Port forwarding success'; read"
              ],
              forwardPortSuccessRegex: RegExp('Port forwarding success'),
              screenshotCommand: const <String>[
                'ssh',
                '-o', 'BatchMode=yes',
                'testuser@testhostname',
                'testscreenshot'
              ]
            )
          )
        );
      },
    );
  });
}