// 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.

import 'dart:async';
import 'dart:isolate';

import 'package:stack_trace/stack_trace.dart';

import '../frontend/expect.dart';

/// An exception that was thrown remotely.
///
/// This could be an exception thrown in a different isolate, a different
/// process, or on an entirely different computer.
class RemoteException implements Exception {
  /// The original exception's message, if it had one.
  ///
  /// If the original exception was a plain string, this will contain that
  /// string.
  final String message;

  /// The value of the original exception's `runtimeType.toString()`.
  final String type;

  /// The value of the original exception's `toString()`.
  final String _toString;

  /// Serializes [error] and [stackTrace] into a JSON-safe object.
  ///
  /// Other than JSON- and isolate-safety, no guarantees are made about the
  /// serialized format.
  static serialize(error, StackTrace stackTrace) {
    String message;
    if (error is String) {
      message = error;
    } else {
      try {
        message = error.message.toString();
      } on NoSuchMethodError catch (_) {
        // Do nothing.
      }
    }

    // It's possible (although unlikely) for a user-defined class to have
    // multiple of these supertypes. That's fine, though, since we only care
    // about core-library-raised IsolateSpawnExceptions anyway.
    String supertype;
    if (error is TestFailure) {
      supertype = 'TestFailure';
    } else if (error is IsolateSpawnException) {
      supertype = 'IsolateSpawnException';
    }

    return {
      'message': message,
      'type': error.runtimeType.toString(),
      'supertype': supertype,
      'toString': error.toString(),
      'stackChain': Chain.forTrace(stackTrace).toString()
    };
  }

  /// Deserializes an exception serialized with [RemoteException.serialize].
  ///
  /// The returned [AsyncError] is guaranteed to have a [RemoteException] as its
  /// error and a [Chain] as its stack trace.
  static AsyncError deserialize(serialized) {
    return AsyncError(_deserializeException(serialized),
        Chain.parse(serialized['stackChain'] as String));
  }

  /// Deserializes the exception portion of [serialized].
  static RemoteException _deserializeException(serialized) {
    String message = serialized['message'];
    String type = serialized['type'];
    String toString = serialized['toString'];

    switch (serialized['supertype'] as String) {
      case 'TestFailure':
        return _RemoteTestFailure(message, type, toString);
      case 'IsolateSpawnException':
        return _RemoteIsolateSpawnException(message, type, toString);
      default:
        return RemoteException._(message, type, toString);
    }
  }

  RemoteException._(this.message, this.type, this._toString);

  String toString() => _toString;
}

/// A subclass of [RemoteException] that implements [TestFailure].
///
/// It's important to preserve [TestFailure]-ness, because tests have different
/// results depending on whether an exception was a failure or an error.
class _RemoteTestFailure extends RemoteException implements TestFailure {
  _RemoteTestFailure(String message, String type, String toString)
      : super._(message, type, toString);
}

/// A subclass of [RemoteException] that implements [IsolateSpawnException].
class _RemoteIsolateSpawnException extends RemoteException
    implements IsolateSpawnException {
  _RemoteIsolateSpawnException(String message, String type, String toString)
      : super._(message, type, toString);
}
