// 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/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/io.dart' as io;
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/drive/drive_service.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:package_config/package_config_types.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;

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


final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: vm_service.Event(
    kind: vm_service.EventKind.kResume,
    timestamp: 0
  ),
  breakpoints: <vm_service.Breakpoint>[],
  libraries: <vm_service.LibraryRef>[
    vm_service.LibraryRef(
      id: '1',
      uri: 'file:///hello_world/main.dart',
      name: '',
    ),
  ],
  livePorts: 0,
  name: 'test',
  number: '1',
  pauseOnExit: false,
  runnable: true,
  startTime: 0,
  isSystemIsolate: false,
  isolateFlags: <vm_service.IsolateFlag>[],
);

final vm_service.VM fakeVM = vm_service.VM(
  isolates: <vm_service.IsolateRef>[fakeUnpausedIsolate],
  pid: 1,
  hostCPU: '',
  isolateGroups: <vm_service.IsolateGroupRef>[],
  targetCPU: '',
  startTime: 0,
  name: 'dart',
  architectureBits: 64,
  operatingSystem: '',
  version: '',
  systemIsolateGroups: <vm_service.IsolateGroupRef>[],
  systemIsolates: <vm_service.IsolateRef>[],
);

final FlutterView fakeFlutterView = FlutterView(
  id: 'a',
  uiIsolate: fakeUnpausedIsolate,
);

final FakeVmServiceRequest listViews = FakeVmServiceRequest(
  method: kListViewsMethod,
  jsonResponse: <String, Object>{
    'views': <Object>[
      fakeFlutterView.toJson(),
    ],
  },
);

final FakeVmServiceRequest getVM = FakeVmServiceRequest(
  method: 'getVM',
  args: <String, Object>{},
  jsonResponse: fakeVM.toJson(),
);

