// 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:dds/dds.dart' as dds;
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/asset.dart';
import 'package:flutter_tools/src/base/dds.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/tools/scene_importer.dart';
import 'package:flutter_tools/src/build_system/tools/shader_compiler.dart';
import 'package:flutter_tools/src/compile.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:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/run_cold.dart';
import 'package:flutter_tools/src/run_hot.dart';
import 'package:flutter_tools/src/vmservice.dart';
import 'package:package_config/package_config.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;

import '../src/fake_vm_services.dart';

final vm_service.Event fakeUnpausedEvent = vm_service.Event(
  kind: vm_service.EventKind.kResume,
  timestamp: 0
);

final vm_service.Event fakePausedEvent = vm_service.Event(
  kind: vm_service.EventKind.kPauseException,
  timestamp: 0
);

final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: fakeUnpausedEvent,
  breakpoints: <vm_service.Breakpoint>[],
  extensionRPCs: <String>[],
  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.Isolate fakePausedIsolate = vm_service.Isolate(
  id: '1',
  pauseEvent: fakePausedEvent,
  breakpoints: <vm_service.Breakpoint>[
    vm_service.Breakpoint(
      breakpointNumber: 123,
      id: 'test-breakpoint',
      location: vm_service.SourceLocation(
        tokenPos: 0,
        script: vm_service.ScriptRef(id: 'test-script', uri: 'foo.dart'),
      ),
      enabled: true,
      resolved: true,
    ),
  ],
  libraries: <vm_service.LibraryRef>[],
  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(),
    ],
  },
);

const FakeVmServiceRequest setAssetBundlePath = FakeVmServiceRequest(
  method: '_flutter.setAssetBundlePath',
  args: <String, Object>{
    'viewId': 'a',
    'assetDirectory': 'build/flutter_assets',
    'isolateId': '1',
  }
);

const FakeVmServiceRequest evict = FakeVmServiceRequest(
  method: 'ext.flutter.evict',
  args: <String, Object>{
    'value': 'asset',
    'isolateId': '1',
  }
);

const FakeVmServiceRequest evictShader = FakeVmServiceRequest(
  method: 'ext.ui.window.reinitializeShader',
  args: <String, Object>{
    'assetKey': 'foo.frag',
    'isolateId': '1',
  }
);

final Uri testUri = Uri.parse('foo://bar');

// This implements [dds.DartDevelopmentService], not the [DartDevelopmentService]
// interface from package:flutter_tools.
class FakeDartDevelopmentService extends Fake implements dds.DartDevelopmentService {
  @override
  Future<void> get done => Future<void>.value();

  @override
  Uri? get uri => null;
}

class FakeDartDevelopmentServiceException implements dds.DartDevelopmentServiceException {
  FakeDartDevelopmentServiceException({this.message = defaultMessage});

  @override
  final int errorCode = dds.DartDevelopmentServiceException.existingDdsInstanceError;

  @override
  final String message;
  static const String defaultMessage = 'A DDS instance is already connected at http://localhost:8181';
}

class TestFlutterDevice extends FlutterDevice {
  TestFlutterDevice(super.device, { Stream<Uri>? vmServiceUris })
    : _vmServiceUris = vmServiceUris, super(buildInfo: BuildInfo.debug, developmentShaderCompiler: const FakeShaderCompiler());

  final Stream<Uri>? _vmServiceUris;

  @override
  Stream<Uri> get vmServiceUris => _vmServiceUris!;
}

class ThrowingForwardingFileSystem extends ForwardingFileSystem {
  ThrowingForwardingFileSystem(super.delegate);

  @override
  File file(dynamic path) {
    if (path == 'foo') {
      throw const FileSystemException();
    }
    return delegate.file(path);
  }
}

class FakeFlutterDevice extends Fake implements FlutterDevice {
  FakeVmServiceHost? Function()? vmServiceHost;
  Uri? testUri;
  UpdateFSReport report = UpdateFSReport(
    success: true,
    invalidatedSourcesCount: 1,
  );
  Exception? reportError;
  Exception? runColdError;
  int runHotCode = 0;
  int runColdCode = 0;

  @override
  ResidentCompiler? generator;

