// Copyright (c) 2014, 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:convert';

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() {
  compileFormatterExecutable();

  test('formats a directory', () async {
    await d.dir('code', [
      d.file('a.dart', unformattedSource),
      d.file('b.dart', formattedSource),
      d.file('c.dart', unformattedSource)
    ]).create();

    var process = await runFormatterOnDir();
    await expectLater(
        process.stdout, emits(startsWith('Formatting directory')));

    // Prints the formatting result.
    await expectLater(process.stdout, emits(formattedOutput));
    await expectLater(process.stdout, emits(formattedOutput));
    await expectLater(process.stdout, emits(formattedOutput));
    await process.shouldExit(0);

    // Does not overwrite by default.
    await d.dir('code', [d.file('a.dart', unformattedSource)]).validate();
    await d.dir('code', [d.file('c.dart', unformattedSource)]).validate();
  });

  test('Overwrites files', () async {
    await d.dir('code', [
      d.file('a.dart', unformattedSource),
      d.file('b.dart', formattedSource),
      d.file('c.dart', unformattedSource)
    ]).create();

    var process = await runFormatterOnDir(['--overwrite']);
    await expectLater(
        process.stdout, emits(startsWith('Formatting directory')));

    // Prints whether each file was changed.
    await expectLater(
        process.stdout, emits('Formatted ${p.join('code', 'a.dart')}'));
    await expectLater(
        process.stdout, emits('Unchanged ${p.join('code', 'b.dart')}'));
    await expectLater(
        process.stdout, emits('Formatted ${p.join('code', 'c.dart')}'));
    await process.shouldExit(0);

    // Overwrites the files.
    await d.dir('code', [d.file('a.dart', formattedSource)]).validate();
    await d.dir('code', [d.file('c.dart', formattedSource)]).validate();
  });

  test('exits with 64 on a command line argument error', () async {
    var process = await runFormatter(['-wat']);
    await process.shouldExit(64);
  });

  test('exits with 65 on a parse error', () async {
    await d.dir('code', [d.file('a.dart', 'herp derp i are a dart')]).create();

    var process = await runFormatterOnDir();
    await process.shouldExit(65);
  });

  test('errors if --dry-run and --overwrite are both passed', () async {
    var process = await runFormatter(['--dry-run', '--overwrite']);
    await process.shouldExit(64);
  });

  test('errors if --dry-run and --machine are both passed', () async {
    var process = await runFormatter(['--dry-run', '--machine']);
    await process.shouldExit(64);
  });

  test('errors if --machine and --overwrite are both passed', () async {
    var process = await runFormatter(['--machine', '--overwrite']);
    await process.shouldExit(64);
  });

  test('errors if --dry-run and --machine are both passed', () async {
    var process = await runFormatter(['--dry-run', '--machine']);
    await process.shouldExit(64);
  });

  test('errors if --machine and --overwrite are both passed', () async {
    var process = await runFormatter(['--machine', '--overwrite']);
    await process.shouldExit(64);
  });

  test('errors if --stdin-name and a path are both passed', () async {
    var process = await runFormatter(['--stdin-name=name', 'path.dart']);
    await process.shouldExit(64);
  });

  test('--version prints the version number', () async {
    var process = await runFormatter(['--version']);

    // Match something roughly semver-like.
    expect(await process.stdout.next, matches(RegExp(r'\d+\.\d+\.\d+.*')));
    await process.shouldExit(0);
  });

  group('--help', () {
    test('non-verbose shows description and common options', () async {
      var process = await runFormatter(['--help']);
      await expectLater(
          process.stdout, emits('Idiomatically format Dart source code.'));
      await expectLater(process.stdout, emits(''));
      await expectLater(process.stdout,
          emits('Usage:   dartfmt [options...] [files or directories...]'));
      await expectLater(process.stdout, emitsThrough(contains('--overwrite')));
      await expectLater(
          process.stdout, emitsThrough(contains('--set-exit-if-changed')));
      await process.shouldExit(0);
    });

    test('verbose shows description and all options', () async {
      var process = await runFormatter(['--help', '--verbose']);
      await expectLater(
          process.stdout, emits('Idiomatically format Dart source code.'));
      await expectLater(process.stdout, emits(''));
      await expectLater(process.stdout,
          emits('Usage:   dartfmt [options...] [files or directories...]'));
      await expectLater(process.stdout, emitsThrough(contains('--overwrite')));
      await expectLater(
          process.stdout, emitsThrough(contains('--set-exit-if-changed')));
      await expectLater(process.stdout, emitsThrough(contains('--fix')));
      await process.shouldExit(0);
    });
  });

  test('--verbose errors if not used with --help', () async {
    var process = await runFormatterOnDir(['--verbose']);
    expect(await process.stdout.next, 'Can only use --verbose with --help.');
    await process.shouldExit(64);
  });

  test('only prints a hidden directory once', () async {
    await d.dir('code', [
      d.dir('.skip', [
        d.file('a.dart', unformattedSource),
        d.file('b.dart', unformattedSource)
      ])
    ]).create();

    var process = await runFormatterOnDir();

    expect(await process.stdout.next, startsWith('Formatting directory'));
    expect(await process.stdout.next,
        "Skipping hidden path ${p.join("code", ".skip")}");
    await process.shouldExit();
  });

  group('--dry-run', () {
    test('prints names of files that would change', () async {
      await d.dir('code', [
        d.file('a_bad.dart', unformattedSource),
        d.file('b_good.dart', formattedSource),
        d.file('c_bad.dart', unformattedSource),
        d.file('d_good.dart', formattedSource)
      ]).create();

      var aBad = p.join('code', 'a_bad.dart');
      var cBad = p.join('code', 'c_bad.dart');

      var process = await runFormatterOnDir(['--dry-run']);

      // The order isn't specified.
      expect(await process.stdout.next, anyOf(aBad, cBad));
      expect(await process.stdout.next, anyOf(aBad, cBad));
      await process.shouldExit();
    });

    test('does not modify files', () async {
      await d.dir('code', [d.file('a.dart', unformattedSource)]).create();

      var process = await runFormatterOnDir(['--dry-run']);
      expect(await process.stdout.next, p.join('code', 'a.dart'));
      await process.shouldExit();

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

  group('--machine', () {
    test('writes each output as json', () async {
      await d.dir('code', [
        d.file('a.dart', unformattedSource),
        d.file('b.dart', unformattedSource)
      ]).create();

      var jsonA = jsonEncode({
        'path': p.join('code', 'a.dart'),
        'source': formattedSource,
        'selection': {'offset': -1, 'length': -1}
      });

      var jsonB = jsonEncode({
        'path': p.join('code', 'b.dart'),
        'source': formattedSource,
        'selection': {'offset': -1, 'length': -1}
      });

      var process = await runFormatterOnDir(['--machine']);

      // The order isn't specified.
      expect(await process.stdout.next, anyOf(jsonA, jsonB));
      expect(await process.stdout.next, anyOf(jsonA, jsonB));
      await process.shouldExit();
    });
  });

  group('--preserve', () {
    test('errors if given paths', () async {
      var process = await runFormatter(['--preserve', 'path', 'another']);
      await process.shouldExit(64);
    });

    test('errors on wrong number of components', () async {
      var process = await runFormatter(['--preserve', '1']);
      await process.shouldExit(64);

      process = await runFormatter(['--preserve', '1:2:3']);
      await process.shouldExit(64);
    });

    test('errors on non-integer component', () async {
      var process = await runFormatter(['--preserve', '1:2.3']);
      await process.shouldExit(64);
    });

    test('updates selection', () async {
      var process = await runFormatter(['--preserve', '6:10', '-m']);
      process.stdin.writeln(unformattedSource);
      await process.stdin.close();

      var json = jsonEncode({
        'path': 'stdin',
        'source': formattedSource,
        'selection': {'offset': 5, 'length': 9}
      });

      expect(await process.stdout.next, json);
      await process.shouldExit();
    });
  });

  group('--indent', () {
    test('sets the leading indentation of the output', () async {
      var process = await runFormatter(['--indent', '3']);
      process.stdin.writeln("main() {'''");
      process.stdin.writeln("a flush left multi-line string''';}");
      await process.stdin.close();

      expect(await process.stdout.next, '   main() {');
      expect(await process.stdout.next, "     '''");
      expect(await process.stdout.next, "a flush left multi-line string''';");
      expect(await process.stdout.next, '   }');
      await process.shouldExit(0);
    });

    test('errors if the indent is not a non-negative number', () async {
      var process = await runFormatter(['--indent', 'notanum']);
      await process.shouldExit(64);

      process = await runFormatter(['--indent', '-4']);
      await process.shouldExit(64);
    });
  });

  group('--set-exit-if-changed', () {
    test('gives exit code 0 if there are no changes', () async {
      await d.dir('code', [d.file('a.dart', formattedSource)]).create();

      var process = await runFormatterOnDir(['--set-exit-if-changed']);
      await process.shouldExit(0);
    });

    test('gives exit code 1 if there are changes', () async {
      await d.dir('code', [d.file('a.dart', unformattedSource)]).create();

      var process = await runFormatterOnDir(['--set-exit-if-changed']);
      await process.shouldExit(1);
    });

    test('gives exit code 1 if there are changes even in dry run', () async {
      await d.dir('code', [d.file('a.dart', unformattedSource)]).create();

      var process =
          await runFormatterOnDir(['--set-exit-if-changed', '--dry-run']);
      await process.shouldExit(1);
    });
  });

  group('fix', () {
    test('--fix applies all fixes', () async {
      var process = await runFormatter(['--fix']);
      process.stdin.writeln('foo({a:1}) {');
      process.stdin.writeln('  new Bar(const Baz(const []));}');
      await process.stdin.close();

      expect(await process.stdout.next, 'foo({a = 1}) {');
      expect(await process.stdout.next, '  Bar(const Baz([]));');
      expect(await process.stdout.next, '}');
      await process.shouldExit(0);
    });

    test('--fix-named-default-separator', () async {
      var process = await runFormatter(['--fix-named-default-separator']);
      process.stdin.writeln('foo({a:1}) {');
      process.stdin.writeln('  new Bar();}');
      await process.stdin.close();

      expect(await process.stdout.next, 'foo({a = 1}) {');
      expect(await process.stdout.next, '  new Bar();');
      expect(await process.stdout.next, '}');
      await process.shouldExit(0);
    });

    test('--fix-optional-const', () async {
      var process = await runFormatter(['--fix-optional-const']);
      process.stdin.writeln('foo({a:1}) {');
      process.stdin.writeln('  const Bar(const Baz());}');
      await process.stdin.close();

      expect(await process.stdout.next, 'foo({a: 1}) {');
      expect(await process.stdout.next, '  const Bar(Baz());');
      expect(await process.stdout.next, '}');
      await process.shouldExit(0);
    });

    test('--fix-optional-new', () async {
      var process = await runFormatter(['--fix-optional-new']);
      process.stdin.writeln('foo({a:1}) {');
      process.stdin.writeln('  new Bar();}');
      await process.stdin.close();

      expect(await process.stdout.next, 'foo({a: 1}) {');
      expect(await process.stdout.next, '  Bar();');
      expect(await process.stdout.next, '}');
      await process.shouldExit(0);
    });

    test('errors with --fix and specific fix flag', () async {
      var process =
          await runFormatter(['--fix', '--fix-named-default-separator']);
      await process.shouldExit(64);
    });
  });

  group('with no paths', () {
    test('errors on --overwrite', () async {
      var process = await runFormatter(['--overwrite']);
      await process.shouldExit(64);
    });

    test('exits with 65 on parse error', () async {
      var process = await runFormatter();
      process.stdin.writeln('herp derp i are a dart');
      await process.stdin.close();
      await process.shouldExit(65);
    });

    test('reads from stdin', () async {
      var process = await runFormatter();
      process.stdin.writeln(unformattedSource);
      await process.stdin.close();

      // No trailing newline at the end.
      expect(await process.stdout.next, formattedOutput);
      await process.shouldExit(0);
    });

    test('allows specifying stdin path name', () async {
      var path = p.join('some', 'path.dart');
      var process = await runFormatter(['--stdin-name=$path']);
      process.stdin.writeln('herp');
      await process.stdin.close();

      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(path));
      await process.stderr.cancel();
      await process.shouldExit(65);
    });
  });
}