void main() {
  testWithoutContext('Exits if device fails to start', () {
    final DriverService driverService = setUpDriverService();
    final Device device = FakeDevice(LaunchResult.failed());

    expect(
      () => driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true),
      throwsToolExit(message: 'Application failed to start. Will not run test. Quitting.'),
    );
  });

  testWithoutContext('Retries application launch if it fails the first time', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
    ]);
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      const FakeCommand(
        command: <String>['dart', '--enable-experiment=non-nullable', 'foo.test', '-rexpanded'],
        exitCode: 23,
        environment: <String, String>{
          'FOO': 'BAR',
          'VM_SERVICE_URL': 'http://127.0.0.1:1234/', // dds forwarded URI
        },
      ),
    ]);
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final Device device = FakeDevice(LaunchResult.succeeded(
      vmServiceUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
    ))..failOnce = true;

    await expectLater(
      () async => driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true),
      returnsNormally,
    );
  });

  testWithoutContext('Connects to device VM Service and runs test application', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
    ]);
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      const FakeCommand(
        command: <String>['dart', '--enable-experiment=non-nullable', 'foo.test', '-rexpanded'],
        exitCode: 23,
        environment: <String, String>{
          'FOO': 'BAR',
          'VM_SERVICE_URL': 'http://127.0.0.1:1234/', // dds forwarded URI
        },
      ),
    ]);
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final Device device = FakeDevice(LaunchResult.succeeded(
      vmServiceUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
    ));

    await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true);
    final int testResult = await driverService.startTest(
      'foo.test',
      <String>['--enable-experiment=non-nullable'],
      <String, String>{'FOO': 'BAR'},
      PackageConfig(<Package>[Package('test', Uri.base)]),
    );

    expect(testResult, 23);
  });

  testWithoutContext('Connects to device VM Service and runs test application with devtools memory profile', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
    ]);
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      const FakeCommand(
        command: <String>['dart', '--enable-experiment=non-nullable', 'foo.test', '-rexpanded'],
        exitCode: 23,
        environment: <String, String>{
          'FOO': 'BAR',
          'VM_SERVICE_URL': 'http://127.0.0.1:1234/', // dds forwarded URI
        },
      ),
    ]);
    final FakeDevtoolsLauncher launcher = FakeDevtoolsLauncher();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService, devtoolsLauncher: launcher);
    final Device device = FakeDevice(LaunchResult.succeeded(
      vmServiceUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
    ));

    await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true);
    final int testResult = await driverService.startTest(
      'foo.test',
      <String>['--enable-experiment=non-nullable'],
      <String, String>{'FOO': 'BAR'},
      PackageConfig(<Package>[Package('test', Uri.base)]),
      profileMemory: 'devtools_memory.json',
    );

    expect(launcher.closed, true);
    expect(testResult, 23);
  });

  testWithoutContext('Uses dart to execute the test if there is no package:test dependency', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
    ]);
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      const FakeCommand(
        command: <String>['dart', '--enable-experiment=non-nullable', 'foo.test', '-rexpanded'],
        exitCode: 23,
        environment: <String, String>{
          'FOO': 'BAR',
          'VM_SERVICE_URL': 'http://127.0.0.1:1234/', // dds forwarded URI
        },
      ),
    ]);
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final Device device = FakeDevice(LaunchResult.succeeded(
      vmServiceUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
    ));

    await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true);
    final int testResult = await driverService.startTest(
      'foo.test',
      <String>['--enable-experiment=non-nullable'],
      <String, String>{'FOO': 'BAR'},
      PackageConfig.empty,
    );

    expect(testResult, 23);
  });


  testWithoutContext('Connects to device VM Service and runs test application without dds', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
    ]);
    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
      const FakeCommand(
        command: <String>['dart', 'foo.test', '-rexpanded'],
        exitCode: 11,
        environment: <String, String>{
          'VM_SERVICE_URL': 'http://127.0.0.1:63426/1UasC_ihpXY=/',
        },
      ),
    ]);
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final Device device = FakeDevice(LaunchResult.succeeded(
      vmServiceUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
    ));
    final FakeDartDevelopmentService dds = device.dds as FakeDartDevelopmentService;

    expect(dds.started, false);
    await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile, enableDds: false), true);
    expect(dds.started, false);

    final int testResult = await driverService.startTest(
      'foo.test',
      <String>[],
      <String, String>{},
      PackageConfig(<Package>[Package('test', Uri.base)]),
    );

    expect(testResult, 11);
    expect(dds.started, false);
  });

  testWithoutContext('Safely stops and uninstalls application', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
    ]);
    final FakeProcessManager processManager = FakeProcessManager.empty();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final FakeDevice device = FakeDevice(LaunchResult.succeeded(
      vmServiceUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
    ));

    await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true);
    await driverService.stop();

    expect(device.didStopApp, true);
    expect(device.didUninstallApp, true);
    expect(device.didDispose, true);
  });

  // FlutterVersion requires context.
  testUsingContext('Writes SkSL to file when provided with out file', () async {
    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
      listViews,
      const FakeVmServiceRequest(
        method: '_flutter.getSkSLs',
        args: <String, Object>{
          'viewId': 'a',
        },
        jsonResponse: <String, Object>{
          'SkSLs': <String, Object>{
            'A': 'B',
          },
        },
      ),
    ]);
    final FakeProcessManager processManager = FakeProcessManager.empty();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final FakeDevice device = FakeDevice(LaunchResult.succeeded(
      vmServiceUri: Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
    ));

    await driverService.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true);
    await driverService.stop(writeSkslOnExit: fileSystem.file('out.json'));

    expect(device.didStopApp, true);
    expect(device.didUninstallApp, true);
    expect(json.decode(fileSystem.file('out.json').readAsStringSync()), <String, Object>{
      'platform': 'android',
      'name': 'test',
      'engineRevision': 'abcdefghijklmnopqrstuvwxyz',
      'data': <String, Object>{'A': 'B'},
    });
  }, overrides: <Type, Generator>{
    FlutterVersion: () => FakeFlutterVersion(),
  });

  testWithoutContext('Can connect to existing application and stop it during cleanup', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
      getVM,
      const FakeVmServiceRequest(
        method: 'ext.flutter.exit',
        args: <String, Object>{
          'isolateId': '1',
        },
      ),
    ]);
    final FakeProcessManager processManager = FakeProcessManager.empty();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final FakeDevice device = FakeDevice(LaunchResult.failed());

    await driverService.reuseApplication(
      Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
      device,
      DebuggingOptions.enabled(BuildInfo.debug),
      false,
    );
    await driverService.stop();
  });

  testWithoutContext('Can connect to existing application using ws URI', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
      getVM,
      const FakeVmServiceRequest(
        method: 'ext.flutter.exit',
        args: <String, Object>{
          'isolateId': '1',
        },
      ),
    ]);
    final FakeProcessManager processManager = FakeProcessManager.empty();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final FakeDevice device = FakeDevice(LaunchResult.failed());

    await driverService.reuseApplication(
      Uri.parse('ws://127.0.0.1:63426/1UasC_ihpXY=/ws/'),
      device,
      DebuggingOptions.enabled(BuildInfo.debug),
      false,
    );
    await driverService.stop();
  });

  testWithoutContext('Can connect to existing application using ws URI (no trailing slash)', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
      getVM,
      const FakeVmServiceRequest(
        method: 'ext.flutter.exit',
        args: <String, Object>{
          'isolateId': '1',
        },
      ),
    ]);
    final FakeProcessManager processManager = FakeProcessManager.empty();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final FakeDevice device = FakeDevice(LaunchResult.failed());

    await driverService.reuseApplication(
      Uri.parse('ws://127.0.0.1:63426/1UasC_ihpXY=/ws'),
      device,
      DebuggingOptions.enabled(BuildInfo.debug),
      false,
    );
    await driverService.stop();
  });

  testWithoutContext('Can connect to existing application using ws URI (no trailing slash, ws in auth code)', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
      getVM,
      const FakeVmServiceRequest(
        method: 'ext.flutter.exit',
        args: <String, Object>{
          'isolateId': '1',
        },
      ),
    ]);
    final FakeProcessManager processManager = FakeProcessManager.empty();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final FakeDevice device = FakeDevice(LaunchResult.failed());

    await driverService.reuseApplication(
      Uri.parse('ws://127.0.0.1:63426/wsasC_ihpXY=/ws'),
      device,
      DebuggingOptions.enabled(BuildInfo.debug),
      false,
    );
    await driverService.stop();
  });

  testWithoutContext('Does not call flutterExit on device types that do not support it', () async {
    final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[
      getVM,
    ]);
    final FakeProcessManager processManager = FakeProcessManager.empty();
    final DriverService driverService = setUpDriverService(processManager: processManager, vmService: fakeVmServiceHost.vmService);
    final FakeDevice device = FakeDevice(LaunchResult.failed(), supportsFlutterExit: false);

    await driverService.reuseApplication(
      Uri.parse('http://127.0.0.1:63426/1UasC_ihpXY=/'),
      device,
      DebuggingOptions.enabled(BuildInfo.debug),
      false,
    );
    await driverService.stop();
  });
}

