// Copyright (c) 2015, 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.

@TestOn('vm')
library;

import 'dart:async';
import 'dart:io';

import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

import '../io.dart';

void main() {
  setUpAll(precompileTestExecutable);

  test('reports when no tests are run', () async {
    await d.file('test.dart', 'void main() {}').create();

    var test = await runTest(['test.dart'], reporter: 'compact');
    expect(test.stdout, emitsThrough(contains('No tests ran.')));
    await test.shouldExit(79);
  });

  test('runs several successful tests and reports when each completes', () {
    return _expectReport(
      '''
        test('success 1', () {});
        test('success 2', () {});
        test('success 3', () {});''',
      '''
        +0: loading test.dart
        +0: success 1
        +1: success 1
        +1: success 2
        +2: success 2
        +2: success 3
        +3: success 3
        +3: All tests passed!''',
    );
  });

  test('runs several failing tests and reports when each fails', () {
    return _expectReport(
      '''
        test('failure 1', () => throw TestFailure('oh no'));
        test('failure 2', () => throw TestFailure('oh no'));
        test('failure 3', () => throw TestFailure('oh no'));''',
      '''
        +0: loading test.dart
        +0: failure 1
        +0 -1: failure 1 [E]
          oh no
          test.dart 6:33  main.<fn>


        +0 -1: failure 2
        +0 -2: failure 2 [E]
          oh no
          test.dart 7:33  main.<fn>


        +0 -2: failure 3
        +0 -3: failure 3 [E]
          oh no
          test.dart 8:33  main.<fn>


        +0 -3: Some tests failed.''',
    );
  });

  test('includes the full stack trace with --verbose-trace', () async {
    await d.file('test.dart', '''
import 'dart:async';

import 'package:test/test.dart';

void main() {
  test("failure", () => throw "oh no");
}
''').create();

    var test = await runTest([
      '--verbose-trace',
      'test.dart',
    ], reporter: 'compact');
    expect(test.stdout, emitsThrough(contains('dart:async')));
    await test.shouldExit(1);
  });

  test('runs failing tests along with successful tests', () {
    return _expectReport(
      '''
        test('failure 1', () => throw TestFailure('oh no'));
        test('success 1', () {});
        test('failure 2', () => throw TestFailure('oh no'));
        test('success 2', () {});''',
      '''
        +0: loading test.dart
        +0: failure 1
        +0 -1: failure 1 [E]
          oh no
          test.dart 6:33  main.<fn>


        +0 -1: success 1
        +1 -1: success 1
        +1 -1: failure 2
        +1 -2: failure 2 [E]
          oh no
          test.dart 8:33  main.<fn>


        +1 -2: success 2
        +2 -2: success 2
        +2 -2: Some tests failed.''',
    );
  });

  test('gracefully handles multiple test failures in a row', () {
    return _expectReport(
      '''
        // This completer ensures that the test isolate isn't killed until all
        // errors have been thrown.
        var completer = Completer();
        test('failures', () {
          Future.microtask(() => throw 'first error');
          Future.microtask(() => throw 'second error');
          Future.microtask(() => throw 'third error');
          Future.microtask(completer.complete);
        });
        test('wait', () => completer.future);''',
      '''
        +0: loading test.dart
        +0: failures
        +0 -1: failures [E]
          first error
          test.dart 10:34  main.<fn>.<fn>
          ===== asynchronous gap ===========================
          dart:async       new Future.microtask
          test.dart 10:18  main.<fn>

          second error
          test.dart 11:34  main.<fn>.<fn>
          ===== asynchronous gap ===========================
          dart:async       new Future.microtask
          test.dart 11:18  main.<fn>

          third error
          test.dart 12:34  main.<fn>.<fn>
          ===== asynchronous gap ===========================
          dart:async       new Future.microtask
          test.dart 12:18  main.<fn>


        +0 -1: wait
        +1 -1: wait
        +1 -1: Some tests failed.''',
    );
  });

  test('prints the full test name before an error', () {
    return _expectReport(
      '''
        test(
           'really gosh dang long test name. Even longer than that. No, yet '
               'longer. Even more. We have to get to at least 200 characters. '
               'I know that seems like a lot, but I believe in you. A little '
               'more... okay, that should do it.',
           () => throw TestFailure('oh no'));''',
      '''
        +0: loading test.dart
        +0: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
        +0 -1: really gosh dang long test name. Even longer than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it. [E]
          oh no
          test.dart 11:18  main.<fn>


        +0 -1: Some tests failed.''',
    );
  });

  group('print:', () {
    test('handles multiple prints', () {
      return _expectReport(
        '''
        test('test', () {
          print("one");
          print("two");
          print("three");
          print("four");
        });''',
        '''
        +0: loading test.dart
        +0: test
        one
        two
        three
        four

        +1: test
        +1: All tests passed!''',
      );
    });

    test('handles a print after the test completes', () {
      return _expectReport(
        '''
        // This completer ensures that the test isolate isn't killed until all
        // prints have happened.
        var testDone = Completer();
        var waitStarted = Completer();
        test('test', () {
          waitStarted.future.then((_) {
            Future(() => print("one"));
            Future(() => print("two"));
            Future(() => print("three"));
            Future(() => print("four"));
            Future(testDone.complete);
          });
        });

        test('wait', () {
          waitStarted.complete();
          return testDone.future;
        });''',
        '''
        +0: loading test.dart
        +0: test
        +1: test
        +1: wait
        +1: test
        one
        two
        three
        four

        +2: wait
        +2: All tests passed!''',
      );
    });

    test('interleaves prints and errors', () {
      return _expectReport(
        '''
        // This completer ensures that the test isolate isn't killed until all
        // prints have happened.
        var completer = Completer();
        test('test', () {
          scheduleMicrotask(() {
            print("three");
            print("four");
            throw "second error";
          });

          scheduleMicrotask(() {
            print("five");
            print("six");
            completer.complete();
          });

          print("one");
          print("two");
          throw "first error";
        });

        test('wait', () => completer.future);''',
        '''
        +0: loading test.dart
        +0: test
        one
        two

        +0 -1: test [E]
          first error
          test.dart 24:11  main.<fn>

        three
        four
          second error
          test.dart 13:13  main.<fn>.<fn>
          ===== asynchronous gap ===========================
          dart:async       scheduleMicrotask
          test.dart 10:11  main.<fn>

        five
        six

        +0 -1: wait
        +1 -1: wait
        +1 -1: Some tests failed.''',
      );
    });

    test('prints the full test name before a print', () {
      return _expectReport(
        '''
          test(
             'really gosh dang long test name. Even longer than that. No, yet '
                 'longer. Even more. We have to get to at least 200 '
                 'characters. I know that seems like a lot, but I believe in '
                 'you. A little more... okay, that should do it.',
             () => print('hello'));''',
        '''
          +0: loading test.dart
          +0: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
          +0: really gosh dang long test name. Even longer than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
          hello

          +1: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
          +1: All tests passed!''',
      );
    });

    test("doesn't print a clock update between two prints", () {
      return _expectReport(
        '''
          test('slow', () async {
            print('hello');
            await Future.delayed(Duration(seconds: 3));
            print('goodbye');
          });''',
        '''
          +0: loading test.dart
          +0: slow
          hello
          goodbye

          +1: slow
          +1: All tests passed!''',
      );
    });
  });

  group('skip:', () {
    test('displays skipped tests separately', () {
      return _expectReport(
        '''
          test('skip 1', () {}, skip: true);
          test('skip 2', () {}, skip: true);
          test('skip 3', () {}, skip: true);''',
        '''
          +0: loading test.dart
          +0: skip 1
          +0 ~1: skip 1
          +0 ~1: skip 2
          +0 ~2: skip 2
          +0 ~2: skip 3
          +0 ~3: skip 3
          +0 ~3: All tests skipped.''',
      );
    });

    test('displays a skipped group', () {
      return _expectReport(
        '''
          group('skip', () {
            test('test 1', () {});
            test('test 2', () {});
            test('test 3', () {});
          }, skip: true);''',
        '''
          +0: loading test.dart
          +0: skip test 1
          +0 ~1: skip test 1
          +0 ~1: skip test 2
          +0 ~2: skip test 2
          +0 ~2: skip test 3
          +0 ~3: skip test 3
          +0 ~3: All tests skipped.''',
      );
    });

    test('runs skipped tests along with successful tests', () {
      return _expectReport(
        '''
          test('skip 1', () {}, skip: true);
          test('success 1', () {});
          test('skip 2', () {}, skip: true);
          test('success 2', () {});''',
        '''
          +0: loading test.dart
          +0: skip 1
          +0 ~1: skip 1
          +0 ~1: success 1
          +1 ~1: success 1
          +1 ~1: skip 2
          +1 ~2: skip 2
          +1 ~2: success 2
          +2 ~2: success 2
          +2 ~2: 2 skipped tests.
          +2 ~2: All other tests passed!''',
      );
    });

    test('runs skipped tests along with successful and failing tests', () {
      return _expectReport(
        '''
          test('failure 1', () => throw TestFailure('oh no'));
          test('skip 1', () {}, skip: true);
          test('success 1', () {});
          test('failure 2', () => throw TestFailure('oh no'));
          test('skip 2', () {}, skip: true);
          test('success 2', () {});''',
        '''
          +0: loading test.dart
          +0: failure 1
          +0 -1: failure 1 [E]
            oh no
            test.dart 6:35  main.<fn>


          +0 -1: skip 1
          +0 ~1 -1: skip 1
          +0 ~1 -1: success 1
          +1 ~1 -1: success 1
          +1 ~1 -1: failure 2
          +1 ~1 -2: failure 2 [E]
            oh no
            test.dart 9:35  main.<fn>


          +1 ~1 -2: skip 2
          +1 ~2 -2: skip 2
          +1 ~2 -2: success 2
          +2 ~2 -2: success 2
          +2 ~2 -2: Some tests failed.''',
      );
    });

    test('hides skip reasons', () {
      return _expectReport(
        '''
          test('skip 1', () {}, skip: 'some reason');
          test('skip 2', () {}, skip: 'or another');''',
        '''
          +0: loading test.dart
          +0 ~1: skip 1
          +0 ~2: skip 2
          +0 ~2: All tests skipped.''',
        disallowedLines: ['some reason', 'or another'],
      );
    });

    test('runs skipped tests with --run-skipped', () {
      return _expectReport(
        '''
          test('skip 1', () {}, skip: 'some reason');
          test('skip 2', () {}, skip: 'or another');''',
        '''
          +0: loading test.dart
          +0: skip 1
          +1: skip 1
          +1: skip 2
          +2: skip 2
          +2: All tests passed!''',
        args: ['--run-skipped'],
      );
    });
  });

  test('Directs users to enable stack trace chaining if disabled', () async {
    await _expectReport(
      '''test('failure 1', () => throw TestFailure('oh no'));''',
      '''
        +0: loading test.dart
        +0: failure 1
        +0 -1: failure 1 [E]
          oh no
          test.dart 6:25  main.<fn>
        +0 -1: Some tests failed.
        Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
        For example, 'dart test --chain-stack-traces'.''',
      chainStackTraces: false,
    );
  });

  group('gives non-windows users a way to re-run failed tests', () {
    final executablePath = p.absolute(Platform.resolvedExecutable);

    test('with simple names', () {
      return _expectReport(
        '''
        test('failure', () {
          expect(1, equals(2));
        });''',
        '''
        +0: loading test.dart
        +0: failure
        +0 -1: failure [E]
          Expected: <2>
            Actual: <1>

        To run this test again: $executablePath test test.dart -p vm --plain-name 'failure'

        +0 -1: Some tests failed.''',
      );
    });

    test('escapes names containing single quotes', () {
      return _expectReport(
        '''
        test("failure with a ' in the name", () {
          expect(1, equals(2));
        });''',
        '''
        +0: loading test.dart
        +0: failure with a ' in the name
        +0 -1: failure with a ' in the name [E]
          Expected: <2>
            Actual: <1>

        To run this test again: $executablePath test test.dart -p vm --plain-name 'failure with a '\\'' in the name'

        +0 -1: Some tests failed.''',
      );
    });
  }, testOn: '!windows');

  group('gives windows users a way to re-run failed tests', () {
    final executablePath = p.absolute(Platform.resolvedExecutable);

    test('with simple names', () {
      return _expectReport(
        '''
        test('failure', () {
          expect(1, equals(2));
        });''',
        '''
        +0: loading test.dart
        +0: failure
        +0 -1: failure [E]
          Expected: <2>
            Actual: <1>

        To run this test again: $executablePath test test.dart -p vm --plain-name "failure"

        +0 -1: Some tests failed.''',
      );
    });

    test('escapes names containing double quotes', () {
      return _expectReport(
        '''
        test('failure with a " in the name', () {
          expect(1, equals(2));
        });''',
        '''
        +0: loading test.dart
        +0: failure with a " in the name
        +0 -1: failure with a " in the name [E]
          Expected: <2>
            Actual: <1>

        To run this test again: $executablePath test test.dart -p vm --plain-name "failure with a """ in the name"

        +0 -1: Some tests failed.''',
      );
    });
  }, testOn: 'windows');
}

