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

@Tags(['load_test'])
library;

import 'dart:ffi';
import 'dart:io';
import 'dart:math';

import 'package:ffi/ffi.dart';
import 'package:jni/jni.dart';
import 'package:jni/src/third_party/generated_bindings.dart';
import 'package:test/test.dart';

import 'test_util/test_util.dart';

/// Taken from
/// https://github.com/dart-lang/ffigen/blob/master/test/native_objc_test/automated_ref_count_test.dart
final executeInternalCommand = DynamicLibrary.process().lookupFunction<
    Void Function(Pointer<Char>, Pointer<Void>),
    void Function(Pointer<Char>, Pointer<Void>)>('Dart_ExecuteInternalCommand');

void doGC() {
  final gcNow = 'gc-now'.toNativeUtf8();
  executeInternalCommand(gcNow.cast(), nullptr);
  calloc.free(gcNow);
}

void main() {
  if (!Platform.isAndroid) {
    checkDylibIsUpToDate();
    spawnJvm();
  }
  run(testRunner: test);
}

const k4 = 4 * 1024;
const k256 = 256 * 1024;

const secureRandomSeedBound = 4294967296;

final random = Random.secure();

final randomClass = JClass.forName('java/util/Random');
JObject newRandom() => randomClass
    .constructorId('(J)V')
    .call(randomClass, [random.nextInt(secureRandomSeedBound)]);

void run({required TestRunnerCallback testRunner}) {
  testRunner('Test 4K refs can be created in a row', () {
    final list = <JObject>[];
    for (var i = 0; i < k4; i++) {
      list.add(newRandom());
    }
    for (final jobject in list) {
      jobject.release();
    }
  });

  testRunner('Create and release 256K references in a loop using arena', () {
    for (var i = 0; i < k256; i++) {
      using((arena) {
        final random = newRandom()..releasedBy(arena);
        // The actual expect here does not matter. I am just being paranoid
        // against assigning to `_` because compiler may optimize it. (It has
        // side effect of calling FFI but still.)
        expect(random.reference.pointer, isNot(nullptr));
      });
    }
  });

  testRunner('Create and release 256K references in a loop (explicit release)',
      () {
    for (var i = 0; i < k256; i++) {
      final random = newRandom();
      expect(random.reference.pointer, isNot(nullptr));
      random.release();
    }
  });

  testRunner('Create and release 64K references, in batches of 256', () {
    for (var i = 0; i < 64 * 4; i++) {
      using((arena) {
        for (var i = 0; i < 256; i++) {
          final r = newRandom()..releasedBy(arena);
          expect(r.reference.pointer, isNot(nullptr));
        }
      });
    }
  });

  // We don't have a direct way to check if something creates JNI references.
  // So we are checking if we can run this for large number of times.
  testRunner('Verify a call returning primitive can be run any times', () {
    final random = newRandom();
    final nextInt = randomClass.instanceMethodId('nextInt', '()I');
    for (var i = 0; i < k256; i++) {
      final rInt = nextInt(random, jint.type, []);
      expect(rInt, isA<int>());
    }
  });

  void testFinalizer() {
    testRunner('Finalizer correctly removes the references', () {
      // We are checking if we can run this for large number of times.
      // More than the number of available global references.
      for (var i = 0; i < k256; ++i) {
        final random = newRandom();
        expect(random.reference.pointer, isNot(nullptr));
        if (i % k4 == 0) {
          doGC();
        }
      }
    });
  }

  void testRefValidityAfterGC(int delayInSeconds) {
    testRunner('Validate reference after GC & ${delayInSeconds}s sleep', () {
      final random = newRandom();
      final nextInt = randomClass.instanceMethodId('nextInt', '()I');
      doGC();
      sleep(Duration(seconds: delayInSeconds));
      expect(
        nextInt(random, jint.type, []),
        isA<int>(),
      );
      expect(
        Jni.env.GetObjectRefType(random.reference.pointer),
        equals(JObjectRefType.JNIGlobalRefType),
      );
    });
  }

  // Dart_ExecuteInternalCommand doesn't exist in Android.
  if (!Platform.isAndroid) {
    testFinalizer();
    testRefValidityAfterGC(1);
    testRefValidityAfterGC(10);
  }
}
