// Copyright 2014 The Flutter Authors. 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:io';
import 'dart:isolate';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:platform/platform.dart';

final Matcher throwsRemoteError = throwsA(isA<RemoteError>());

int test1(int value) {
  return value + 1;
}

int test2(int value) {
  throw 2;
}

int test3(int value) {
  Isolate.exit();
}

int test4(int value) {
  Isolate.current.kill();

  return value + 1;
}

int test5(int value) {
  Isolate.current.kill(priority: Isolate.immediate);

  return value + 1;
}

Future<int> test1Async(int value) async {
  return value + 1;
}

Future<int> test2Async(int value) async {
  throw 2;
}

Future<int> test3Async(int value) async {
  Isolate.exit();
}

Future<int> test4Async(int value) async {
  Isolate.current.kill();

  return value + 1;
}

Future<int> test5Async(int value) async {
  Isolate.current.kill(priority: Isolate.immediate);

  return value + 1;
}

Future<int> test1CallCompute(int value) {
  return compute(test1, value);
}

Future<int> test2CallCompute(int value) {
  return compute(test2, value);
}

Future<int> test3CallCompute(int value) {
  return compute(test3, value);
}

Future<int> test4CallCompute(int value) {
  return compute(test4, value);
}

Future<int> test5CallCompute(int value) {
  return compute(test5, value);
}

Future<void> expectFileSuccessfullyCompletes(String filename,
    [bool unsound = false]) async {
  // Run a Dart script that calls compute().
  // The Dart process will terminate only if the script exits cleanly with
  // all isolate ports closed.
  const FileSystem fs = LocalFileSystem();
  const Platform platform = LocalPlatform();
  final String flutterRoot = platform.environment['FLUTTER_ROOT']!;
  final String dartPath =
      fs.path.join(flutterRoot, 'bin', 'cache', 'dart-sdk', 'bin', 'dart');
  final String packageRoot = fs.path.dirname(fs.path.fromUri(platform.script));
  final String scriptPath =
      fs.path.join(packageRoot, 'test', 'foundation', filename);
  final String nullSafetyArg =
      unsound ? '--no-sound-null-safety' : '--sound-null-safety';

  // Enable asserts to also catch potentially invalid assertions.
  final ProcessResult result = await Process.run(
      dartPath, <String>[nullSafetyArg, 'run', '--enable-asserts', scriptPath]);
  expect(result.exitCode, 0);
}

class ComputeTestSubject {
  ComputeTestSubject(this.base, [this.additional]);

  final int base;
  final dynamic additional;

  int method(int x) {
    return base * x;
  }

  static int staticMethod(int square) {
    return square * square;
  }
}

Future<int> computeStaticMethod(int square) {
  return compute(ComputeTestSubject.staticMethod, square);
}

Future<int> computeClosure(int square) {
  return compute((_) => square * square, null);
}

Future<int> computeInvalidClosure(int square) {
  final ReceivePort r = ReceivePort();

  return compute((_) {
    r.sendPort.send('Computing!');

    return square * square;
  }, null);
}

Future<int> computeInstanceMethod(int square) {
  final ComputeTestSubject subject = ComputeTestSubject(square);
  return compute(subject.method, square);
}

Future<int> computeInvalidInstanceMethod(int square) {
  final ComputeTestSubject subject = ComputeTestSubject(square, ReceivePort());
  return compute(subject.method, square);
}

dynamic testInvalidResponse(int square) {
  final ReceivePort r = ReceivePort();
  try {
    return r;
  } finally {
    r.close();
  }
}

dynamic testInvalidError(int square) {
  final ReceivePort r = ReceivePort();
  try {
    throw r;
  } finally {
    r.close();
  }
}

String? testDebugName(_) {
  return Isolate.current.debugName;
}

int? testReturnNull(_) {
  return null;
}

void main() {
  test('compute()', () async {
    expect(await compute(test1, 0), 1);
    expect(compute(test2, 0), throwsA(2));
    expect(compute(test3, 0), throwsRemoteError);
    expect(await compute(test4, 0), 1);
    expect(compute(test5, 0), throwsRemoteError);

    expect(await compute(test1Async, 0), 1);
    expect(compute(test2Async, 0), throwsA(2));
    expect(compute(test3Async, 0), throwsRemoteError);
    expect(await compute(test4Async, 0), 1);
    expect(compute(test5Async, 0), throwsRemoteError);

    expect(await compute(test1CallCompute, 0), 1);
    expect(compute(test2CallCompute, 0), throwsA(2));
    expect(compute(test3CallCompute, 0), throwsRemoteError);
    expect(await compute(test4CallCompute, 0), 1);
    expect(compute(test5CallCompute, 0), throwsRemoteError);

    expect(compute(testInvalidResponse, 0), throwsRemoteError);
    expect(compute(testInvalidError, 0), throwsRemoteError);

    expect(await computeStaticMethod(10), 100);
    expect(await computeClosure(10), 100);
    expect(computeInvalidClosure(10), throwsArgumentError);
    expect(await computeInstanceMethod(10), 100);
    expect(computeInvalidInstanceMethod(10), throwsArgumentError);

    expect(await compute(testDebugName, null, debugLabel: 'debug_name'),
        'debug_name');
    expect(await compute(testReturnNull, null), null);
  }, skip: kIsWeb); // [intended] isn't supported on the web.

  group('compute() closes all ports', () {
    test('with valid message', () async {
      await expectFileSuccessfullyCompletes('_compute_caller.dart');
    });
    test('with invalid message', () async {
      await expectFileSuccessfullyCompletes(
          '_compute_caller_invalid_message.dart');
    });
    test('with valid error', () async {
      await expectFileSuccessfullyCompletes('_compute_caller.dart');
    });
    test('with invalid error', () async {
      await expectFileSuccessfullyCompletes(
          '_compute_caller_invalid_message.dart');
    });
  }, skip: kIsWeb); // [intended] isn't supported on the web.
}
