// 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 show IOSink, ProcessSignal, Stdout, StdoutException;

import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/bot_detector.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/os.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:test/fake.dart';

/// Environment with DYLD_LIBRARY_PATH=/path/to/libraries
class FakeDyldEnvironmentArtifact extends ArtifactSet {
  FakeDyldEnvironmentArtifact() : super(DevelopmentArtifact.iOS);
  @override
  Map<String, String> get environment => <String, String>{
    'DYLD_LIBRARY_PATH': '/path/to/libraries',
  };

  @override
  Future<bool> isUpToDate(FileSystem fileSystem) => Future<bool>.value(true);

  @override
  String get name => 'fake';

  @override
  Future<void> update(ArtifactUpdater artifactUpdater, Logger logger, FileSystem fileSystem, OperatingSystemUtils operatingSystemUtils, {bool offline = false}) async {
  }
}

/// A fake process implementation which can be provided all necessary values.
class FakeProcess implements Process {
  FakeProcess({
    this.pid = 1,
    Future<int>? exitCode,
    IOSink? stdin,
    this.stdout = const Stream<List<int>>.empty(),
    this.stderr = const Stream<List<int>>.empty(),
  }) : exitCode = exitCode ?? Future<int>.value(0),
       stdin = stdin ?? MemoryIOSink();

  @override
  final int pid;

  @override
  final Future<int> exitCode;

  @override
  final io.IOSink stdin;

  @override
  final Stream<List<int>> stdout;

  @override
  final Stream<List<int>> stderr;

  @override
  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
    return true;
  }
}

/// An IOSink that completes a future with the first line written to it.
class CompleterIOSink extends MemoryIOSink {
  CompleterIOSink({
    this.throwOnAdd = false,
  });

  final bool throwOnAdd;

  final Completer<List<int>> _completer = Completer<List<int>>();

  Future<List<int>> get future => _completer.future;

  @override
  void add(List<int> data) {
    if (!_completer.isCompleted) {
      // When throwOnAdd is true, complete with empty so any expected output
      // doesn't appear.
      _completer.complete(throwOnAdd ? <int>[] : data);
    }
    if (throwOnAdd) {
      throw Exception('CompleterIOSink Error');
    }
    super.add(data);
  }
}

/// An IOSink that collects whatever is written to it.
class MemoryIOSink implements IOSink {
  @override
  Encoding encoding = utf8;

  final List<List<int>> writes = <List<int>>[];

  @override
  void add(List<int> data) {
    writes.add(data);
  }

  @override
  Future<void> addStream(Stream<List<int>> stream) {
    final Completer<void> completer = Completer<void>();
    late StreamSubscription<List<int>> sub;
    sub = stream.listen(
      (List<int> data) {
        try {
          add(data);
        // Catches all exceptions to propagate them to the completer.
        } catch (err, stack) { // ignore: avoid_catches_without_on_clauses
          sub.cancel();
          completer.completeError(err, stack);
        }
      },
      onError: completer.completeError,
      onDone: completer.complete,
      cancelOnError: true,
    );
    return completer.future;
  }

  @override
  void writeCharCode(int charCode) {
    add(<int>[charCode]);
  }

  @override
  void write(Object? obj) {
    add(encoding.encode('$obj'));
  }

  @override
  void writeln([ Object? obj = '' ]) {
    add(encoding.encode('$obj\n'));
  }

  @override
  void writeAll(Iterable<dynamic> objects, [ String separator = '' ]) {
    bool addSeparator = false;
    for (final dynamic object in objects) {
      if (addSeparator) {
        write(separator);
      }
      write(object);
      addSeparator = true;
    }
  }

  @override
  void addError(dynamic error, [ StackTrace? stackTrace ]) {
    throw UnimplementedError();
  }

  @override
  Future<void> get done => close();

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

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

  void clear() {
    writes.clear();
  }

  String getAndClear() {
    final String result = utf8.decode(writes.expand((List<int> l) => l).toList());
    clear();
    return result;
  }
}

