import 'dart:io';
import 'dart:isolate';

import 'package:jni/jni.dart';
import 'package:test/test.dart';

import 'test_util/test_util.dart';

void main() {
  if (!Platform.isAndroid) {
    checkDylibIsUpToDate();
    spawnJvm();
  }

  test('Debug mode captures stack trace on double release', () {
    Jni.captureStackTraceOnRelease = true;
    addTearDown(() {
      Jni.captureStackTraceOnRelease = false;
    });

    final s = 'hello'.toJString();
    s.release();

    try {
      s.release();
      // ignore: avoid_catching_errors
    } on DoubleReleaseError catch (e) {
      expect(e.toString(), contains('Object was released at:'));
      expect(e.toString(), contains('debug_release_test.dart'));
    }
  });

  test('Debug mode captures stack trace on use after release', () {
    Jni.captureStackTraceOnRelease = true;
    addTearDown(() {
      Jni.captureStackTraceOnRelease = false;
    });

    final s = 'hello'.toJString();
    s.release();

    try {
      s.toDartString();
      // ignore: avoid_catching_errors
    } on UseAfterReleaseError catch (e) {
      expect(e.toString(), contains('Object was released at:'));
      expect(e.toString(), contains('debug_release_test.dart'));
    }
  });

  test('Debug mode captures arena stack trace', () {
    Jni.captureStackTraceOnRelease = true;
    addTearDown(() {
      Jni.captureStackTraceOnRelease = false;
    });

    final s = 'hello'.toJString();

    s.release();
    using(s.releasedBy);
    using(s.releasedBy);

    try {
      s.toDartString();
      // ignore: avoid_catching_errors
    } on UseAfterReleaseError catch (e) {
      expect(
          e.toString(),
          stringContainsInOrder([
            'Object was released at:',
            'Object was registered to be released',
            'Object was registered to be released'
          ]));
      expect(e.toString(), contains('debug_release_test.dart'));
    }
    try {
      s.release();
      // ignore: avoid_catching_errors
    } on DoubleReleaseError catch (e) {
      expect(
          e.toString(),
          stringContainsInOrder([
            'Object was released at:',
            'Object was registered to be released',
            'Object was registered to be released'
          ]));
      expect(e.toString(), contains('debug_release_test.dart'));
    }
  });

  test('Hint is shown when debug mode is disabled', () {
    Jni.captureStackTraceOnRelease = false;

    final s = 'hello'.toJString();
    s.release();

    try {
      s.release();
      // ignore: avoid_catching_errors
    } on DoubleReleaseError catch (e) {
      expect(e.toString(),
          contains('set `Jni.captureStackTraceOnRelease = true`'));
    }
  });

  test('Debug mode flag is shared across isolates', () async {
    Jni.captureStackTraceOnRelease = true;
    addTearDown(() {
      Jni.captureStackTraceOnRelease = false;
    });

    final isEnabled = await Isolate.run(() {
      return Jni.captureStackTraceOnRelease;
    });

    expect(isEnabled, isTrue);

    Jni.captureStackTraceOnRelease = false;

    final isEnabled2 = await Isolate.run(() {
      return Jni.captureStackTraceOnRelease;
    });

    expect(isEnabled2, isFalse);
  });

  test('Debug mode captures stack trace in another isolate', () async {
    Jni.captureStackTraceOnRelease = true;
    addTearDown(() {
      Jni.captureStackTraceOnRelease = false;
    });

    await Isolate.run(() {
      final s = 'hello'.toJString();
      s.release();
      try {
        s.release();
        // ignore: avoid_catching_errors
      } on DoubleReleaseError catch (e) {
        if (!e.toString().contains('Object was released at:')) {
          throw StateError('Stack trace not captured in isolate');
        }
      }
    });
  });

  test(
      'Debug mode captures stack trace from another isolate'
      ' (cross-isolate release)', () async {
    Jni.captureStackTraceOnRelease = true;
    addTearDown(() {
      Jni.captureStackTraceOnRelease = false;
    });

    final s = 'hello'.toJString();

    s.release();

    final (isReleased, doubleReleaseError, useAfterReleaseError) =
        await Isolate.run(() {
      String? doubleReleaseError;
      String? useAfterReleaseError;
      try {
        s.release();
        // ignore: avoid_catching_errors
      } on DoubleReleaseError catch (e) {
        doubleReleaseError = e.toString();
      }

      try {
        s.toString();
        // ignore: avoid_catching_errors
      } on UseAfterReleaseError catch (e) {
        useAfterReleaseError = e.toString();
      }
      return (s.isReleased, doubleReleaseError, useAfterReleaseError);
    });

    expect(isReleased, isTrue);
    expect(doubleReleaseError, contains('Object was released at:'));
    expect(useAfterReleaseError, contains('Object was released at:'));
  });
}
