// 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/artifacts.dart';
import 'package:flutter_tools/src/base/async_guard.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:package_config/package_config.dart';

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

void main() {
  late ResidentCompiler generator;
  late ResidentCompiler generatorWithScheme;
  late ResidentCompiler generatorWithPlatformDillAndLibrariesSpec;
  late MemoryIOSink frontendServerStdIn;
  late BufferLogger testLogger;
  late StdoutHandler generatorStdoutHandler;
  late StdoutHandler generatorWithSchemeStdoutHandler;
  late FakeProcessManager fakeProcessManager;

  const List<String> frontendServerCommand = <String>[
    'Artifact.engineDartBinary',
    '--disable-dart-dev',
    'Artifact.frontendServerSnapshotForEngineDartSdk',
    '--sdk-root',
    'sdkroot/',
    '--incremental',
    '--target=flutter',
    '--experimental-emit-debug-metadata',
    '--output-dill',
    '/build/',
    '-Ddart.vm.profile=false',
    '-Ddart.vm.product=false',
    '--enable-asserts',
    '--track-widget-creation',
  ];

  setUp(() {
    testLogger = BufferLogger.test();
    frontendServerStdIn = MemoryIOSink();

    fakeProcessManager = FakeProcessManager.empty();
    generatorStdoutHandler = StdoutHandler(logger: testLogger, fileSystem: MemoryFileSystem.test());
    generatorWithSchemeStdoutHandler = StdoutHandler(logger: testLogger, fileSystem: MemoryFileSystem.test());
    generator = DefaultResidentCompiler(
      'sdkroot',
      buildMode: BuildMode.debug,
      logger: testLogger,
      processManager: fakeProcessManager,
      artifacts: Artifacts.test(),
      platform: FakePlatform(),
      fileSystem: MemoryFileSystem.test(),
      stdoutHandler: generatorStdoutHandler,
    );
    generatorWithScheme = DefaultResidentCompiler(
      'sdkroot',
      buildMode: BuildMode.debug,
      logger: testLogger,
      processManager: fakeProcessManager,
      artifacts: Artifacts.test(),
      platform: FakePlatform(),
      fileSystemRoots: <String>[
        '/foo/bar/fizz',
      ],
      fileSystemScheme: 'scheme',
      fileSystem: MemoryFileSystem.test(),
      stdoutHandler: generatorWithSchemeStdoutHandler,
    );
    generatorWithPlatformDillAndLibrariesSpec = DefaultResidentCompiler(
      'sdkroot',
      buildMode: BuildMode.debug,
      logger: testLogger,
      processManager: fakeProcessManager,
      artifacts: Artifacts.test(),
      platform: FakePlatform(),
      fileSystem: MemoryFileSystem.test(),
      stdoutHandler: generatorStdoutHandler,
      platformDill: '/foo/platform.dill',
      librariesSpec: '/bar/libraries.json',
    );
  });

  testWithoutContext('incremental compile single dart compile', () async {
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[...frontendServerCommand, '--verbosity=error'],
      stdout: 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
    ));

    final CompilerOutput? output = await generator.recompile(
      Uri.parse('/path/to/main.dart'),
        null /* invalidatedFiles */,
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    );
    expect(frontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
    expect(testLogger.errorText, equals('line1\nline2\n'));
    expect(output?.outputFilename, equals('/path/to/main.dart.dill'));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('incremental compile single dart compile with filesystem scheme', () async {
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[
        ...frontendServerCommand,
        '--filesystem-root',
        '/foo/bar/fizz',
        '--filesystem-scheme',
        'scheme',
        '--verbosity=error',
      ],
      stdout: 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
    ));

    final CompilerOutput? output = await generatorWithScheme.recompile(
      Uri.parse('file:///foo/bar/fizz/main.dart'),
        null /* invalidatedFiles */,
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    );
    expect(frontendServerStdIn.getAndClear(), 'compile scheme:///main.dart\n');
    expect(testLogger.errorText, equals('line1\nline2\n'));
    expect(output?.outputFilename, equals('/path/to/main.dart.dill'));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('incremental compile single dart compile abnormally terminates', () async {
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[...frontendServerCommand, '--verbosity=error'],
      stdin: frontendServerStdIn,
    ));

    expect(asyncGuard(() => generator.recompile(
      Uri.parse('/path/to/main.dart'),
      null, /* invalidatedFiles */
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    )), throwsToolExit());
  });

  testWithoutContext('incremental compile single dart compile abnormally terminates via exitCode', () async {
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[...frontendServerCommand, '--verbosity=error'],
      stdin: frontendServerStdIn,
      exitCode: 1,
    ));

    expect(asyncGuard(() => generator.recompile(
      Uri.parse('/path/to/main.dart'),
      null, /* invalidatedFiles */
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    )), throwsToolExit(message: 'the Dart compiler exited unexpectedly.'));
  });

  testWithoutContext('incremental compile and recompile', () async {
    final Completer<void> completer = Completer<void>();
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[...frontendServerCommand, '--verbosity=error'],
      stdout: 'result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
      completer: completer,
    ));

    await generator.recompile(
      Uri.parse('/path/to/main.dart'),
      null, /* invalidatedFiles */
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      projectRootPath: '',
      fs: MemoryFileSystem(),
    );
    expect(frontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');

    // No accept or reject commands should be issued until we
    // send recompile request.
    await _accept(generator, frontendServerStdIn, '');
    await _reject(generatorStdoutHandler, generator, frontendServerStdIn, '', '');

    await _recompile(generatorStdoutHandler, generator, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');

    await _accept(generator, frontendServerStdIn, r'^accept\n$');

    await _recompile(generatorStdoutHandler, generator, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');
    // No sources returned from reject command.
    await _reject(generatorStdoutHandler, generator, frontendServerStdIn, 'result abc\nabc\n',
      r'^reject\n$');
    completer.complete();
    expect(frontendServerStdIn.getAndClear(), isEmpty);
    expect(testLogger.errorText, equals(
      'line0\nline1\n'
      'line1\nline2\n'
      'line1\nline2\n'
    ));
  });

  testWithoutContext('incremental compile and recompile with filesystem scheme', () async {
    final Completer<void> completer = Completer<void>();
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[
        ...frontendServerCommand,
        '--filesystem-root',
        '/foo/bar/fizz',
        '--filesystem-scheme',
        'scheme',
        '--verbosity=error',
      ],
      stdout: 'result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
      completer: completer,
    ));
    await generatorWithScheme.recompile(
      Uri.parse('file:///foo/bar/fizz/main.dart'),
      null, /* invalidatedFiles */
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    );
    expect(frontendServerStdIn.getAndClear(), 'compile scheme:///main.dart\n');

    // No accept or reject commands should be issued until we
    // send recompile request.
    await _accept(generatorWithScheme, frontendServerStdIn, '');
    await _reject(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn, '', '');

    await _recompile(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n',
      mainUri: Uri.parse('file:///foo/bar/fizz/main.dart'),
      expectedMainUri: 'scheme:///main.dart');

    await _accept(generatorWithScheme, frontendServerStdIn, r'^accept\n$');

    await _recompile(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n',
      mainUri: Uri.parse('file:///foo/bar/fizz/main.dart'),
      expectedMainUri: 'scheme:///main.dart');
    // No sources returned from reject command.
    await _reject(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn, 'result abc\nabc\n',
      r'^reject\n$');
    completer.complete();
    expect(frontendServerStdIn.getAndClear(), isEmpty);
    expect(testLogger.errorText, equals(
      'line0\nline1\n'
      'line1\nline2\n'
      'line1\nline2\n'
    ));
  });

  testWithoutContext('incremental compile and recompile non-entrypoint file with filesystem scheme', () async {
    final Uri mainUri = Uri.parse('file:///foo/bar/fizz/main.dart');
    const String expectedMainUri = 'scheme:///main.dart';
    final List<Uri> updatedUris = <Uri>[
      mainUri,
      Uri.parse('file:///foo/bar/fizz/other.dart'),
    ];
    const List<String> expectedUpdatedUris = <String>[
      expectedMainUri,
      'scheme:///other.dart',
    ];

    final Completer<void> completer = Completer<void>();
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[
        ...frontendServerCommand,
        '--filesystem-root',
        '/foo/bar/fizz',
        '--filesystem-scheme',
        'scheme',
        '--verbosity=error',
      ],
      stdout: 'result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
      completer: completer,
    ));
    await generatorWithScheme.recompile(
      Uri.parse('file:///foo/bar/fizz/main.dart'),
      null, /* invalidatedFiles */
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    );
    expect(frontendServerStdIn.getAndClear(), 'compile scheme:///main.dart\n');

    // No accept or reject commands should be issued until we
    // send recompile request.
    await _accept(generatorWithScheme, frontendServerStdIn, '');
    await _reject(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn, '', '');

    await _recompile(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n',
      mainUri: mainUri,
      expectedMainUri: expectedMainUri,
      updatedUris: updatedUris,
      expectedUpdatedUris: expectedUpdatedUris);

    await _accept(generatorWithScheme, frontendServerStdIn, r'^accept\n$');

    await _recompile(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n',
      mainUri: mainUri,
      expectedMainUri: expectedMainUri,
      updatedUris: updatedUris,
      expectedUpdatedUris: expectedUpdatedUris);
    // No sources returned from reject command.
    await _reject(generatorWithSchemeStdoutHandler, generatorWithScheme, frontendServerStdIn, 'result abc\nabc\n',
      r'^reject\n$');
    completer.complete();
    expect(frontendServerStdIn.getAndClear(), isEmpty);
    expect(testLogger.errorText, equals(
      'line0\nline1\n'
      'line1\nline2\n'
      'line1\nline2\n'
    ));
  });

  testWithoutContext('incremental compile can suppress errors', () async {
    final Completer<void> completer = Completer<void>();
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[...frontendServerCommand, '--verbosity=error'],
      stdout: 'result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
      completer: completer,
    ));

    await generator.recompile(
      Uri.parse('/path/to/main.dart'),
      <Uri>[],
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    );
    expect(frontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');

    await _recompile(generatorStdoutHandler, generator, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');

    await _accept(generator, frontendServerStdIn, r'^accept\n$');

    await _recompile(generatorStdoutHandler, generator, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n', suppressErrors: true);

    completer.complete();
    expect(frontendServerStdIn.getAndClear(), isEmpty);

    // Compiler message is not printed with suppressErrors: true above.
    expect(testLogger.errorText, isNot(equals(
      'line1\nline2\n'
    )));
    expect(testLogger.traceText, contains(
      'line1\nline2\n'
    ));
  });

  testWithoutContext('incremental compile and recompile twice', () async {
    final Completer<void> completer = Completer<void>();
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[...frontendServerCommand, '--verbosity=error'],
      stdout: 'result abc\nline0\nline1\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
      completer: completer,
    ));
    await generator.recompile(
      Uri.parse('/path/to/main.dart'),
      null /* invalidatedFiles */,
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    );
    expect(frontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');

    await _recompile(generatorStdoutHandler, generator, frontendServerStdIn,
      'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0\n');
    await _recompile(generatorStdoutHandler, generator, frontendServerStdIn,
      'result abc\nline2\nline3\nabc\nabc /path/to/main.dart.dill 0\n');

    completer.complete();
    expect(frontendServerStdIn.getAndClear(), isEmpty);
    expect(testLogger.errorText, equals(
      'line0\nline1\n'
      'line1\nline2\n'
      'line2\nline3\n'
    ));
  });

  testWithoutContext('incremental compile with dartPluginRegistrant', () async {
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[
        ...frontendServerCommand,
        '--filesystem-root',
        '/foo/bar/fizz',
        '--filesystem-scheme',
        'scheme',
        '--source',
        'some/dir/plugin_registrant.dart',
        '--source',
        'package:flutter/src/dart_plugin_registrant.dart',
        '-Dflutter.dart_plugin_registrant=some/dir/plugin_registrant.dart',
        '--verbosity=error',
      ],
      stdout: 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
    ));

    final MemoryFileSystem fs = MemoryFileSystem();
    final File dartPluginRegistrant = fs.file('some/dir/plugin_registrant.dart')..createSync(recursive: true);
    final CompilerOutput? output = await generatorWithScheme.recompile(
      Uri.parse('file:///foo/bar/fizz/main.dart'),
        null /* invalidatedFiles */,
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: fs,
      projectRootPath: '',
      checkDartPluginRegistry: true,
      dartPluginRegistrant: dartPluginRegistrant,
    );
    expect(frontendServerStdIn.getAndClear(), 'compile scheme:///main.dart\n');
    expect(testLogger.errorText, equals('line1\nline2\n'));
    expect(output?.outputFilename, equals('/path/to/main.dart.dill'));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });

  testWithoutContext('compile does not pass libraries-spec when using a platform dill', () async {
    fakeProcessManager.addCommand(FakeCommand(
      command: const <String>[
        ...frontendServerCommand,
        '--platform',
        '/foo/platform.dill',
        '--verbosity=error'
      ],
      stdout: 'result abc\nline1\nline2\nabc\nabc /path/to/main.dart.dill 0',
      stdin: frontendServerStdIn,
    ));

    final CompilerOutput? output = await generatorWithPlatformDillAndLibrariesSpec.recompile(
      Uri.parse('/path/to/main.dart'),
        null /* invalidatedFiles */,
      outputPath: '/build/',
      packageConfig: PackageConfig.empty,
      fs: MemoryFileSystem(),
      projectRootPath: '',
    );
    expect(frontendServerStdIn.getAndClear(), 'compile /path/to/main.dart\n');
    expect(testLogger.errorText, equals('line1\nline2\n'));
    expect(output?.outputFilename, equals('/path/to/main.dart.dill'));
    expect(fakeProcessManager, hasNoRemainingExpectations);
  });
}