class MemoryStdout extends MemoryIOSink implements io.Stdout {
  @override
  bool get hasTerminal => _hasTerminal;
  set hasTerminal(bool value) {
    _hasTerminal = value;
  }
  bool _hasTerminal = true;

  @override
  io.IOSink get nonBlocking => this;

  @override
  bool get supportsAnsiEscapes => _supportsAnsiEscapes;
  set supportsAnsiEscapes(bool value) {
    _supportsAnsiEscapes = value;
  }
  bool _supportsAnsiEscapes = true;

  @override
  int get terminalColumns {
    if (_terminalColumns != null) {
      return _terminalColumns!;
    }
    throw const io.StdoutException('unspecified mock value');
  }
  set terminalColumns(int value) => _terminalColumns = value;
  int? _terminalColumns;

  @override
  int get terminalLines {
    if (_terminalLines != null) {
      return _terminalLines!;
    }
    throw const io.StdoutException('unspecified mock value');
  }
  set terminalLines(int value) => _terminalLines = value;
  int? _terminalLines;
}

/// A Stdio that collects stdout and supports simulated stdin.
class FakeStdio extends Stdio {
  final MemoryStdout _stdout = MemoryStdout()..terminalColumns = 80;
  final MemoryIOSink _stderr = MemoryIOSink();
  final FakeStdin _stdin = FakeStdin();

  @override
  MemoryStdout get stdout => _stdout;

  @override
  MemoryIOSink get stderr => _stderr;

  @override
  Stream<List<int>> get stdin => _stdin;

  void simulateStdin(String line) {
    _stdin.controller.add(utf8.encode('$line\n'));
  }

  @override
  bool hasTerminal = true;

  List<String> get writtenToStdout => _stdout.writes.map<String>(_stdout.encoding.decode).toList();
  List<String> get writtenToStderr => _stderr.writes.map<String>(_stderr.encoding.decode).toList();
}

class FakeStdin extends Fake implements Stdin {
  final StreamController<List<int>> controller = StreamController<List<int>>();

  void Function(bool mode)? echoModeCallback;

  bool _echoMode = true;

  @override
  bool get echoMode => _echoMode;

  @override
  set echoMode(bool mode) {
    _echoMode = mode;
    if (echoModeCallback != null) {
      echoModeCallback!(mode);
    }
  }

  @override
  bool lineMode = true;

  @override
  Stream<S> transform<S>(StreamTransformer<List<int>, S> transformer) {
    return controller.stream.transform(transformer);
  }

  @override
  StreamSubscription<List<int>> listen(
    void Function(List<int> event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) {
    return controller.stream.listen(
      onData,
      onError: onError,
      onDone: onDone,
      cancelOnError: cancelOnError,
    );
  }
}

class FakePlistParser implements PlistParser {
  FakePlistParser([Map<String, Object>? underlyingValues]):
    _underlyingValues = underlyingValues ?? <String, Object>{};

  final Map<String, Object> _underlyingValues;

  void setProperty(String key, Object value) {
    _underlyingValues[key] = value;
  }

  @override
  String? plistXmlContent(String plistFilePath) => throw UnimplementedError();

  @override
  Map<String, Object> parseFile(String plistFilePath) {
    return _underlyingValues;
  }

  @override
  T? getValueFromFile<T>(String plistFilePath, String key) {
    return _underlyingValues[key] as T?;
  }

  @override
  bool replaceKey(String plistFilePath, {required String key, String? value}) {
    if (value == null) {
      _underlyingValues.remove(key);
      return true;
    }
    setProperty(key, value);
    return true;
  }
}

class FakeBotDetector implements BotDetector {
  const FakeBotDetector(bool isRunningOnBot)
      : _isRunningOnBot = isRunningOnBot;

  @override
  Future<bool> get isRunningOnBot async => _isRunningOnBot;