  @override
  DevelopmentShaderCompiler get developmentShaderCompiler => const FakeShaderCompiler();

  @override
  TargetPlatform targetPlatform = TargetPlatform.android;

  @override
  Stream<Uri?> get vmServiceUris => Stream<Uri?>.value(testUri);

  @override
  FlutterVmService? get vmService => vmServiceHost?.call()?.vmService;

  DevFS? fakeDevFS;

  @override
  DevFS? get devFS => fakeDevFS;

  @override
  set devFS(DevFS? value) { }

  @override
  Device? device;

  @override
  Future<void> stopEchoingDeviceLog() async { }

  @override
  Future<void> initLogReader() async { }

  @override
  Future<Uri> setupDevFS(String fsName, Directory rootDirectory) async {
    return testUri!;
  }

  @override
  Future<int> runHot({required HotRunner hotRunner, String? route}) async {
    return runHotCode;
  }

  @override
  Future<int> runCold({required ColdRunner coldRunner, String? route}) async {
    if (runColdError != null) {
      throw runColdError!;
    }
    return runColdCode;
  }

  @override
  Future<void> connect({
    ReloadSources? reloadSources,
    Restart? restart,
    CompileExpression? compileExpression,
    GetSkSLMethod? getSkSLMethod,
    FlutterProject? flutterProject,
    PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
    int? hostVmServicePort,
    int? ddsPort,
    bool disableServiceAuthCodes = false,
    bool enableDds = true,
    bool cacheStartupProfile = false,
    required bool allowExistingDdsInstance,
    bool ipv6 = false,
  }) async { }

  @override
  Future<UpdateFSReport> updateDevFS({
    required Uri mainUri,
    String? target,
    AssetBundle? bundle,
    DateTime? firstBuildTime,
    bool bundleFirstUpload = false,
    bool bundleDirty = false,
    bool fullRestart = false,
    String? projectRootPath,
    required String pathToReload,
    required String dillOutputPath,
    required List<Uri> invalidatedFiles,
    required PackageConfig packageConfig,
  }) async {
    if (reportError != null) {
      throw reportError!;
    }
    return report;
  }

  @override
  Future<void> updateReloadStatus(bool wasReloadSuccessful) async { }
}

class FakeDelegateFlutterDevice extends FlutterDevice {
  FakeDelegateFlutterDevice(
    super.device,
    BuildInfo buildInfo,
    ResidentCompiler residentCompiler,
    this.fakeDevFS,
  ) : super(buildInfo: buildInfo, generator: residentCompiler, developmentShaderCompiler: const FakeShaderCompiler());

  @override
  Future<void> connect({
    ReloadSources? reloadSources,
    Restart? restart,
    bool enableDds = true,
    bool cacheStartupProfile = false,
    bool disableServiceAuthCodes = false,
    bool ipv6 = false,
    CompileExpression? compileExpression,
    GetSkSLMethod? getSkSLMethod,
    FlutterProject? flutterProject,
    int? hostVmServicePort,
    int? ddsPort,
    PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
    bool allowExistingDdsInstance = false,
  }) async { }


  final DevFS fakeDevFS;

  @override
  DevFS? get devFS => fakeDevFS;

  @override
  set devFS(DevFS? value) {}
}

class FakeResidentCompiler extends Fake implements ResidentCompiler {
  CompilerOutput? nextOutput;
  bool didSuppressErrors = false;
  Uri? receivedNativeAssetsYaml;
  bool recompileCalled = false;

  @override
  Future<CompilerOutput?> recompile(
    Uri mainUri,
    List<Uri>? invalidatedFiles, {
    required String outputPath,
    required PackageConfig packageConfig,
    String? projectRootPath,
    required FileSystem fs,
    bool suppressErrors = false,
    bool checkDartPluginRegistry = false,
    File? dartPluginRegistrant,
    Uri? nativeAssetsYaml,
  }) async {
    recompileCalled = true;
    receivedNativeAssetsYaml = nativeAssetsYaml;
    didSuppressErrors = suppressErrors;
    return nextOutput ?? const CompilerOutput('foo.dill', 0, <Uri>[]);
  }

  @override
  void accept() { }

  @override
  void reset() { }
}

