// Copyright 2024 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:devtools_shared/devtools_shared.dart';
import 'package:path/path.dart' as path;

typedef TestDtdConnectionInfo = ({
  DtdInfo? info,
  Process? process,
});

/// Helper method to start DTD for the purpose of testing.
Future<TestDtdConnectionInfo> startDtd() async {
  const dtdConnectTimeout = Duration(seconds: 10);

  final completer = Completer<TestDtdConnectionInfo>();
  Process? dtdProcess;
  StreamSubscription? dtdStoutSubscription;

  TestDtdConnectionInfo onFailure() => (info: null, process: dtdProcess);

  try {
    dtdProcess = await Process.start(
      Platform.resolvedExecutable,
      ['tooling-daemon', '--machine'],
    );

    dtdStoutSubscription = dtdProcess.stdout.listen((List<int> data) {
      try {
        final decoded = utf8.decode(data);
        final json = jsonDecode(decoded) as Map<String, Object?>;
        if (json
            case {
              'tooling_daemon_details': {
                'uri': final String uri,
                'trusted_client_secret': final String secret,
              }
            }) {
          completer.complete(
            (
              info: DtdInfo(Uri.parse(uri), secret: secret),
              process: dtdProcess,
            ),
          );
        } else {
          completer.complete(onFailure());
        }
      } catch (e) {
        completer.complete(onFailure());
      }
    });

    return completer.future
        .timeout(dtdConnectTimeout, onTimeout: onFailure)
        .then((value) async {
      await dtdStoutSubscription?.cancel();
      return value;
    });
  } catch (e) {
    await dtdStoutSubscription?.cancel();
    return onFailure();
  }
}

class TestDartApp {
  static final dartVMServiceRegExp = RegExp(
    r'The Dart VM service is listening on (http://127.0.0.1:.*)',
  );

  final directory = Directory('tmp/test_app');

  Process? process;

  Future<String> start() async {
    await _initTestApp();
    process = await Process.start(
      Platform.resolvedExecutable,
      ['--observe=0', 'run', 'bin/main.dart'],
      workingDirectory: directory.path,
    );

    final serviceUriCompleter = Completer<String>();
    late StreamSubscription sub;
    sub = process!.stdout
        .transform(utf8.decoder)
        .transform(const LineSplitter())
        .listen((line) async {
      if (line.contains(dartVMServiceRegExp)) {
        await sub.cancel();
        serviceUriCompleter.complete(
          dartVMServiceRegExp.firstMatch(line)!.group(1),
        );
      }
    });
    return await serviceUriCompleter.future.timeout(
      const Duration(seconds: 5),
      onTimeout: () async {
        await sub.cancel();
        return '';
      },
    );
  }

  Future<void> kill() async {
    process?.kill();
    await process?.exitCode;
    process = null;
    await deleteDirectoryWithRetry(directory);
  }

  Future<void> _initTestApp() async {
    await deleteDirectoryWithRetry(directory);
    directory.createSync(recursive: true);

    final mainFile = File(path.join(directory.path, 'bin', 'main.dart'))
      ..createSync(recursive: true);
    mainFile.writeAsStringSync('''
import 'dart:async';
void main() async {
  for (int i = 0; i < 10000; i++) {
    await Future.delayed(const Duration(seconds: 2));
  }
}
''');
  }
}

/// Deletes [directory] and retries if the delete operation fails.
///
/// Deletes will be retried if they fail for a period to avoid failing due to
/// Windows being slow to unlock files after processes terminate.
Future<void> deleteDirectoryWithRetry(Directory directory) async {
  // On Windows, trying to delete a directory immediately after the
  // test completes may fail with a file locking error. To avoid this, retry
  // the delete a few times before failing.
  //
  // On DanTup's Windows PC, it can take ~5s for the delete to work sometimes
  // and this will probably be slower on bots. Allow a reasonable time because
  // taking 10s to delete is better than failing the tests for a non-bug.
  await runWithRetry(
    callback: () => directory.deleteSync(recursive: true),
    maxRetries: 20,
    retryDelay: const Duration(milliseconds: 500),
    stopCondition: () => !directory.existsSync(),
    onRetry: (attempt) =>
        // ignore: avoid_print, deliberate print to monitor delete failures
        print('Failed to delete directory on attempt $attempt. Retrying...'),
  );
}