  final bool _isRunningOnBot;
}

class FakeFlutterVersion implements FlutterVersion {
  FakeFlutterVersion({
    this.branch = 'master',
    this.dartSdkVersion = '12',
    this.devToolsVersion = '2.8.0',
    this.engineRevision = 'abcdefghijklmnopqrstuvwxyz',
    this.engineRevisionShort = 'abcde',
    this.repositoryUrl = 'https://github.com/flutter/flutter.git',
    this.frameworkVersion = '0.0.0',
    this.frameworkRevision = '11111111111111111111',
    this.frameworkRevisionShort = '11111',
    this.frameworkAge = '0 hours ago',
    this.frameworkCommitDate = '12/01/01',
    this.gitTagVersion = const GitTagVersion.unknown(),
    this.flutterRoot = '/path/to/flutter',
    this.nextFlutterVersion,
  });

  final String branch;

  bool get didFetchTagsAndUpdate => _didFetchTagsAndUpdate;
  bool _didFetchTagsAndUpdate = false;

  /// Will be returned by [fetchTagsAndGetVersion] if not null.
  final FlutterVersion? nextFlutterVersion;

  @override
    FlutterVersion fetchTagsAndGetVersion({
      SystemClock clock = const SystemClock(),
    }) {
    _didFetchTagsAndUpdate = true;
    return nextFlutterVersion ?? this;
  }

  bool get didCheckFlutterVersionFreshness => _didCheckFlutterVersionFreshness;
  bool _didCheckFlutterVersionFreshness = false;

  @override
  String get channel {
    if (kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) {
      return branch;
    }
    return kUserBranch;
  }

  @override
  final String flutterRoot;

  @override
  final String devToolsVersion;

  @override
  final String dartSdkVersion;

  @override
  final String engineRevision;

  @override
  final String engineRevisionShort;

  @override
  final String? repositoryUrl;

  @override
  final String frameworkVersion;

  @override
  final String frameworkRevision;

  @override
  final String frameworkRevisionShort;

  @override
  final String frameworkAge;

  @override
  final String frameworkCommitDate;

  @override
  final GitTagVersion gitTagVersion;

  @override
  FileSystem get fs => throw UnimplementedError('FakeFlutterVersion.fs is not implemented');

  @override
  Future<void> checkFlutterVersionFreshness() async {
    _didCheckFlutterVersionFreshness = true;
  }

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

  @override
  String getBranchName({bool redactUnknownBranches = false}) {
    if (!redactUnknownBranches || kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) {
      return branch;
    }
    return kUserBranch;
  }

  @override
  String getVersionString({bool redactUnknownBranches = false}) {
    return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevision';
  }

  @override
  Map<String, Object> toJson() {
    return <String, Object>{};
  }
}

// A test implementation of [FeatureFlags] that allows enabling without reading
// config. If not otherwise specified, all values default to false.
class TestFeatureFlags implements FeatureFlags {
  TestFeatureFlags({
    this.isLinuxEnabled = false,
    this.isMacOSEnabled = false,
    this.isWebEnabled = false,
    this.isWindowsEnabled = false,
    this.isAndroidEnabled = true,
    this.isIOSEnabled = true,
    this.isFuchsiaEnabled = false,
    this.areCustomDevicesEnabled = false,
    this.isFlutterWebWasmEnabled = false,
    this.isCliAnimationEnabled = true,
    this.isNativeAssetsEnabled = false,
    this.isPreviewDeviceEnabled = false,
  });

  @override
  final bool isLinuxEnabled;

  @override
  final bool isMacOSEnabled;

  @override
  final bool isWebEnabled;

  @override
  final bool isWindowsEnabled;

  @override
  final bool isAndroidEnabled;

  @override
  final bool isIOSEnabled;

  @override
  final bool isFuchsiaEnabled;

  @override
  final bool areCustomDevicesEnabled;

  @override
  final bool isFlutterWebWasmEnabled;

  @override
  final bool isCliAnimationEnabled;

  @override
  final bool isNativeAssetsEnabled;

  @override
  final bool isPreviewDeviceEnabled;

