// Copyright (c) 2017, 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')
@Tags(['node'])
library;

import 'dart:convert';
import 'dart:io';

import 'package:test/src/runner/executable_settings.dart';
import 'package:test/test.dart';
import 'package:test_core/src/util/io.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

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

final _success = '''
  import 'package:test/test.dart';

  void main() {
    test("success", () {});
  }
''';

final _failure = '''
  import 'package:test/test.dart';

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

({int major, String full})? _nodeVersion;

({int major, String full}) _readNodeVersion() {
  final process = Process.runSync(
    ExecutableSettings(
      linuxExecutable: 'node',
      macOSExecutable: 'node',
      windowsExecutable: 'node.exe',
    ).executable,
    ['--version'],
    stdoutEncoding: utf8,
  );
  if (process.exitCode != 0) {
    throw const OSError('Could not run node --version');
  }

  final version = RegExp(r'v(\d+)\..*');
  final parsed = version.firstMatch(process.stdout as String)!;
  return (major: int.parse(parsed.group(1)!), full: process.stdout);
}

String? skipBelowMajorNodeVersion(int minimumMajorVersion) {
  final (:major, :full) = _nodeVersion ??= _readNodeVersion();
  if (major < minimumMajorVersion) {
    return 'This test requires Node $minimumMajorVersion.x or later, '
        'but is running on $full';
  }

  return null;
}

String? skipAboveMajorNodeVersion(int maximumMajorVersion) {
  final (:major, :full) = _nodeVersion ??= _readNodeVersion();
  if (major > maximumMajorVersion) {
    return 'This test requires Node $maximumMajorVersion.x or older, '
        'but is running on $full';
  }

  return null;
}

void main() {
  setUpAll(precompileTestExecutable);

  group('fails gracefully if', () {
    test('a test file fails to compile', () async {
      await d.file('test.dart', 'void main() {invalid Dart}').create();
      var test = await runTest(['-p', 'node', 'test.dart']);

      expect(
        test.stdout,
        containsInOrder([
          'Error: Compilation failed.',
          '-1: loading test.dart [E]',
          'Failed to load "test.dart": dart2js failed.',
        ]),
      );
      await test.shouldExit(1);
    });

    test('a test file throws', () async {
      await d.file('test.dart', "void main() => throw 'oh no';").create();

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(
        test.stdout,
        containsInOrder([
          '-1: loading test.dart [E]',
          'Failed to load "test.dart": oh no',
        ]),
      );
      await test.shouldExit(1);
    });

    test(
      "a test file doesn't have a main defined",
      () async {
        await d.file('test.dart', 'void foo() {}').create();

        var test = await runTest(['-p', 'node', 'test.dart']);
        expect(
          test.stdout,
          containsInOrder([
            '-1: loading test.dart [E]',
            'Failed to load "test.dart": No top-level main() function defined.',
          ]),
        );
        await test.shouldExit(1);
      },
      skip: 'https://github.com/dart-lang/test/issues/894',
    );

    test(
      'a test file has a non-function main',
      () async {
        await d.file('test.dart', 'int main;').create();

        var test = await runTest(['-p', 'node', 'test.dart']);
        expect(
          test.stdout,
          containsInOrder([
            '-1: loading test.dart [E]',
            'Failed to load "test.dart": Top-level main getter is not a function.',
          ]),
        );
        await test.shouldExit(1);
      },
      skip: 'https://github.com/dart-lang/test/issues/894',
    );

    test('a test file has a main with arguments', () async {
      await d.file('test.dart', 'void main(arg) {}').create();

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(
        test.stdout,
        containsInOrder([
          '-1: loading test.dart [E]',
          'Failed to load "test.dart": Top-level main() function takes arguments.',
        ]),
      );
      await test.shouldExit(1);
    });
  });

  group('runs successful tests', () {
    test('on Node and the VM', () async {
      await d.file('test.dart', _success).create();
      var test = await runTest(['-p', 'node', '-p', 'vm', 'test.dart']);

      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
      await test.shouldExit(0);
    });

    // Regression test; this broke in 0.12.0-beta.9.
    test('on a file in a subdirectory', () async {
      await d.dir('dir', [d.file('test.dart', _success)]).create();

      var test = await runTest(['-p', 'node', 'dir/test.dart']);
      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
      await test.shouldExit(0);
    });

    test('compiled with dart2wasm', () async {
      await d.file('test.dart', _success).create();
      var test = await runTest([
        '-p',
        'node',
        '--compiler',
        'dart2wasm',
        'test.dart',
      ]);

      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
      await test.shouldExit(0);
    }, skip: skipBelowMajorNodeVersion(22));
  });

  test('defines a node environment constant', () async {
    await d.file('test.dart', '''
        import 'package:test/test.dart';

        void main() {
          test("test", () {
            expect(const bool.fromEnvironment("node"), isTrue);
          });
        }
      ''').create();

    var test = await runTest(['-p', 'node', 'test.dart']);
    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
    await test.shouldExit(0);
  });

  test('runs failing tests that fail only on node', () async {
    await d.file('test.dart', '''
        import 'package:path/path.dart' as p;
        import 'package:test/test.dart';

        void main() {
          test("test", () {
            if (const bool.fromEnvironment("node")) {
              throw TestFailure("oh no");
            }
          });
        }
      ''').create();

    var test = await runTest([
      '-p',
      'node',
      '-p',
      'vm',
      '-c',
      'dart2js',
      'test.dart',
    ]);
    expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.')));
    await test.shouldExit(1);
  });

  test(
    'runs failing tests that fail only on node (with dart2wasm)',
    () async {
      await d.file('test.dart', '''
        import 'package:path/path.dart' as p;
        import 'package:test/test.dart';

        void main() {
          test("test", () {
            if (const bool.fromEnvironment("node")) {
              throw TestFailure("oh no");
            }
          });
        }
      ''').create();

      var test = await runTest([
        '-p',
        'node',
        '-p',
        'vm',
        '-c',
        'dart2js',
        '-c',
        'dart2wasm',
        'test.dart',
      ]);
      expect(test.stdout, emitsThrough(contains('+1 -2: Some tests failed.')));
      await test.shouldExit(1);
    },
    skip: skipBelowMajorNodeVersion(22),
  );

  test(
    'gracefully handles wasm errors on old node versions',
    () async {
      // Old Node.JS versions can't read the WebAssembly modules emitted by
      // dart2wasm. The node process exits before connecting to the server
      // opened by the test runner, leading to timeouts. So, this is a
      // regression test for https://github.com/dart-lang/test/pull/2259#issuecomment-2307868442
      await d.file('test.dart', '''
        import 'package:test/test.dart';

        void main() {
          test("test", () {
            // Should pass on newer node versions
          });
        }
      ''').create();

      var test = await runTest(['-p', 'node', '-c', 'dart2wasm', 'test.dart']);
      expect(
        test.stdout,
        emitsInOrder([
          emitsThrough(
            contains('Node exited before connecting to the test channel.'),
          ),
          emitsThrough(contains('-1: Some tests failed.')),
        ]),
      );
      await test.shouldExit(1);
    },
    skip: skipAboveMajorNodeVersion(21),
  );

  test('forwards prints from the Node test', () async {
    await d.file('test.dart', '''
      import 'dart:async';

      import 'package:test/test.dart';

      void main() {
        test("test", () {
          print("Hello,");
          return Future(() => print("world!"));
        });
      }
    ''').create();

    var test = await runTest(['-p', 'node', 'test.dart']);
    expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!']));
    await test.shouldExit(0);
  });

  test('forwards raw JS prints from the Node test', () async {
    await d.file('test.dart', '''
      import 'dart:async';
      import 'dart:js_interop';
      
      import 'package:test/test.dart';
      
      @JS('console.log')
      external void log(JSString value);
      
      void main() {
        test('test', () {
          log('Hello,'.toJS);
          return Future(() => log('world!'.toJS));
        });
      }
    ''').create();

    var test = await runTest(['-p', 'node', 'test.dart']);
    expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!']));
    await test.shouldExit(0);
  });

  test('dartifies stack traces for JS-compiled tests by default', () async {
    await d.file('test.dart', _failure).create();

    var test = await runTest(['-p', 'node', '--verbose-trace', 'test.dart']);
    expect(
      test.stdout,
      containsInOrder([' main.<fn>', 'package:test', 'dart:async/zone.dart']),
      skip: 'https://github.com/dart-lang/sdk/issues/41949',
    );
    await test.shouldExit(1);
  });

  test(
    "doesn't dartify stack traces for JS-compiled tests with --js-trace",
    () async {
      await d.file('test.dart', _failure).create();

      var test = await runTest([
        '-p',
        'node',
        '--verbose-trace',
        '--js-trace',
        'test.dart',
      ]);
      expect(test.stdoutStream(), neverEmits(endsWith(' main.<fn>')));
      expect(test.stdoutStream(), neverEmits(contains('package:test')));
      expect(test.stdoutStream(), neverEmits(contains('dart:async/zone.dart')));
      expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
      await test.shouldExit(1);
    },
  );

  test('supports node_modules in the package directory', () async {
    await d.dir('node_modules', [
      d.dir('my_module', [d.file('index.js', 'module.exports.value = 12;')]),
    ]).create();

    await d.file('test.dart', '''
      import 'dart:js_interop';
      
      import 'package:test/test.dart';
      
      @JS()
      external MyModule require(String name);
      
      @JS()
      extension type MyModule(JSObject _) implements JSObject {
        external int get value;
      }
      
      void main() {
        test('can load from a module', () {
          expect(require('my_module').value, equals(12));
        });
      }
    ''').create();

    var test = await runTest(['-p', 'node', 'test.dart']);
    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
    await test.shouldExit(0);
  });

  group('with onPlatform', () {
    test('respects matching Skips', () async {
      await d.file('test.dart', '''
        import 'dart:async';

        import 'package:test/test.dart';

        void main() {
          test("fail", () => throw 'oh no', onPlatform: {"node": Skip()});
        }
      ''').create();

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
      await test.shouldExit(0);
    });

    test('ignores non-matching Skips', () async {
      await d.file('test.dart', '''
        import 'dart:async';

        import 'package:test/test.dart';

        void main() {
          test("success", () {}, onPlatform: {"browser": Skip()});
        }
      ''').create();

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
      await test.shouldExit(0);
    });

    test('matches the current OS', () async {
      await d.file('test.dart', '''
        import 'dart:async';

        import 'package:test/test.dart';

        void main() {
          test("fail", () => throw 'oh no',
              onPlatform: {"${currentOS.identifier}": Skip()});
        }
      ''').create();

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
      await test.shouldExit(0);
    });

    test("doesn't match a different OS", () async {
      await d.file('test.dart', '''
        import 'dart:async';

        import 'package:test/test.dart';

        void main() {
          test("success", () {}, onPlatform: {"$otherOS": Skip()});
        }
      ''').create();

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
      await test.shouldExit(0);
    });
  });

  group('with an @OnPlatform annotation', () {
    test('respects matching Skips', () async {
      await d.file('test.dart', '''
        @OnPlatform(const {"js": const Skip()})

        import 'dart:async';

        import 'package:test/test.dart';

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

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(test.stdout, emitsThrough(contains('~1: All tests skipped.')));
      await test.shouldExit(0);
    });

    test('ignores non-matching Skips', () async {
      await d.file('test.dart', '''
        @OnPlatform(const {"vm": const Skip()})

        import 'dart:async';

        import 'package:test/test.dart';

        void main() {
          test("success", () {});
        }
      ''').create();

      var test = await runTest(['-p', 'node', 'test.dart']);
      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
      await test.shouldExit(0);
    });
  });
}
