// 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 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/user_messages.dart';
import 'package:flutter_tools/src/runner/local_engine.dart';

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

const String kEngineRoot = '/flutter/engine';
const String kArbitraryEngineRoot = '/arbitrary/engine';
const String kDotPackages = '.packages';

void main() {
  testWithoutContext('works if --local-engine is specified and --local-engine-src-path '
    'is determined by sky_engine', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    fileSystem
      .directory('$kArbitraryEngineRoot/src/out/ios_debug/gen/dart-pkg/sky_engine/lib/')
      .createSync(recursive: true);
    fileSystem
      .directory('$kArbitraryEngineRoot/src/out/host_debug')
      .createSync(recursive: true);
    fileSystem
      .file(kDotPackages)
      .writeAsStringSync('sky_engine:file://$kArbitraryEngineRoot/src/out/ios_debug/gen/dart-pkg/sky_engine/lib/');
    fileSystem
      .file('bin/cache/pkg/sky_engine/lib')
      .createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: '',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: 'ios_debug', localHostEngine: 'host_debug'),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug',
        targetEngine: '/arbitrary/engine/src/out/ios_debug',
      ),
    );
    expect(logger.traceText, contains('Local engine source at /arbitrary/engine/src'));

    // Verify that this also works if the sky_engine path is a symlink to the engine root.
    fileSystem.link('/symlink').createSync(kArbitraryEngineRoot);
    fileSystem
      .file(kDotPackages)
      .writeAsStringSync('sky_engine:file:///symlink/src/out/ios_debug/gen/dart-pkg/sky_engine/lib/');

    expect(
      await localEngineLocator.findEnginePath(localEngine: 'ios_debug', localHostEngine: 'host_debug'),
      matchesEngineBuildPaths(
        hostEngine: '/symlink/src/out/host_debug',
        targetEngine: '/symlink/src/out/ios_debug',
      ),
    );
    expect(logger.traceText, contains('Local engine source at /symlink/src'));
  });

  testWithoutContext('works if --local-engine is specified and --local-engine-src-path '
    'is specified', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    // Intentionally do not create a package_config to verify that it is not required.
    fileSystem.directory('$kArbitraryEngineRoot/src/out/ios_debug').createSync(recursive: true);
    fileSystem.directory('$kArbitraryEngineRoot/src/out/host_debug').createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: '',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(engineSourcePath: '$kArbitraryEngineRoot/src', localEngine: 'ios_debug', localHostEngine: 'host_debug'),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug',
        targetEngine: '/arbitrary/engine/src/out/ios_debug',
      ),
    );
    expect(logger.traceText, contains('Local engine source at /arbitrary/engine/src'));
  });

  testWithoutContext('works if --local-engine is specified and --local-engine-host is specified', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/android_debug_unopt_arm64/')
        ..createSync(recursive: true);
    fileSystem.directory('$kArbitraryEngineRoot/src/out/host_debug_unopt_arm64/').createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug_unopt_arm64'),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug_unopt_arm64',
        targetEngine: '/arbitrary/engine/src/out/android_debug_unopt_arm64',
      ),
    );
    expect(logger.traceText, contains('Local engine source at /arbitrary/engine/src'));
  });

  testWithoutContext('fails if --local-engine-host is omitted', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/android_debug_unopt_arm64/')
        ..createSync(recursive: true);
    fileSystem.directory('$kArbitraryEngineRoot/src/out/host_debug_unopt/').createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    await expectLater(
      localEngineLocator.findEnginePath(localEngine: localEngine.path),
      throwsToolExit(message: 'You are using a locally built engine (--local-engine) but have not specified --local-engine-host'),
    );
  });

  testWithoutContext('works if --local-engine is specified and --local-engine-src-path '
      'is determined by --local-engine', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/ios_debug/')
        ..createSync(recursive: true);
    fileSystem.directory('$kArbitraryEngineRoot/src/out/host_debug/').createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug'),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug',
        targetEngine: '/arbitrary/engine/src/out/ios_debug',
      ),
    );
    expect(logger.traceText, contains('Parsed engine source from local engine as /arbitrary/engine/src'));
    expect(logger.traceText, contains('Local engine source at /arbitrary/engine/src'));
  });

  testWithoutContext('works if local engine is host engine', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/host_debug/')
      ..createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: localEngine.path),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug',
        targetEngine: '/arbitrary/engine/src/out/host_debug',
      ),
    );
    expect(logger.traceText, contains('Local engine source at /arbitrary/engine/src'));
  });

  testWithoutContext('works if local engine is host engine with suffixes', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/host_debug_unopt_arm64/')
      ..createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: localEngine.path),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug_unopt_arm64',
        targetEngine: '/arbitrary/engine/src/out/host_debug_unopt_arm64',
      ),
    );
  });

  testWithoutContext('works if local engine is simulator', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/ios_debug_sim/')
      ..createSync(recursive: true);
    fileSystem
        .directory('$kArbitraryEngineRoot/src/out/host_debug/')
        .createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug'),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug',
        targetEngine: '/arbitrary/engine/src/out/ios_debug_sim',
      ),
    );
  });

  testWithoutContext('works if local engine is simulator unoptimized',
      () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/ios_debug_sim_unopt/')
      ..createSync(recursive: true);
    fileSystem
        .directory('$kArbitraryEngineRoot/src/out/host_debug_unopt/')
        .createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug_unopt'),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/host_debug_unopt',
        targetEngine: '/arbitrary/engine/src/out/ios_debug_sim_unopt',
      ),
    );
  });

  testWithoutContext('fails if host_debug does not exist', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/ios_debug/')
      ..createSync(recursive: true);

    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    await expectToolExitLater(
      localEngineLocator.findEnginePath(localEngine: localEngine.path, localHostEngine: 'host_debug'),
      contains('No Flutter engine build found at /arbitrary/engine/src/out/host_debug'),
    );
  });

  testWithoutContext('works if --local-engine is specified and --local-engine-src-path '
    'is determined by flutter root', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    fileSystem.file(kDotPackages).writeAsStringSync('\n');
    fileSystem
      .directory('$kEngineRoot/src/out/ios_debug')
      .createSync(recursive: true);
    fileSystem
      .directory('$kEngineRoot/src/out/host_debug')
      .createSync(recursive: true);
    fileSystem
      .file('bin/cache/pkg/sky_engine/lib')
      .createSync(recursive: true);

    final BufferLogger logger = BufferLogger.test();
    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: logger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localEngineLocator.findEnginePath(localEngine: 'ios_debug', localHostEngine: 'host_debug'),
      matchesEngineBuildPaths(
        hostEngine: 'flutter/engine/src/out/host_debug',
        targetEngine: 'flutter/engine/src/out/ios_debug',
      ),
    );
    expect(logger.traceText, contains('Local engine source at flutter/engine/src'));
  });

  testWithoutContext('fails if --local-engine is specified and --local-engine-src-path '
      'cannot be determined', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();

    final LocalEngineLocator localEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    await expectToolExitLater(
      localEngineLocator.findEnginePath(localEngine: '/path/to/nothing', localHostEngine: '/path/to/nothing'),
      contains('Unable to detect local Flutter engine src directory'),
    );
  });

  testWithoutContext('works for local web engine', () async {
    final FileSystem fileSystem = MemoryFileSystem.test();
    final Directory localWasmEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/wasm_whatever/')
      ..createSync(recursive: true);
    final Directory localWebEngine = fileSystem
        .directory('$kArbitraryEngineRoot/src/out/web_whatever/')
      ..createSync(recursive: true);

    final BufferLogger wasmLogger = BufferLogger.test();
    final LocalEngineLocator localWasmEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: wasmLogger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localWasmEngineLocator.findEnginePath(localEngine: localWasmEngine.path, localHostEngine: localWasmEngine.path),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/wasm_whatever',
        targetEngine: '/arbitrary/engine/src/out/wasm_whatever',
      ),
    );
    expect(wasmLogger.traceText, contains('Local engine source at /arbitrary/engine/src'));

    final BufferLogger webLogger = BufferLogger.test();
    final LocalEngineLocator localWebEngineLocator = LocalEngineLocator(
      fileSystem: fileSystem,
      flutterRoot: 'flutter/flutter',
      logger: webLogger,
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    expect(
      await localWebEngineLocator.findEnginePath(localEngine: localWebEngine.path, localHostEngine: localWebEngine.path),
      matchesEngineBuildPaths(
        hostEngine: '/arbitrary/engine/src/out/web_whatever',
        targetEngine: '/arbitrary/engine/src/out/web_whatever',
      ),
    );
    expect(webLogger.traceText, contains('Local engine source at /arbitrary/engine/src'));
  });

  test('returns null without throwing if nothing is specified', () async {
    final LocalEngineLocator localWebEngineLocator = LocalEngineLocator(
      fileSystem: MemoryFileSystem.test(),
      flutterRoot: 'flutter/flutter',
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{}),
    );

    final EngineBuildPaths? paths = await localWebEngineLocator.findEnginePath();
    expect(paths, isNull);
  });

  test('throws if nothing is specified but the FLUTTER_ENGINE environment variable is set', () async {
    final LocalEngineLocator localWebEngineLocator = LocalEngineLocator(
      fileSystem: MemoryFileSystem.test(),
      flutterRoot: 'flutter/flutter',
      logger: BufferLogger.test(),
      userMessages: UserMessages(),
      platform: FakePlatform(environment: <String, String>{'FLUTTER_ENGINE': 'blah'}),
    );

    await expectToolExitLater(
      localWebEngineLocator.findEnginePath(),
      contains('Unable to detect a Flutter engine build directory in blah'),
    );
  });
}

Matcher matchesEngineBuildPaths({
  String? hostEngine,
  String? targetEngine,
}) {
  return const TypeMatcher<EngineBuildPaths>()
    .having((EngineBuildPaths paths) => paths.hostEngine, 'hostEngine', hostEngine)
    .having((EngineBuildPaths paths) => paths.targetEngine, 'targetEngine', targetEngine);
}