FlutterDriverService setUpDriverService({
  Logger? logger,
  ProcessManager? processManager,
  FlutterVmService? vmService,
  DevtoolsLauncher? devtoolsLauncher,
}) {
  logger ??= BufferLogger.test();
  return FlutterDriverService(
    applicationPackageFactory: FakeApplicationPackageFactory(FakeApplicationPackage()),
    logger: logger,
    processUtils: ProcessUtils(
      logger: logger,
      processManager: processManager ?? FakeProcessManager.any(),
    ),
    dartSdkPath: 'dart',
    devtoolsLauncher: devtoolsLauncher ?? FakeDevtoolsLauncher(),
    vmServiceConnector: (Uri httpUri, {
      ReloadSources? reloadSources,
      Restart? restart,
      CompileExpression? compileExpression,
      GetSkSLMethod? getSkSLMethod,
      FlutterProject? flutterProject,
      PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
      io.CompressionOptions compression = io.CompressionOptions.compressionDefault,
      Device? device,
      required Logger logger,
    }) async {
      if (httpUri.scheme != 'http') {
        fail('Expected an HTTP scheme, found $httpUri');
      }
      if (httpUri.path.endsWith('/ws')) {
        fail('Expected HTTP uri to not contain `/ws`, found $httpUri');
      }
      return vmService!;
    }
  );
}

class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFactory {
  FakeApplicationPackageFactory(this.applicationPackage);

  ApplicationPackage applicationPackage;

  @override
  Future<ApplicationPackage> getPackageForPlatform(
    TargetPlatform platform, {
    BuildInfo? buildInfo,
    File? applicationBinary,
  }) async => applicationPackage;
}

class FakeApplicationPackage extends Fake implements ApplicationPackage { }

// Unfortunately Device, despite not being immutable, has an `operator ==`.
// Until we fix that, we have to also ignore related lints here.
// ignore: avoid_implementing_value_types
class FakeDevice extends Fake implements Device {
  FakeDevice(this.result, {this.supportsFlutterExit = true});

  LaunchResult result;
  bool didStopApp = false;
  bool didUninstallApp = false;
  bool didDispose = false;
  bool failOnce = false;
  @override
  final PlatformType platformType = PlatformType.web;

  @override
  String get name => 'test';

  @override
  final bool supportsFlutterExit;

  @override
  final DartDevelopmentService dds = FakeDartDevelopmentService();

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

  @override
  Future<DeviceLogReader> getLogReader({
    ApplicationPackage? app,
    bool includePastLogs = false,
  }) async => NoOpDeviceLogReader('test');

  @override
  Future<LaunchResult> startApp(
    ApplicationPackage? package, {
    String? mainPath,
    String? route,
    required DebuggingOptions debuggingOptions,
    Map<String, Object?> platformArgs = const <String, Object?>{},
    bool prebuiltApplication = false,
    bool ipv6 = false,
    String? userIdentifier,
    }) async {
    if (failOnce) {
      failOnce = false;
      return LaunchResult.failed();
    }
    return result;
  }

  @override
  Future<bool> stopApp(ApplicationPackage? app, {String? userIdentifier}) async {
    didStopApp = true;
    return true;
  }

  @override
  Future<bool> uninstallApp(ApplicationPackage app, {String? userIdentifier}) async {
    didUninstallApp = true;
    return true;
  }

  @override
  Future<void> dispose() async {
    didDispose = true;
  }
}

class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
  bool started = false;
  bool disposed = false;

  @override
  final Uri uri = Uri.parse('http://127.0.0.1:1234/');

  @override
  Future<void> startDartDevelopmentService(
    Uri vmServiceUri, {
    required Logger logger,
    int? hostPort,
    bool? ipv6,
    bool? disableServiceAuthCodes,
    bool cacheStartupProfile = false,
  }) async {
    started = true;
  }

  @override
  Future<void> shutdown() async {
    disposed = true;
  }
}

class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
  bool closed = false;
  final Completer<void> _processStarted = Completer<void>();

  @override
  Future<void> launch(Uri vmServiceUri, {List<String>? additionalArguments}) {
    _processStarted.complete();
    return Completer<void>().future;
  }

  @override
  Future<void> get processStart => _processStarted.future;

  @override
  Future<void> close() async {
    closed = true;
  }
}
