// 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:typed_data';

import 'package:process/process.dart';

import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../convert.dart';
import '../web/compile.dart';
import 'test_compiler.dart';
import 'test_config.dart';

/// Helper class to start golden file comparison in a separate process.
///
/// The golden file comparator is configured using flutter_test_config.dart and that
/// file can contain arbitrary Dart code that depends on dart:ui. Thus it has to
/// be executed in a `flutter_tester` environment. This helper class generates a
/// Dart file configured with flutter_test_config.dart to perform the comparison
/// of golden files.
class TestGoldenComparator {
  /// Creates a [TestGoldenComparator] instance.
  TestGoldenComparator(this.shellPath, this.compilerFactory, {
    required Logger logger,
    required FileSystem fileSystem,
    required ProcessManager processManager,
    required this.webRenderer,
  }) : tempDir = fileSystem.systemTempDirectory.createTempSync('flutter_web_platform.'),
       _logger = logger,
       _fileSystem = fileSystem,
       _processManager = processManager;

  final String? shellPath;
  final Directory tempDir;
  final TestCompiler Function() compilerFactory;
  final Logger _logger;
  final FileSystem _fileSystem;
  final ProcessManager _processManager;
  final WebRendererMode webRenderer;

  TestCompiler? _compiler;
  TestGoldenComparatorProcess? _previousComparator;
  Uri? _previousTestUri;

  Future<void> close() async {
    tempDir.deleteSync(recursive: true);
    await _compiler?.dispose();
    await _previousComparator?.close();
  }

  /// Start golden comparator in a separate process. Start one file per test file
  /// to reduce the overhead of starting `flutter_tester`.
  Future<TestGoldenComparatorProcess?> _processForTestFile(Uri testUri) async {
    if (testUri == _previousTestUri) {
      return _previousComparator!;
    }

    final String bootstrap = TestGoldenComparatorProcess.generateBootstrap(_fileSystem.file(testUri), testUri, logger: _logger);
    final Process? process = await _startProcess(bootstrap);
    if (process == null) {
      return null;
    }
    unawaited(_previousComparator?.close());
    _previousComparator = TestGoldenComparatorProcess(process, logger: _logger);
    _previousTestUri = testUri;

    return _previousComparator!;
  }

  Future<Process?> _startProcess(String testBootstrap) async {
    // Prepare the Dart file that will talk to us and start the test.
    final File listenerFile = (await tempDir.createTemp('listener')).childFile('listener.dart');
    await listenerFile.writeAsString(testBootstrap);

    // Lazily create the compiler
    _compiler = _compiler ?? compilerFactory();
    final String? output = await _compiler!.compile(listenerFile.uri);
    if (output == null) {
      return null;
    }
    final List<String> command = <String>[
      shellPath!,
      '--disable-vm-service',
      '--non-interactive',
      '--packages=${_fileSystem.path.join('.dart_tool', 'package_config.json')}',
      output,
    ];

    final Map<String, String> environment = <String, String>{
      // Chrome is the only supported browser currently.
      'FLUTTER_TEST_BROWSER': 'chrome',
      'FLUTTER_WEB_RENDERER': webRenderer == WebRendererMode.html ? 'html' : 'canvaskit',
    };
    return _processManager.start(command, environment: environment);
  }

  Future<String?> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool? updateGoldens) async {
    final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
    final TestGoldenComparatorProcess? process = await _processForTestFile(testUri);
    if (process == null) {
      return 'process was null';
    }

    process.sendCommand(imageFile, goldenKey, updateGoldens);

    final Map<String, dynamic> result = await process.getResponse();
    return (result['success'] as bool) ? null : ((result['message'] as String?) ?? 'does not match');
  }
}

/// Represents a `flutter_tester` process started for golden comparison. Also
/// handles communication with the child process.
class TestGoldenComparatorProcess {
  /// Creates a [TestGoldenComparatorProcess] backed by [process].
  TestGoldenComparatorProcess(this.process, {required Logger logger}) : _logger = logger {
    // Pipe stdout and stderr to printTrace and printError.
    // Also parse stdout as a stream of JSON objects.
    streamIterator = StreamIterator<Map<String, dynamic>>(
      process.stdout
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter())
        .where((String line) {
          logger.printTrace('<<< $line');
          return line.isNotEmpty && line[0] == '{';
        })
        .map<dynamic>(jsonDecode)
        .cast<Map<String, dynamic>>());

    process.stderr
        .transform<String>(utf8.decoder)
        .transform<String>(const LineSplitter())
        .forEach((String line) {
          logger.printError('<<< $line');
        });
  }

  final Logger _logger;
  final Process process;
  late StreamIterator<Map<String, dynamic>> streamIterator;

  Future<void> close() async {
    process.kill();
    await process.exitCode;
  }

  void sendCommand(File imageFile, Uri? goldenKey, bool? updateGoldens) {
    final Object command = jsonEncode(<String, dynamic>{
      'imageFile': imageFile.path,
      'key': goldenKey.toString(),
      'update': updateGoldens,
    });
    _logger.printTrace('Preparing to send command: $command');
    process.stdin.writeln(command);
  }

  Future<Map<String, dynamic>> getResponse() async {
    final bool available = await streamIterator.moveNext();
    assert(available);
    return streamIterator.current;
  }

  static String generateBootstrap(File testFile, Uri testUri, {required Logger logger}) {
    final File? testConfigFile = findTestConfigFile(testFile, logger);
    // Generate comparator process for the file.
    return '''
import 'dart:convert'; // flutter_ignore: dart_convert_import
import 'dart:io'; // flutter_ignore: dart_io_import

import 'package:flutter_test/flutter_test.dart';

${testConfigFile != null ? "import '${Uri.file(testConfigFile.path)}' as test_config;" : ""}

void main() async {
  LocalFileComparator comparator = LocalFileComparator(Uri.parse('$testUri'));
  goldenFileComparator = comparator;

  ${testConfigFile != null ? 'test_config.testExecutable(() async {' : ''}
  final commands = stdin
    .transform<String>(utf8.decoder)
    .transform<String>(const LineSplitter())
    .map<dynamic>(jsonDecode);
  await for (final dynamic command in commands) {
    if (command is Map<String, dynamic>) {
      File imageFile = File(command['imageFile'] as String);
      Uri goldenKey = Uri.parse(command['key'] as String);
      bool update = command['update'] as bool;

      final bytes = await File(imageFile.path).readAsBytes();
      if (update) {
        await goldenFileComparator.update(goldenKey, bytes);
        print(jsonEncode({'success': true}));
      } else {
        try {
          bool success = await goldenFileComparator.compare(bytes, goldenKey);
          print(jsonEncode({'success': success}));
        } on Exception catch (ex) {
          print(jsonEncode({'success': false, 'message': '\$ex'}));
        }
      }
    } else {
      print('object type is not right');
    }
  }
  ${testConfigFile != null ? '});' : ''}
}
    ''';
  }
}
