// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
// for details. 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:io';

import 'package:dart_style/dart_style.dart';
import 'package:dart_style/src/testing/test_file.dart';
import 'package:path/path.dart' as p;

/// Update the formatting test expectations based on the current formatter's
/// output.
///
/// The command line arguments should be the names of tests to be updated. A
/// name can be a directory to update all of the tests in that directory or a
/// file path to update the tests in that file.
///
/// All paths are relative to the "test" directory.
///
/// Note: This script can't correctly update any tests that contain the special
/// "×XX" Unicode markers or selections.
// TODO(rnystrom): Support updating individual tests within a file.
void main(List<String> arguments) async {
  if (arguments.isEmpty) {
    print('Usage: update_tests.dart <tests...>');
    exit(1);
  }

  for (var argument in arguments) {
    var path = p.join(await findTestDirectory(), argument);
    if (Directory(path).existsSync()) {
      await _updateDirectory(path);
    } else if (File(path).existsSync()) {
      await _updateFile(path);
    }
  }

  if (_totalTests > 0) {
    print('Changed $_changedTests out of $_totalTests updated tests');
  } else {
    print('No updatable tests found');
  }

  if (_skippedFiles > 0) {
    print('Skipped $_skippedFiles files '
        'which contain selections or Unicode escapes');
  }
}

int _totalTests = 0;
int _changedTests = 0;
int _skippedFiles = 0;

Future<void> _updateDirectory(String path) async {
  for (var testFile in await TestFile.listDirectory(path)) {
    await _updateTestFile(testFile);
  }
}

Future<void> _updateFile(String path) async {
  await _updateTestFile(await TestFile.read(path));
}

Future<void> _updateTestFile(TestFile testFile) async {
  // TODO(rnystrom): The test updater doesn't know how to handle selection
  // markers or Unicode escapes in tests, so just skip any file that contains
  // tests with those in it.
  var testSource = File(p.join('test', testFile.path)).readAsStringSync();
  if (testSource.contains('‹') || testSource.contains('×')) {
    print('Skipped ${testFile.path}');

    _skippedFiles++;
    return;
  }

  var buffer = StringBuffer();

  // Write the page width line if needed.
  var pageWidth = testFile.pageWidth;
  if (pageWidth != null) {
    var columns = '$pageWidth columns';
    buffer.write(columns);
    buffer.write(' ' * (pageWidth - columns.length));
    buffer.writeln('|');
  }

  // Write the file-level comments.
  _writeComments(buffer, testFile.comments);

  _totalTests += testFile.tests.length;

  for (var formatTest in testFile.tests) {
    var formatter = DartFormatter(
        languageVersion: formatTest.languageVersion,
        pageWidth: testFile.pageWidth,
        indent: formatTest.leadingIndent,
        experimentFlags: const ['macros']);

    var actual = formatter.formatSource(formatTest.input);

    // The test files always put a newline at the end of the expectation.
    // Statements from the formatter (correctly) don't have that, so add
    // one to line up with the expected result.
    var actualText = actual.text;
    if (!testFile.isCompilationUnit) actualText += '\n';

    // Insert a newline between each test, but not after the last.
    if (formatTest != testFile.tests.first) buffer.writeln();

    var defaultLanguageVersion = p.split(testFile.path).contains('tall')
        ? DartFormatter.latestLanguageVersion
        : DartFormatter.latestShortStyleLanguageVersion;

    var descriptionParts = [
      if (formatTest.leadingIndent != 0) '(indent ${formatTest.leadingIndent})',
      if (formatTest.languageVersion != defaultLanguageVersion)
        '(version ${formatTest.languageVersion.major}.'
            '${formatTest.languageVersion.minor})',
      formatTest.description
    ];

    buffer.writeln('>>> ${descriptionParts.join(' ')}'.trim());
    _writeComments(buffer, formatTest.inputComments);
    buffer.write(formatTest.input.text);

    buffer.writeln('<<< ${formatTest.outputDescription}'.trim());
    _writeComments(buffer, formatTest.outputComments);

    var output = actual.text;

    // Remove the trailing newline so that we don't end up with an extra
    // newline at the end of the test file.
    output = output.trimRight();
    buffer.write(output);

    // Fail with an explicit message because it's easier to read than
    // the matcher output.
    if (actualText != formatTest.output.text) {
      print('Updated ${testFile.path} ${formatTest.label}');
      _changedTests++;
    }
  }

  // Rewrite the file. Do this even if nothing changed so that we normalize the
  // test markers.
  var path = p.join(await findTestDirectory(), testFile.path);
  File(path).writeAsStringSync(buffer.toString());
}

void _writeComments(StringBuffer buffer, List<String> comments) {
  for (var comment in comments) {
    buffer.writeln(comment);
  }
}
