// 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 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.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/build_info.dart';
import 'package:flutter_tools/src/desktop_device.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/project.dart';

import 'package:test/fake.dart';

import '../src/common.dart';
import '../src/fake_process_manager.dart';

void main() {
  group('Basic info', () {
    testWithoutContext('Category is desktop', () async {
      final FakeDesktopDevice device = setUpDesktopDevice();

      expect(device.category, Category.desktop);
    });

    testWithoutContext('Not an emulator', () async {
      final FakeDesktopDevice device = setUpDesktopDevice();

      expect(await device.isLocalEmulator, false);
      expect(await device.emulatorId, null);
    });

    testWithoutContext('Uses OS name as SDK name', () async {
      final FakeDesktopDevice device = setUpDesktopDevice();

      expect(await device.sdkNameAndVersion, 'Example');
    });
  });

  group('Install', () {
    testWithoutContext('Install checks always return true', () async {
      final FakeDesktopDevice device = setUpDesktopDevice();

      expect(await device.isAppInstalled(FakeApplicationPackage()), true);
      expect(await device.isLatestBuildInstalled(FakeApplicationPackage()), true);
      expect(device.category, Category.desktop);
    });

    testWithoutContext('Install and uninstall are no-ops that report success', () async {
      final FakeDesktopDevice device = setUpDesktopDevice();
      final FakeApplicationPackage package = FakeApplicationPackage();

      expect(await device.uninstallApp(package), true);
      expect(await device.isAppInstalled(package), true);
      expect(await device.isLatestBuildInstalled(package), true);

      expect(await device.installApp(package), true);
      expect(await device.isAppInstalled(package), true);
      expect(await device.isLatestBuildInstalled(package), true);
      expect(device.category, Category.desktop);
    });
  });

  group('Starting and stopping application', () {
    testWithoutContext('Stop without start is a successful no-op', () async {
      final FakeDesktopDevice device = setUpDesktopDevice();
      final FakeApplicationPackage package = FakeApplicationPackage();

      expect(await device.stopApp(package), true);
    });

    testWithoutContext('Can run from prebuilt application', () async {
      final FileSystem fileSystem = MemoryFileSystem.test();
      final Completer<void> completer = Completer<void>();
      final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
        FakeCommand(
          command: const <String>['debug'],
          stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n',
          completer: completer,
        ),
      ]);
      final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager, fileSystem: fileSystem);
      final String? executableName = device.executablePathForDevice(FakeApplicationPackage(), BuildInfo.debug);
      fileSystem.file(executableName).writeAsStringSync('\n');
      final FakeApplicationPackage package = FakeApplicationPackage();
      final LaunchResult result = await device.startApp(
        package,
        prebuiltApplication: true,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      );

      expect(result.started, true);
      expect(result.vmServiceUri, Uri.parse('http://127.0.0.1/0'));
    });

    testWithoutContext('Null executable path fails gracefully', () async {
      final BufferLogger logger = BufferLogger.test();
      final DesktopDevice device = setUpDesktopDevice(nullExecutablePathForDevice: true, logger: logger);
      final FakeApplicationPackage package = FakeApplicationPackage();
      final LaunchResult result = await device.startApp(
        package,
        prebuiltApplication: true,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      );

      expect(result.started, false);
      expect(logger.errorText, contains('Unable to find executable to run'));
    });

    testWithoutContext('stopApp kills process started by startApp', () async {
      final Completer<void> completer = Completer<void>();
      final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
        FakeCommand(
          command: const <String>['debug'],
          stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n',
          completer: completer,
        ),
      ]);
      final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager);
      final FakeApplicationPackage package = FakeApplicationPackage();
      final LaunchResult result = await device.startApp(
        package,
        prebuiltApplication: true,
        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
      );

      expect(result.started, true);
      expect(await device.stopApp(package), true);
    });
  });

  testWithoutContext('startApp supports DebuggingOptions through FLUTTER_ENGINE_SWITCH environment variables', () async {
    final Completer<void> completer = Completer<void>();
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: const <String>['debug'],
        stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n',
        completer: completer,
        environment: const <String, String>{
          'FLUTTER_ENGINE_SWITCH_1': 'enable-dart-profiling=true',
          'FLUTTER_ENGINE_SWITCH_2': 'trace-startup=true',
          'FLUTTER_ENGINE_SWITCH_3': 'enable-software-rendering=true',
          'FLUTTER_ENGINE_SWITCH_4': 'skia-deterministic-rendering=true',
          'FLUTTER_ENGINE_SWITCH_5': 'trace-skia=true',
          'FLUTTER_ENGINE_SWITCH_6': 'trace-allowlist=foo,bar',
          'FLUTTER_ENGINE_SWITCH_7': 'trace-skia-allowlist=skia.a,skia.b',
          'FLUTTER_ENGINE_SWITCH_8': 'trace-systrace=true',
          'FLUTTER_ENGINE_SWITCH_9': 'trace-to-file=path/to/trace.binpb',
          'FLUTTER_ENGINE_SWITCH_10': 'endless-trace-buffer=true',
          'FLUTTER_ENGINE_SWITCH_11': 'dump-skp-on-shader-compilation=true',
          'FLUTTER_ENGINE_SWITCH_12': 'cache-sksl=true',
          'FLUTTER_ENGINE_SWITCH_13': 'purge-persistent-cache=true',
          'FLUTTER_ENGINE_SWITCH_14': 'enable-impeller=false',
          'FLUTTER_ENGINE_SWITCH_15': 'enable-checked-mode=true',
          'FLUTTER_ENGINE_SWITCH_16': 'verify-entry-points=true',
          'FLUTTER_ENGINE_SWITCH_17': 'start-paused=true',
          'FLUTTER_ENGINE_SWITCH_18': 'disable-service-auth-codes=true',
          'FLUTTER_ENGINE_SWITCH_19': 'dart-flags=--null_assertions',
          'FLUTTER_ENGINE_SWITCH_20': 'use-test-fonts=true',
          'FLUTTER_ENGINE_SWITCH_21': 'verbose-logging=true',
          'FLUTTER_ENGINE_SWITCHES': '21',
        }
      ),
    ]);
    final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager);
    final FakeApplicationPackage package = FakeApplicationPackage();
    final LaunchResult result = await device.startApp(
      package,
      prebuiltApplication: true,
      platformArgs: <String, Object>{
        'trace-startup': true,
      },
      debuggingOptions: DebuggingOptions.enabled(
        BuildInfo.debug,
        startPaused: true,
        disableServiceAuthCodes: true,
        enableSoftwareRendering: true,
        skiaDeterministicRendering: true,
        traceSkia: true,
        traceAllowlist: 'foo,bar',
        traceSkiaAllowlist: 'skia.a,skia.b',
        traceSystrace: true,
        traceToFile: 'path/to/trace.binpb',
        endlessTraceBuffer: true,
        dumpSkpOnShaderCompilation: true,
        cacheSkSL: true,
        purgePersistentCache: true,
        useTestFonts: true,
        verboseSystemLogs: true,
        nullAssertions: true,
      ),
    );

    expect(result.started, true);
  });

  testWithoutContext('startApp supports DebuggingOptions through FLUTTER_ENGINE_SWITCH environment variables when debugging is disabled', () async {
    final Completer<void> completer = Completer<void>();
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: const <String>['debug'],
        stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n',
        completer: completer,
        environment: const <String, String>{
          'FLUTTER_ENGINE_SWITCH_1': 'enable-dart-profiling=true',
          'FLUTTER_ENGINE_SWITCH_2': 'trace-startup=true',
          'FLUTTER_ENGINE_SWITCH_3': 'trace-allowlist=foo,bar',
          'FLUTTER_ENGINE_SWITCH_4': 'cache-sksl=true',
          'FLUTTER_ENGINE_SWITCH_5': 'enable-impeller=false',
          'FLUTTER_ENGINE_SWITCHES': '5',
        }
      ),
    ]);
    final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager);
    final FakeApplicationPackage package = FakeApplicationPackage();
    final LaunchResult result = await device.startApp(
      package,
      prebuiltApplication: true,
      platformArgs: <String, Object>{
        'trace-startup': true,
      },
      debuggingOptions: DebuggingOptions.disabled(
        BuildInfo.debug,
        traceAllowlist: 'foo,bar',
        cacheSkSL: true,
      ),
    );

    expect(result.started, true);
  });

  testWithoutContext('Port forwarder is a no-op', () async {
    final FakeDesktopDevice device = setUpDesktopDevice();
    final DevicePortForwarder portForwarder = device.portForwarder;
    final int result = await portForwarder.forward(2);

    expect(result, 2);
    expect(portForwarder.forwardedPorts.isEmpty, true);
  });

  testWithoutContext('createDevFSWriter returns a LocalDevFSWriter', () {
    final FakeDesktopDevice device = setUpDesktopDevice();

    expect(device.createDevFSWriter(FakeApplicationPackage(), ''), isA<LocalDevFSWriter>());
  });

  testWithoutContext('startApp supports dartEntrypointArgs', () async {
    final Completer<void> completer = Completer<void>();
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: const <String>['debug', 'arg1', 'arg2'],
        stdout: 'The Dart VM service is listening on http://127.0.0.1/0\n',
        completer: completer,
      ),
    ]);
    final FakeDesktopDevice device = setUpDesktopDevice(processManager: processManager);
    final FakeApplicationPackage package = FakeApplicationPackage();
    final LaunchResult result = await device.startApp(
      package,
      prebuiltApplication: true,
      debuggingOptions: DebuggingOptions.enabled(
        BuildInfo.debug,
        dartEntrypointArgs: <String>['arg1', 'arg2'],
      ),
    );

    expect(result.started, true);
  });

  testWithoutContext('Device logger captures all output', () async {
    final Completer<void> exitCompleter = Completer<void>();
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      FakeCommand(
        command: const <String>['debug', 'arg1', 'arg2'],
        exitCode: -1,
        stderr: 'Oops\n',
        completer: exitCompleter,
        outputFollowsExit: true,
      ),
    ]);
    final FakeDesktopDevice device = setUpDesktopDevice(
      processManager: processManager,
    );
    unawaited(Future<void>(() {
      exitCompleter.complete();
    }));

    // Start looking for 'Oops' in the stream before starting the app.
    expect(device.getLogReader().logLines, emits('Oops'));

    final FakeApplicationPackage package = FakeApplicationPackage();
    await device.startApp(
      package,
      prebuiltApplication: true,
      debuggingOptions: DebuggingOptions.enabled(
        BuildInfo.debug,
        dartEntrypointArgs: <String>['arg1', 'arg2'],
      ),
    );
  });

  testWithoutContext('Desktop devices pass through the enable-impeller flag', () async {
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      const FakeCommand(
        command: <String>['debug'],
        exitCode: -1,
        environment: <String, String>{
          'FLUTTER_ENGINE_SWITCH_1': 'enable-dart-profiling=true',
          'FLUTTER_ENGINE_SWITCH_2': 'enable-impeller=true',
          'FLUTTER_ENGINE_SWITCH_3': 'enable-checked-mode=true',
          'FLUTTER_ENGINE_SWITCH_4': 'verify-entry-points=true',
          'FLUTTER_ENGINE_SWITCHES': '4'
        }
      ),
    ]);
    final FakeDesktopDevice device = setUpDesktopDevice(
      processManager: processManager,
    );

    final FakeApplicationPackage package = FakeApplicationPackage();
    await device.startApp(
      package,
      prebuiltApplication: true,
      debuggingOptions: DebuggingOptions.enabled(
        BuildInfo.debug,
        enableImpeller: ImpellerStatus.enabled,
        dartEntrypointArgs: <String>[],
      ),
    );
  });

  testWithoutContext('Desktop devices pass through the --no-enable-impeller flag', () async {
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      const FakeCommand(
        command: <String>['debug'],
        exitCode: -1,
        environment: <String, String>{
          'FLUTTER_ENGINE_SWITCH_1': 'enable-dart-profiling=true',
          'FLUTTER_ENGINE_SWITCH_2': 'enable-impeller=false',
          'FLUTTER_ENGINE_SWITCH_3': 'enable-checked-mode=true',
          'FLUTTER_ENGINE_SWITCH_4': 'verify-entry-points=true',
          'FLUTTER_ENGINE_SWITCHES': '4'
        }
      ),
    ]);
    final FakeDesktopDevice device = setUpDesktopDevice(
      processManager: processManager,
    );

    final FakeApplicationPackage package = FakeApplicationPackage();
    await device.startApp(
      package,
      prebuiltApplication: true,
      debuggingOptions: DebuggingOptions.enabled(
        BuildInfo.debug,
        enableImpeller: ImpellerStatus.disabled,
        dartEntrypointArgs: <String>[],
      ),
    );
  });
}