  @override
  bool isEnabled(Feature feature) {
    return switch (feature) {
      flutterWebFeature => isWebEnabled,
      flutterLinuxDesktopFeature => isLinuxEnabled,
      flutterMacOSDesktopFeature => isMacOSEnabled,
      flutterWindowsDesktopFeature => isWindowsEnabled,
      flutterAndroidFeature => isAndroidEnabled,
      flutterIOSFeature => isIOSEnabled,
      flutterFuchsiaFeature => isFuchsiaEnabled,
      flutterCustomDevicesFeature => areCustomDevicesEnabled,
      cliAnimation => isCliAnimationEnabled,
      nativeAssets => isNativeAssetsEnabled,
      _ => false,
    };
  }
}

class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
  FakeOperatingSystemUtils({this.hostPlatform = HostPlatform.linux_x64});

  final List<List<String>> chmods = <List<String>>[];

  @override
  void makeExecutable(File file) { }

  @override
  HostPlatform hostPlatform = HostPlatform.linux_x64;

  @override
  void chmod(FileSystemEntity entity, String mode) {
    chmods.add(<String>[entity.path, mode]);
  }

  @override
  File? which(String execName) => null;

  @override
  List<File> whichAll(String execName) => <File>[];

  @override
  void unzip(File file, Directory targetDirectory) { }

  @override
  void unpack(File gzippedTarFile, Directory targetDirectory) { }

  @override
  Stream<List<int>> gzipLevel1Stream(Stream<List<int>> stream) => stream;

  @override
  String get name => 'fake OS name and version';

  @override
  String get pathVarSeparator => ';';

  @override
  Future<int> findFreePort({bool ipv6 = false}) async => 12345;
}

class FakeStopwatch implements Stopwatch {
  @override
  bool get isRunning => _isRunning;
  bool _isRunning = false;

  @override
  void start() => _isRunning = true;

  @override
  void stop() => _isRunning = false;

  @override
  Duration elapsed = Duration.zero;

  @override
  int get elapsedMicroseconds => elapsed.inMicroseconds;

  @override
  int get elapsedMilliseconds => elapsed.inMilliseconds;

  @override
  int get elapsedTicks => elapsed.inMilliseconds;

  @override
  int get frequency => 1000;

  @override
  void reset() {
    _isRunning = false;
    elapsed = Duration.zero;
  }

  @override
  String toString() => '$runtimeType $elapsed $isRunning';
}

class FakeStopwatchFactory implements StopwatchFactory {
  FakeStopwatchFactory({
    Stopwatch? stopwatch,
    Map<String, Stopwatch>? stopwatches
  }) : stopwatches = <String, Stopwatch>{
         if (stopwatches != null) ...stopwatches,
         if (stopwatch != null) '': stopwatch,
       };

  Map<String, Stopwatch> stopwatches;

  @override
  Stopwatch createStopwatch([String name = '']) {
    return stopwatches[name] ?? FakeStopwatch();
  }
}

class FakeFlutterProjectFactory implements FlutterProjectFactory {
  @override
  FlutterProject fromDirectory(Directory directory) {
    return FlutterProject.fromDirectoryTest(directory);
  }

  @override
  Map<String, FlutterProject> get projects => throw UnimplementedError();
}

class FakeAndroidSdk extends Fake implements AndroidSdk {

  @override
  late bool platformToolsAvailable;

  @override
  late bool licensesAvailable;

  @override
  AndroidSdkVersion? latestVersion;
}

class FakeAndroidStudio extends Fake implements AndroidStudio {
  @override
  String get javaPath => 'java';
}

class FakeJava extends Fake implements Java {
  FakeJava({
    this.javaHome = '/android-studio/jbr',
    String binary = '/android-studio/jbr/bin/java',
    Version? version,
    bool canRun = true,
  }): binaryPath = binary,
      version = version ?? const Version.withText(19, 0, 2, 'openjdk 19.0.2 2023-01-17'),
      _environment = <String, String>{
        if (javaHome != null) Java.javaHomeEnvironmentVariable: javaHome,
        'PATH': '/android-studio/jbr/bin',
      },
      _canRun = canRun;

  @override
  String? javaHome;

  @override
  String binaryPath;

  final Map<String, String> _environment;
  final bool _canRun;

  @override
  Map<String, String> get environment => _environment;

  @override
  Version? version;

  @override
  bool canRun() {
    return _canRun;
  }
}