// Copyright (c) 2024, 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 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

import '../utils.dart';

void main() {
  compileFormatter();

  group('--language-version', () {
    // It's hard to validate that the formatter uses the *exact* latest
    // language version supported by the formatter, but at least test that a
    // new-ish language feature can be parsed.
    const extensionTypeBefore = '''
extension type Meters(int value) {
  Meters operator+(Meters other) => Meters(value+other.value);
}''';

    const extensionTypeAfter = '''
extension type Meters(int value) {
  Meters operator +(Meters other) => Meters(value + other.value);
}
''';

    test('uses latest language version if no surrounding package', () async {
      await d.dir('code', [d.file('a.dart', extensionTypeBefore)]).create();

      var process = await runFormatterOnDir();
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', extensionTypeAfter)]).validate();
    });

    test('uses the given language version', () async {
      const before = 'main() { switch (o) { case 1+2: break; } }';

      const after = '''
main() {
  switch (o) {
    case 1 + 2:
      break;
  }
}
''';

      await d.dir('code', [d.file('a.dart', before)]).create();

      // Use an older language version where `1 + 2` was still a valid switch
      // case.
      var process = await runFormatterOnDir(['--language-version=2.19']);
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', after)]).validate();
    });

    test('uses the latest language version if "latest"', () async {
      await d.dir('code', [d.file('a.dart', extensionTypeBefore)]).create();

      var process = await runFormatterOnDir(['--language-version=latest']);
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', extensionTypeAfter)]).validate();
    });

    test("errors if the language version can't be parsed", () async {
      var process = await runFormatter(['--language-version=123']);
      await process.shouldExit(64);
    });
  });

  group('package config', () {
    test('no package search if language version is specified', () async {
      // Put the file in a directory with a malformed package config. If we
      // search for it, we should get an error.
      await d.dir('foo', [
        d.dir('.dart_tool', [
          d.file('package_config.json', 'this no good json is bad json'),
        ]),
        d.file('main.dart', 'main(){    }'),
      ]).create();

      var process = await runFormatterOnDir(['--language-version=latest']);
      await process.shouldExit(0);

      // Should format the file without any error reading the package config.
      await d.dir('foo', [d.file('main.dart', 'main() {}\n')]).validate();
    });

    test('default to language version of surrounding package', () async {
      // The package config sets the language version to 2.19, but pattern
      // variables are only available in 3.0 and later. Verify that the error
      // is reported.
      await d.dir('foo', [
        packageConfig('foo', version: '2.19'),
        d.file('main.dart', 'main() { var (a, b) = (1, 2); }'),
      ]).create();

      var path = p.join(d.sandbox, 'foo', 'main.dart');
      var process = await runFormatter([path]);

      expect(
        await process.stderr.next,
        'Could not format because the source could not be parsed:',
      );
      expect(await process.stderr.next, '');
      expect(await process.stderr.next, contains('main.dart'));
      await process.shouldExit(65);
    });

    test('language version comment overrides package default', () async {
      // The package config sets the language version to 3.1, but the switch
      // case uses a syntax which is valid in earlier versions of Dart but an
      // error in 3.0 and later. Verify that no error is reported since this
      // file opts to the older version.
      await d.dir('foo', [
        packageConfig('foo', version: '3.1'),
        d.file('main.dart', '''
          // @dart=2.19
          main() { switch (obj) { case 1 + 2: // Error in 3.1.
            } }
          '''),
      ]).create();

      var process = await runFormatterOnDir();
      await process.shouldExit(0);

      // Formats the file.
      await d.dir('foo', [
        d.file('main.dart', '''
// @dart=2.19
main() {
  switch (obj) {
    case 1 + 2: // Error in 3.1.
  }
}
'''),
      ]).validate();
    });

    test('use the latest version if the package config is malformed', () async {
      await d.dir('foo', [
        d.dir('.dart_tool', [
          d.file('package_config.json', 'this no good json is bad json'),
        ]),
        d.file('main.dart', 'main() {var (a,b)=(1,2);}'),
      ]).create();

      var process = await runFormatterOnDir();
      await process.shouldExit(0);

      // Formats the file.
      await d.dir('foo', [
        d.file('main.dart', '''
main() {
  var (a, b) = (1, 2);
}
'''),
      ]).validate();
    });
  });

  group('stdin', () {
    test('infers language version from surrounding package', () async {
      // The package config sets the language version to 2.19, when switch
      // cases still allowed arbitrary constant expressions like `1 + 2`.
      // Verify that the code is formatted without error.
      await d.dir('foo', [packageConfig('foo', version: '2.19')]).create();

      var process = await runFormatter(['--stdin-name=foo/main.dart']);
      // Write a switch whose syntax is valid in 2.19, but an error in later
      // versions.
      process.stdin.writeln('main() { switch (o) { case 1 + 2: break; } }');
      await process.stdin.close();

      await expectLater(
        process.stdout,
        emitsInOrder([
          'main() {',
          '  switch (o) {',
          '    case 1 + 2:',
          '      break;',
          '  }',
          '}',
        ]),
      );
      await process.shouldExit(0);
    });

    test('no package search if language version is specified', () async {
      // Put the stdin-name in a directory with a malformed package config. If
      // we search for it, we should get an error.
      await d.dir('foo', [
        d.dir('.dart_tool', [
          d.file('package_config.json', 'this no good json is bad json'),
        ]),
        d.file('main.dart', 'main(){    }'),
      ]).create();

      var process = await runFormatter([
        '--language-version=2.19',
        '--stdin-name=foo/main.dart',
      ]);

      // Write a switch whose syntax is valid in 2.19, but an error in later
      // versions.
      process.stdin.writeln('main() { switch (o) { case 1 + 2: break; } }');
      await process.stdin.close();

      await expectLater(
        process.stdout,
        emitsInOrder([
          'main() {',
          '  switch (o) {',
          '    case 1 + 2:',
          '      break;',
          '  }',
          '}',
        ]),
      );
      await process.shouldExit(0);
    });

    test('use latest language version if no surrounding package', () async {
      await d.dir('foo', []).create();

      var process = await runFormatter(['--stdin-name=foo/main.dart']);
      // Use some relatively recent syntax.
      process.stdin.writeln('main() {var (a,b)=(1,2);}');
      await process.stdin.close();

      await expectLater(
        process.stdout,
        emitsInOrder(['main() {', '  var (a, b) = (1, 2);', '}']),
      );
      await process.shouldExit(0);
    });
  });

  group('style', () {
    test('uses the short style on 3.6 or earlier', () async {
      const before = 'main() { f(argument, // comment\nanother);}';
      const after = '''
main() {
  f(
      argument, // comment
      another);
}
''';

      await d.dir('code', [d.file('a.dart', before)]).create();

      var process = await runFormatterOnDir(['--language-version=3.6']);
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', after)]).validate();
    });

    test('uses the 3.7 tall style on 3.7', () async {
      const before = 'main() { x = cond ? a // comment\n: b;}';
      const after = '''
main() {
  x =
      cond
          ? a // comment
          : b;
}
''';

      await d.dir('code', [d.file('a.dart', before)]).create();

      var process = await runFormatterOnDir(['--language-version=3.7']);
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', after)]).validate();
    });

    test('uses the 3.8 tall style on 3.8 or later', () async {
      const before = 'main() { x = cond ? a // comment\n: b;}';
      const after = '''
main() {
  x = cond
      ? a // comment
      : b;
}
''';

      await d.dir('code', [d.file('a.dart', before)]).create();

      var process = await runFormatterOnDir(['--language-version=3.8']);
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', after)]).validate();
    });

    test('language version comment override opts into short style', () async {
      const before = '''
// @dart=3.6
main() { f(argument, // comment
another);}
''';
      const after = '''
// @dart=3.6
main() {
  f(
      argument, // comment
      another);
}
''';

      await d.dir('code', [d.file('a.dart', before)]).create();

      var process = await runFormatterOnDir(['--language-version=3.7']);
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', after)]).validate();
    });

    test('language version comment override opts into tall style', () async {
      // Note that in real-world code it doesn't make sense for a language
      // version comment to be *higher* than the specified default language
      // version before you can't use a comment that's higher than the minimum
      // version in the package's SDK constraint. (Otherwise, you could end up
      // trying to run a library whose language version isn't supported by the
      // SDK you are running it in.)
      //
      // But we support it in the formatter since it's possible to specify a
      // default language version using mechanisms other than the pubspec SDK
      // constraint.
      const before = '''
// @dart=3.7
main() { f(argument, // comment
another);}
''';
      const after = '''
// @dart=3.7
main() {
  f(
    argument, // comment
    another,
  );
}
''';

      await d.dir('code', [d.file('a.dart', before)]).create();

      var process = await runFormatterOnDir(['--language-version=3.6']);
      await process.shouldExit(0);

      await d.dir('code', [d.file('a.dart', after)]).validate();
    });
  });
}