FakeDesktopDevice setUpDesktopDevice({
  FileSystem? fileSystem,
  Logger? logger,
  ProcessManager? processManager,
  OperatingSystemUtils? operatingSystemUtils,
  bool nullExecutablePathForDevice = false,
}) {
  return FakeDesktopDevice(
    fileSystem: fileSystem ?? MemoryFileSystem.test(),
    logger: logger ?? BufferLogger.test(),
    processManager: processManager ?? FakeProcessManager.any(),
    operatingSystemUtils: operatingSystemUtils ?? FakeOperatingSystemUtils(),
    nullExecutablePathForDevice: nullExecutablePathForDevice,
  );
}

/// A trivial subclass of DesktopDevice for testing the shared functionality.
class FakeDesktopDevice extends DesktopDevice {
  FakeDesktopDevice({
    required ProcessManager processManager,
    required Logger logger,
    required FileSystem fileSystem,
    required OperatingSystemUtils operatingSystemUtils,
    this.nullExecutablePathForDevice = false,
  }) : super(
      'dummy',
      platformType: PlatformType.linux,
      ephemeral: false,
      processManager: processManager,
      logger: logger,
      fileSystem: fileSystem,
      operatingSystemUtils: operatingSystemUtils,
  );

  /// The [mainPath] last passed to [buildForDevice].
  String? lastBuiltMainPath;

  /// The [buildInfo] last passed to [buildForDevice].
  BuildInfo? lastBuildInfo;

  final bool nullExecutablePathForDevice;

  @override
  String get name => 'dummy';

  @override
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.tester;

  @override
  bool isSupported() => true;

  @override
  bool isSupportedForProject(FlutterProject flutterProject) => true;

  @override
  Future<void> buildForDevice({
    String? mainPath,
    BuildInfo? buildInfo,
  }) async {
    lastBuiltMainPath = mainPath;
    lastBuildInfo = buildInfo;
  }

  // Dummy implementation that just returns the build mode name.
  @override
  String? executablePathForDevice(ApplicationPackage package, BuildInfo buildInfo) {
    if (nullExecutablePathForDevice) {
      return null;
    }
    return buildInfo.mode.cliName;
  }
}

class FakeApplicationPackage extends Fake implements ApplicationPackage { }
class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
  @override
  String get name => 'Example';
}