Future<void> _expectReport(
  String tests,
  String expected, {
  List<String> args = const [],
  bool chainStackTraces = true,
  Iterable<String>? disallowedLines,
}) async {
  await d.file('test.dart', '''
    import 'dart:async';

    import 'package:test/test.dart';

    void main() {
$tests
    }
  ''').create();

  var test = await runTest([
    'test.dart',
    if (chainStackTraces) '--chain-stack-traces',
    ...args,
  ], reporter: 'compact');
  await test.shouldExit();

  var stdoutLines = await test.stdout.rest.toList();

  // Skip the first CR, remove excess trailing whitespace, and trim off
  // timestamps.
  String? lastLine;
  var actual = stdoutLines
      .skip(1)
      .map((line) {
        if (line.startsWith('  ') || line.isEmpty) return line.trimRight();

        var trimmed = line.trim().replaceFirst(
          RegExp('^[0-9]{2}:[0-9]{2} '),
          '',
        );

        // Trim identical lines so the test isn't dependent on how fast each test
        // runs.
        if (trimmed == lastLine) return null;
        lastLine = trimmed;
        return trimmed;
      })
      .where((line) => line != null);

  // Un-indent the expected string.
  var indentation = expected.indexOf(RegExp('[^ ]'));
  var expectedLines = expected.split('\n').map((line) {
    if (line.isEmpty) return line;
    return line.substring(indentation);
  });

  expect(actual, containsAllInOrder(expectedLines));
  for (final disallowed in disallowedLines ?? const <String>[]) {
    expect(actual, everyElement(isNot(contains(disallowed))));
  }
}