Future<void> _recompile(
  StdoutHandler stdoutHandler,
  ResidentCompiler generator,
  MemoryIOSink frontendServerStdIn,
  String mockCompilerOutput, {
  bool suppressErrors = false,
  Uri? mainUri,
  String expectedMainUri = '/path/to/main.dart',
  List<Uri>? updatedUris,
  List<String>? expectedUpdatedUris,
}) async {
  mainUri ??= Uri.parse('/path/to/main.dart');
  updatedUris ??= <Uri>[mainUri];
  expectedUpdatedUris ??= <String>[expectedMainUri];

  final Future<CompilerOutput?> recompileFuture = generator.recompile(
    mainUri,
    updatedUris,
    outputPath: '/build/',
    packageConfig: PackageConfig.empty,
    suppressErrors: suppressErrors,
    fs: MemoryFileSystem(),
    projectRootPath: '',
  );

  // Put content into the output stream after generator.recompile gets
  // going few lines below, resets completer.
  scheduleMicrotask(() {
    LineSplitter.split(mockCompilerOutput).forEach(stdoutHandler.handler);
  });
  final CompilerOutput? output = await recompileFuture;
  expect(output?.outputFilename, equals('/path/to/main.dart.dill'));
  final String commands = frontendServerStdIn.getAndClear();
  final RegExp whitespace = RegExp(r'\s+');
  final List<String> parts = commands.split(whitespace);

  // Test that uuid matches at beginning and end.
  expect(parts[2], equals(parts[3 + updatedUris.length]));
  expect(parts[1], equals(expectedMainUri));
  for (int i = 0; i < expectedUpdatedUris.length; i++) {
    expect(parts[3 + i], equals(expectedUpdatedUris[i]));
  }
}

Future<void> _accept(
  ResidentCompiler generator,
  MemoryIOSink frontendServerStdIn,
  String expected,
) async {
  generator.accept();
  final String commands = frontendServerStdIn.getAndClear();
  final RegExp re = RegExp(expected);
  expect(commands, matches(re));
}

Future<void> _reject(
  StdoutHandler stdoutHandler,
  ResidentCompiler generator,
  MemoryIOSink frontendServerStdIn,
  String mockCompilerOutput,
  String expected,
) async {
  // Put content into the output stream after generator.recompile gets
  // going few lines below, resets completer.
  final Future<CompilerOutput?> rejectFuture = generator.reject();
  scheduleMicrotask(() {
    LineSplitter.split(mockCompilerOutput).forEach(stdoutHandler.handler);
  });
  final CompilerOutput? output = await rejectFuture;
  expect(output, isNull);

  final String commands = frontendServerStdIn.getAndClear();
  final RegExp re = RegExp(expected);
  expect(commands, matches(re));
}