class FakeProjectFileInvalidator extends Fake implements ProjectFileInvalidator {
  @override
  Future<InvalidationResult> findInvalidated({
    required DateTime? lastCompiled,
    required List<Uri> urisToMonitor,
    required String packagesPath,
    required PackageConfig packageConfig,
    bool asyncScanning = false,
  }) async {
    return InvalidationResult(
      packageConfig: packageConfig,
      uris: <Uri>[Uri.parse('file:///hello_world/main.dart'),
    ]);
  }
}

class FakeDevice extends Fake implements Device {
  FakeDevice({
    String sdkNameAndVersion = 'Android',
    TargetPlatform targetPlatform = TargetPlatform.android_arm,
    bool isLocalEmulator = false,
    this.supportsHotRestart = true,
    this.supportsScreenshot = true,
    this.supportsFlutterExit = true,
  }) : _isLocalEmulator = isLocalEmulator,
       _targetPlatform = targetPlatform,
       _sdkNameAndVersion = sdkNameAndVersion;

  final bool _isLocalEmulator;
  final TargetPlatform _targetPlatform;
  final String _sdkNameAndVersion;

  bool disposed = false;
  bool appStopped = false;
  bool failScreenshot = false;

  @override
  bool supportsHotRestart;

  @override
  bool supportsScreenshot;

  @override
  bool supportsFlutterExit;

  @override
  PlatformType get platformType => _targetPlatform == TargetPlatform.web_javascript
    ? PlatformType.web
    : PlatformType.android;

  @override
  Future<String> get sdkNameAndVersion async => _sdkNameAndVersion;

  @override
  Future<TargetPlatform> get targetPlatform async => _targetPlatform;

  @override
  Future<bool> get isLocalEmulator async => _isLocalEmulator;

  @override
  String get name => 'FakeDevice';

  @override
  late DartDevelopmentService dds;

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

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

  @override
  Future<void> takeScreenshot(File outputFile) async {
    if (failScreenshot) {
      throw Exception();
    }
    outputFile.writeAsBytesSync(List<int>.generate(1024, (int i) => i));
  }

  @override
  FutureOr<DeviceLogReader> getLogReader({
    ApplicationPackage? app,
    bool includePastLogs = false,
  }) => NoOpDeviceLogReader(name);

  @override
  DevicePortForwarder portForwarder = const NoOpDevicePortForwarder();
}

class FakeDevFS extends Fake implements DevFS {
  @override
  DateTime? lastCompiled = DateTime(2000);

  @override
  PackageConfig? lastPackageConfig = PackageConfig.empty;

  @override
  List<Uri> sources = <Uri>[];

  @override
  Uri baseUri = Uri();

  @override
  Future<void> destroy() async { }

  @override
  Set<String> assetPathsToEvict = <String>{};

  @override
  Set<String> shaderPathsToEvict = <String>{};

  @override
  Set<String> scenePathsToEvict = <String>{};

  @override
  bool didUpdateFontManifest = false;

  UpdateFSReport nextUpdateReport = UpdateFSReport(success: true);

  @override
  bool hasSetAssetDirectory = false;

  @override
  Future<Uri> create() async {
    return Uri();
  }

  @override
  void resetLastCompiled() {
    lastCompiled = null;
  }

  @override
  Future<UpdateFSReport> update({
    required Uri mainUri,
    required ResidentCompiler generator,
    required bool trackWidgetCreation,
    required String pathToReload,
    required List<Uri> invalidatedFiles,
    required PackageConfig packageConfig,
    required String dillOutputPath,
    required DevelopmentShaderCompiler shaderCompiler,
    DevelopmentSceneImporter? sceneImporter,
    DevFSWriter? devFSWriter,
    String? target,
    AssetBundle? bundle,
    DateTime? firstBuildTime,
    bool bundleFirstUpload = false,
    bool fullRestart = false,
    String? projectRootPath,
    File? dartPluginRegistrant,
  }) async {
    return nextUpdateReport;
  }
}

class FakeShaderCompiler implements DevelopmentShaderCompiler {
  const FakeShaderCompiler();

  @override
  void configureCompiler(TargetPlatform? platform) { }

  @override
  Future<DevFSContent> recompileShader(DevFSContent inputShader) {
    throw UnimplementedError();
  }
}
