// 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 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

import '../io.dart';
import 'json_reporter_utils.dart';

void main() {
  setUpAll(precompileTestExecutable);

  test('runs several successful tests and reports when each completes', () {
    return _expectReport(
      '''
      test('success 1', () {});
      test('success 2', () {});
      test('success 3', () {});
    ''',
      [
        [
          suiteJson(0),
          testStartJson(1, 'loading test.dart', groupIDs: []),
          testDoneJson(1, hidden: true),
        ],
        [
          groupJson(2, testCount: 3),
          testStartJson(3, 'success 1', line: 6, column: 7),
          testDoneJson(3),
          testStartJson(4, 'success 2', line: 7, column: 7),
          testDoneJson(4),
          testStartJson(5, 'success 3', line: 8, column: 7),
          testDoneJson(5),
        ],
      ],
      doneJson(),
    );
  });

  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'));
    ''',
      [
        [
          suiteJson(0),
          testStartJson(1, 'loading test.dart', groupIDs: []),
          testDoneJson(1, hidden: true),
        ],
        [
          groupJson(2, testCount: 3),
          testStartJson(3, 'failure 1', line: 6, column: 7),
          errorJson(3, 'oh no', isFailure: true),
          testDoneJson(3, result: 'failure'),
          testStartJson(4, 'failure 2', line: 7, column: 7),
          errorJson(4, 'oh no', isFailure: true),
          testDoneJson(4, result: 'failure'),
          testStartJson(5, 'failure 3', line: 8, column: 7),
          errorJson(5, 'oh no', isFailure: true),
          testDoneJson(5, result: 'failure'),
        ],
      ],
      doneJson(success: false),
    );
  });

  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: 'json');
    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', () {});
    ''',
      [
        [
          suiteJson(0),
          testStartJson(1, 'loading test.dart', groupIDs: []),
          testDoneJson(1, hidden: true),
        ],
        [
          groupJson(2, testCount: 4),
          testStartJson(3, 'failure 1', line: 6, column: 7),
          errorJson(3, 'oh no', isFailure: true),
          testDoneJson(3, result: 'failure'),
          testStartJson(4, 'success 1', line: 7, column: 7),
          testDoneJson(4),
          testStartJson(5, 'failure 2', line: 8, column: 7),
          errorJson(5, 'oh no', isFailure: true),
          testDoneJson(5, result: 'failure'),
          testStartJson(6, 'success 2', line: 9, column: 7),
          testDoneJson(6),
        ],
      ],
      doneJson(success: false),
    );
  });

  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);
    ''',
      [
        [
          suiteJson(0),
          testStartJson(1, 'loading test.dart', groupIDs: []),
          testDoneJson(1, hidden: true),
        ],
        [
          groupJson(2, testCount: 2),
          testStartJson(3, 'failures', line: 9, column: 7),
          errorJson(3, 'first error'),
          errorJson(3, 'second error'),
          errorJson(3, 'third error'),
          testDoneJson(3, result: 'error'),
          testStartJson(4, 'wait', line: 15, column: 7),
          testDoneJson(4),
        ],
      ],
      doneJson(success: false),
    );
  });

  test('gracefully handles a test failing after completion', () {
    return _expectReport(
      '''
      // These completers ensure that the first test won't fail until the second
      // one is running, and that the test isolate isn't killed until all errors
      // have been thrown.
      var waitStarted = Completer();
      var testDone = Completer();
      test('failure', () {
        waitStarted.future.then((_) {
          Future.microtask(testDone.complete);
          throw 'oh no';
        });
      });
      test('wait', () {
        waitStarted.complete();
        return testDone.future;
      });
    ''',
      [
        [
          suiteJson(0),
          testStartJson(1, 'loading test.dart', groupIDs: []),
          testDoneJson(1, hidden: true),
        ],
        [
          groupJson(2, testCount: 2),
          testStartJson(3, 'failure', line: 11, column: 7),
          testDoneJson(3),
          testStartJson(4, 'wait', line: 17, column: 7),
          errorJson(3, 'oh no'),
          errorJson(
            3,
            'This test failed after it had already completed.\n'
            'Make sure to use a matching library which informs the '
            'test runner\nof pending async work.',
          ),
          testDoneJson(4),
        ],
      ],
      doneJson(success: false),
    );
  });

  test('reports each test in its proper groups', () {
    return _expectReport(
      '''
      group('group 1', () {
        group('.2', () {
          group('.3', () {
            test('success', () {});
          });
        });

        test('success1', () {});
        test('success2', () {});
      });
    ''',
      [
        [
          suiteJson(0),
          testStartJson(1, 'loading test.dart', groupIDs: []),
          testDoneJson(1, hidden: true),
        ],
        [
          groupJson(2, testCount: 3),
          groupJson(
            3,
            name: 'group 1',
            parentID: 2,
            testCount: 3,
            line: 6,
            column: 7,
          ),
          groupJson(4, name: 'group 1 .2', parentID: 3, line: 7, column: 9),
          groupJson(5, name: 'group 1 .2 .3', parentID: 4, line: 8, column: 11),
          testStartJson(
            6,
            'group 1 .2 .3 success',
            groupIDs: [2, 3, 4, 5],
            line: 9,
            column: 13,
          ),
          testDoneJson(6),
          testStartJson(
            7,
            'group 1 success1',
            groupIDs: [2, 3],
            line: 13,
            column: 9,
          ),
          testDoneJson(7),
          testStartJson(
            8,
            'group 1 success2',
            groupIDs: [2, 3],
            line: 14,
            column: 9,
          ),
          testDoneJson(8),
        ],
      ],
      doneJson(),
    );
  });

  group('print:', () {
    test('handles multiple prints', () {
      return _expectReport(
        '''
        test('test', () {
          print("one");
          print("two");
          print("three");
          print("four");
        });
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2),
            testStartJson(3, 'test', line: 6, column: 9),
            printJson(3, 'one'),
            printJson(3, 'two'),
            printJson(3, 'three'),
            printJson(3, 'four'),
            testDoneJson(3),
          ],
        ],
        doneJson(),
      );
    });

    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', () async {
          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;
        });
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 2),
            testStartJson(3, 'test', line: 10, column: 9),
            testDoneJson(3),
            testStartJson(4, 'wait', line: 20, column: 9),
            printJson(3, 'one'),
            printJson(3, 'two'),
            printJson(3, 'three'),
            printJson(3, 'four'),
            testDoneJson(4),
          ],
        ],
        doneJson(),
      );
    });

    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);
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 2),
            testStartJson(3, 'test', line: 9, column: 9),
            printJson(3, 'one'),
            printJson(3, 'two'),
            errorJson(3, 'first error'),
            printJson(3, 'three'),
            printJson(3, 'four'),
            errorJson(3, 'second error'),
            printJson(3, 'five'),
            printJson(3, 'six'),
            testDoneJson(3, result: 'error'),
            testStartJson(4, 'wait', line: 27, column: 9),
            testDoneJson(4),
          ],
        ],
        doneJson(success: false),
      );
    });
  });

  group('skip:', () {
    test('reports skipped tests', () {
      return _expectReport(
        '''
        test('skip 1', () {}, skip: true);
        test('skip 2', () {}, skip: true);
        test('skip 3', () {}, skip: true);
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 3),
            testStartJson(3, 'skip 1', skip: true, line: 6, column: 9),
            testDoneJson(3, skipped: true),
            testStartJson(4, 'skip 2', skip: true, line: 7, column: 9),
            testDoneJson(4, skipped: true),
            testStartJson(5, 'skip 3', skip: true, line: 8, column: 9),
            testDoneJson(5, skipped: true),
          ],
        ],
        doneJson(),
      );
    });

    test('reports skipped tests due to solo', () {
      const reason = 'does not have "solo"';
      return _expectReport(
        '''
        test('skip 1', () {});
        test('solo 2', () {}, solo: true);
        test('skip 3', () {});
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 3),
            testStartJson(3, 'skip 1', skip: reason, line: 6, column: 9),
            printJson(3, 'Skip: $reason', type: 'skip'),
            testDoneJson(3, skipped: true),
            testStartJson(4, 'solo 2', skip: false, line: 7, column: 9),
            testDoneJson(4, skipped: false),
            testStartJson(5, 'skip 3', skip: reason, line: 8, column: 9),
            printJson(5, 'Skip: $reason', type: 'skip'),
            testDoneJson(5, skipped: true),
          ],
        ],
        doneJson(),
      );
    });

    test('reports skipped groups', () {
      return _expectReport(
        '''
        group('skip', () {
          test('success 1', () {});
          test('success 2', () {});
          test('success 3', () {});
        }, skip: true);
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 3),
            groupJson(
              3,
              name: 'skip',
              parentID: 2,
              skip: true,
              testCount: 3,
              line: 6,
              column: 9,
            ),
            testStartJson(
              4,
              'skip success 1',
              groupIDs: [2, 3],
              skip: true,
              line: 7,
              column: 11,
            ),
            testDoneJson(4, skipped: true),
            testStartJson(
              5,
              'skip success 2',
              groupIDs: [2, 3],
              skip: true,
              line: 8,
              column: 11,
            ),
            testDoneJson(5, skipped: true),
            testStartJson(
              6,
              'skip success 3',
              groupIDs: [2, 3],
              skip: true,
              line: 9,
              column: 11,
            ),
            testDoneJson(6, skipped: true),
          ],
        ],
        doneJson(),
      );
    });

    test('reports the skip reason if available', () {
      return _expectReport(
        '''
        test('skip 1', () {}, skip: 'some reason');
        test('skip 2', () {}, skip: 'or another');
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 2),
            testStartJson(3, 'skip 1', skip: 'some reason', line: 6, column: 9),
            printJson(3, 'Skip: some reason', type: 'skip'),
            testDoneJson(3, skipped: true),
            testStartJson(4, 'skip 2', skip: 'or another', line: 7, column: 9),
            printJson(4, 'Skip: or another', type: 'skip'),
            testDoneJson(4, skipped: true),
          ],
        ],
        doneJson(),
      );
    });

    test('runs skipped tests with --run-skipped', () {
      return _expectReport(
        '''
        test('skip 1', () {}, skip: 'some reason');
        test('skip 2', () {}, skip: 'or another');
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 2),
            testStartJson(3, 'skip 1', line: 6, column: 9),
            testDoneJson(3),
            testStartJson(4, 'skip 2', line: 7, column: 9),
            testDoneJson(4),
          ],
        ],
        doneJson(),
        args: ['--run-skipped'],
      );
    });
  });

  group('reports line and column numbers for', () {
    test('the first call to setUpAll()', () {
      return _expectReport(
        '''
        setUpAll(() {});
        setUpAll(() {});
        setUpAll(() {});
        test('success', () {});
      ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 1),
            testStartJson(3, '(setUpAll)', line: 6, column: 9),
            testDoneJson(3, hidden: true),
            testStartJson(4, 'success', line: 9, column: 9),
            testDoneJson(4),
            testStartJson(5, '(tearDownAll)'),
            testDoneJson(5, hidden: true),
          ],
        ],
        doneJson(),
      );
    });

    test('the first call to tearDownAll()', () {
      return _expectReport(
        '''
        tearDownAll(() {});
        tearDownAll(() {});
        tearDownAll(() {});
        test('success', () {});
      ''',
        [
          [
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            suiteJson(0),
            groupJson(2, testCount: 1),
            testStartJson(3, 'success', line: 9, column: 9),
            testDoneJson(3),
            testStartJson(4, '(tearDownAll)', line: 6, column: 9),
            testDoneJson(4, hidden: true),
          ],
        ],
        doneJson(),
      );
    });

    test(
      'a test compiled to JS',
      () {
        return _expectReport(
          '''
        test('success', () {});
      ''',
          [
            [
              suiteJson(0, platform: 'chrome'),
              testStartJson(1, 'loading test.dart', groupIDs: []),
              printJson(
                1,
                isA<String>().having(
                  (s) => s.split('\n'),
                  'lines',
                  contains(startsWith('Compiled')),
                ),
              ),
              testDoneJson(1, hidden: true),
            ],
            [
              groupJson(2, testCount: 1),
              testStartJson(3, 'success', line: 6, column: 9),
              testDoneJson(3),
            ],
          ],
          doneJson(),
          args: ['-p', 'chrome'],
        );
      },
      tags: ['chrome'],
      skip: 'https://github.com/dart-lang/test/issues/872',
    );

    test('the root suite from a relative path', () {
      return _expectReport(
        '''
      customTest('success 1', () {});
      test('success 2', () {});
    ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 2),
            testStartJson(
              3,
              'success 1',
              line: 3,
              column: 60,
              url: p.toUri(p.join(d.sandbox, 'common.dart')).toString(),
              rootColumn: 7,
              rootLine: 7,
              rootUrl: p.toUri(p.join(d.sandbox, 'test.dart')).toString(),
            ),
            testDoneJson(3),
            testStartJson(4, 'success 2', line: 8, column: 7),
            testDoneJson(4),
          ],
        ],
        doneJson(),
        externalLibraries: {
          'common.dart': '''
import 'package:test/test.dart';

void customTest(String name, dynamic Function() testFn) => test(name, testFn);
''',
        },
      );
    });

    test('the root suite from an absolute path', () {
      final path = p.prettyUri(p.join(d.sandbox, 'test.dart'));
      return _expectReport(
        '''
      customTest('success 1', () {});
      test('success 2', () {});
    ''',
        useRelativePath: false,
        [
          [
            suiteJson(0, path: equalsIgnoringCase(path)),
            testStartJson(
              1,
              allOf(startsWith('loading '), endsWith('test.dart')),
              groupIDs: [],
            ),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 2),
            testStartJson(
              3,
              'success 1',
              line: 3,
              column: 60,
              url: p.toUri(p.join(d.sandbox, 'common.dart')).toString(),
              rootColumn: 7,
              rootLine: 7,
              rootUrl: p.toUri(p.join(d.sandbox, 'test.dart')).toString(),
            ),
            testDoneJson(3),
            testStartJson(4, 'success 2', line: 8, column: 7),
            testDoneJson(4),
          ],
        ],
        doneJson(),
        externalLibraries: {
          'common.dart': '''
import 'package:test/test.dart';

void customTest(String name, dynamic Function() testFn) => test(name, testFn);
''',
        },
      );
    });

    test('groups and tests with custom locations', () {
      return _expectReport(
        '''
      group('group 1 inferred', () {
        setUpAll(() {});
        test('test 1 inferred', () {});
        tearDownAll(() {});
      });
      group('group 2 custom', location: TestLocation(Uri.parse('file:///foo/group'), 123, 234), () {
        setUpAll(location: TestLocation(Uri.parse('file:///foo/setUpAll'), 345, 456), () {});
        test('test 2 custom', location: TestLocation(Uri.parse('file:///foo/test'), 567, 789), () {});
        tearDownAll(location: TestLocation(Uri.parse('file:///foo/tearDownAll'), 890, 901), () {});
      });
    ''',
        [
          [
            suiteJson(0),
            testStartJson(1, 'loading test.dart', groupIDs: []),
            testDoneJson(1, hidden: true),
          ],
          [
            groupJson(2, testCount: 2),
            groupJson(
              3,
              name: 'group 1 inferred',
              parentID: 2,
              line: 6,
              column: 7,
              testCount: 1,
            ),
            testStartJson(
              4,
              'group 1 inferred (setUpAll)',
              groupIDs: [2, 3],
              line: 7,
              column: 9,
            ),
            testDoneJson(4, hidden: true),
            testStartJson(
              5,
              'group 1 inferred test 1 inferred',
              groupIDs: [2, 3],
              line: 8,
              column: 9,
            ),
            testDoneJson(5),
            testStartJson(
              6,
              'group 1 inferred (tearDownAll)',
              groupIDs: [2, 3],
              line: 9,
              column: 9,
            ),
            testDoneJson(6, hidden: true),
            groupJson(
              7,
              name: 'group 2 custom',
              parentID: 2,
              url: 'file:///foo/group',
              line: 123,
              column: 234,
              testCount: 1,
            ),
            testStartJson(
              8,
              'group 2 custom (setUpAll)',
              url: 'file:///foo/setUpAll',
              groupIDs: [2, 7],
              line: 345,
              column: 456,
            ),
            testDoneJson(8, hidden: true),
            testStartJson(
              9,
              'group 2 custom test 2 custom',
              url: 'file:///foo/test',
              groupIDs: [2, 7],
              line: 567,
              column: 789,
            ),
            testDoneJson(9),
            testStartJson(
              10,
              'group 2 custom (tearDownAll)',
              url: 'file:///foo/tearDownAll',
              groupIDs: [2, 7],
              line: 890,
              column: 901,
            ),
            testDoneJson(10, hidden: true),
          ],
        ],
        doneJson(),
      );
    });
  });

  test("doesn't report line and column information for a test compiled to JS "
      'with --js-trace', () {
    return _expectReport(
      '''
      test('success', () {});
    ''',
      [
        [
          suiteJson(0, platform: 'chrome'),
          testStartJson(1, 'loading test.dart', groupIDs: []),
          printJson(
            1,
            isA<String>().having(
              (s) => s.split('\n'),
              'lines',
              contains(startsWith('Compiled')),
            ),
          ),
          testDoneJson(1, hidden: true),
        ],
        [
          groupJson(2, testCount: 1),
          testStartJson(3, 'success'),
          testDoneJson(3),
        ],
      ],
      doneJson(),
      args: ['-p', 'chrome', '--js-trace'],
    );
  }, tags: ['chrome']);
}

/// Asserts that the tests defined by [tests] produce the JSON events in
/// [expected].
///
/// If [externalLibraries] are provided it should be a map of relative file
/// paths to contents. All libraries will be added as imports to the test, and
/// files will be created for them.
Future<void> _expectReport(
  String tests,
  List<List<Object /*Map|Matcher*/>> expected,
  Map<Object, Object> done, {
  List<String> args = const [],
  bool useRelativePath = true,
  Map<String, String> externalLibraries = const {},
}) async {
  var testContent = StringBuffer('''
import 'dart:async';

import 'package:test/test.dart';

''');
  for (var entry in externalLibraries.entries) {
    testContent.writeln("import '${entry.key}';");
    await d.file(entry.key, entry.value).create();
  }
  testContent
    ..writeln('void main() {')
    ..writeln(tests)
    ..writeln('}');

  await d.file('test.dart', testContent.toString()).create();
  var testPath = useRelativePath ? 'test.dart' : p.join(d.sandbox, 'test.dart');

  var test = await runTest([
    testPath,
    '--chain-stack-traces',
    ...args,
  ], reporter: 'json');
  await test.shouldExit();

  var stdoutLines = await test.stdoutStream().toList();
  return expectJsonReport(stdoutLines, test.pid, expected, done);
}
