// 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:async';
import 'dart:convert';

/// A callback to use with [integrationDriver].
///
/// The callback receives the name of screenshot passed to `binding.takeScreenshot(<name>)`,
/// a PNG byte buffer representing the screenshot, and an optional `Map` of arguments.
///
/// The callback returns `true` if the test passes or `false` otherwise.
///
/// You can use this callback to store the bytes locally in a file or upload them to a service
/// that compares the image against a gold or baseline version.
///
/// The optional `Map` of arguments can be passed from the
/// `binding.takeScreenshot(<name>, <args>)` callsite in the integration test,
/// and then the arguments can be used in the `onScreenshot` handler that is defined by
/// the Flutter driver. This `Map` should only contain values that can be serialized
/// to JSON.
///
/// Since the function is executed on the host driving the test, you can access any environment
/// variable from it.
typedef ScreenshotCallback = Future<bool> Function(String name, List<int> image, [Map<String, Object?>? args]);

/// Classes shared between `integration_test.dart` and `flutter drive` based
/// adoptor (ex: `integration_test_driver.dart`).

/// An object sent from integration_test back to the Flutter Driver in response to
/// `request_data` command.
class Response {
  /// Constructor to use for positive response.
  Response.allTestsPassed({this.data})
      : _allTestsPassed = true,
        _failureDetails = null;

  /// Constructor for failure response.
  Response.someTestsFailed(this._failureDetails, {this.data})
      : _allTestsPassed = false;

  /// Constructor for failure response.
  Response.toolException({String? ex})
      : _allTestsPassed = false,
        _failureDetails = <Failure>[Failure('ToolException', ex)];

  /// Constructor for web driver commands response.
  Response.webDriverCommand({this.data})
      : _allTestsPassed = false,
        _failureDetails = null;

  final List<Failure>? _failureDetails;

  final bool _allTestsPassed;

  /// The extra information to be added along side the test result.
  Map<String, dynamic>? data;

  /// Whether the test ran successfully or not.
  bool get allTestsPassed => _allTestsPassed;

  /// If the result are failures get the formatted details.
  String get formattedFailureDetails =>
      _allTestsPassed ? '' : formatFailures(_failureDetails!);

  /// Failure details as a list.
  List<Failure>? get failureDetails => _failureDetails;

  /// Serializes this message to a JSON map.
  String toJson() => json.encode(<String, dynamic>{
        'result': allTestsPassed.toString(),
        'failureDetails': _failureDetailsAsString(),
        if (data != null) 'data': data,
      });

  /// Deserializes the result from JSON.
  static Response fromJson(String source) {
    final Map<String, dynamic> responseJson = json.decode(source) as Map<String, dynamic>;
    if ((responseJson['result'] as String?) == 'true') {
      return Response.allTestsPassed(data: responseJson['data'] as Map<String, dynamic>?);
    } else {
      return Response.someTestsFailed(
        _failureDetailsFromJson(responseJson['failureDetails'] as List<dynamic>),
        data: responseJson['data'] as Map<String, dynamic>?,
      );
    }
  }

  /// Method for formatting the test failures' details.
  String formatFailures(List<Failure> failureDetails) {
    if (failureDetails.isEmpty) {
      return '';
    }

    final StringBuffer sb = StringBuffer();
    int failureCount = 1;
    for (final Failure failure in failureDetails) {
      sb.writeln('Failure in method: ${failure.methodName}');
      sb.writeln(failure.details);
      sb.writeln('end of failure $failureCount\n\n');
      failureCount++;
    }
    return sb.toString();
  }

  /// Create a list of Strings from [_failureDetails].
  List<String> _failureDetailsAsString() {
    final List<String> list = <String>[];
    if (_failureDetails == null || _failureDetails!.isEmpty) {
      return list;
    }

    for (final Failure failure in _failureDetails!) {
      list.add(failure.toJson());
    }

    return list;
  }

  /// Creates a [Failure] list using a json response.
  static List<Failure> _failureDetailsFromJson(List<dynamic> list) {
    return list.map((dynamic s) {
      return Failure.fromJsonString(s as String);
    }).toList();
  }
}

/// Representing a failure includes the method name and the failure details.
class Failure {
  /// Constructor requiring all fields during initialization.
  Failure(this.methodName, this.details);

  /// The name of the test method which failed.
  final String methodName;

  /// The details of the failure such as stack trace.
  final String? details;

  /// Serializes the object to JSON.
  String toJson() {
    return json.encode(<String, String?>{
      'methodName': methodName,
      'details': details,
    });
  }

  @override
  String toString() => toJson();

  /// Decode a JSON string to create a Failure object.
  static Failure fromJsonString(String jsonString) {
    final Map<String, dynamic> failure = json.decode(jsonString) as Map<String, dynamic>;
    return Failure(failure['methodName'] as String, failure['details'] as String?);
  }
}

/// Message used to communicate between app side tests and driver tests.
///
/// Not all `integration_tests` use this message. They are only used when app
/// side tests are sending [WebDriverCommand]s to the driver side.
///
/// These messages are used for the handshake since they carry information on
/// the driver side test such as: status pending or tests failed.
class DriverTestMessage {
  /// When tests are failed on the driver side.
  DriverTestMessage.error()
      : _isSuccess = false,
        _isPending = false;

  /// When driver side is waiting on [WebDriverCommand]s to be sent from the
  /// app side.
  DriverTestMessage.pending()
      : _isSuccess = false,
        _isPending = true;

  /// When driver side successfully completed executing the [WebDriverCommand].
  DriverTestMessage.complete()
      : _isSuccess = true,
        _isPending = false;

  final bool _isSuccess;
  final bool _isPending;

  // /// Status of this message.
  // ///
  // /// The status will be use to notify `integration_test` of driver side's
  // /// state.
  // String get status => _status;

  /// Has the command completed successfully by the driver.
  bool get isSuccess => _isSuccess;

  /// Is the driver waiting for a command.
  bool get isPending => _isPending;

  /// Depending on the values of [isPending] and [isSuccess], returns a string
  /// to represent the [DriverTestMessage].
  ///
  /// Used as an alternative method to converting the object to json since
  /// [RequestData] is only accepting string as `message`.
  @override
  String toString() {
    if (isPending) {
      return 'pending';
    } else if (isSuccess) {
      return 'complete';
    } else {
      return 'error';
    }
  }

  /// Return a DriverTestMessage depending on `status`.
  static DriverTestMessage fromString(String status) {
    switch (status) {
      case 'error':
        return DriverTestMessage.error();
      case 'pending':
        return DriverTestMessage.pending();
      case 'complete':
        return DriverTestMessage.complete();
      default:
        throw StateError('This type of status does not exist: $status');
    }
  }
}

/// Types of different WebDriver commands that can be used in web integration
/// tests.
///
/// These commands are either commands that WebDriver can execute or used
/// for the communication between `integration_test` and the driver test.
enum WebDriverCommandType {
  /// Acknowledgement for the previously sent message.
  ack,

  /// No further WebDriver commands is requested by the app-side tests.
  noop,

  /// Asking WebDriver to take a screenshot of the Web page.
  screenshot,
}

/// Command for WebDriver to execute.
///
/// Only works on Web when tests are run via `flutter driver` command.
///
/// See: https://www.w3.org/TR/webdriver/
class WebDriverCommand {
  /// Constructor for [WebDriverCommandType.noop] command.
  WebDriverCommand.noop()
      : type = WebDriverCommandType.noop,
        values = <String, dynamic>{};

  /// Constructor for [WebDriverCommandType.noop] screenshot.
  WebDriverCommand.screenshot(String screenshotName, [Map<String, Object?>? args])
      : type = WebDriverCommandType.screenshot,
        values = <String, dynamic>{
          'screenshot_name': screenshotName,
          if (args != null) 'args': args,
        };

  /// Type of the [WebDriverCommand].
  ///
  /// Currently the only command that triggers a WebDriver API is `screenshot`.
  ///
  /// There are also `ack` and `noop` commands defined to manage the handshake
  /// during the communication.
  final WebDriverCommandType type;

  /// Used for adding extra values to the commands such as file name for
  /// `screenshot`.
  final Map<String, dynamic> values;

  /// Util method for converting [WebDriverCommandType] to a map entry.
  ///
  /// Used for converting messages to json format.
  static Map<String, dynamic> typeToMap(WebDriverCommandType type) => <String, dynamic>{
    'web_driver_command': '$type',
  };
}

/// Template methods each class that responses the driver side inputs must
/// implement.
///
/// Depending on the platform the communication between `integration_tests` and
/// the `driver_tests` can be different.
///
/// For the web implementation [WebCallbackManager].
/// For the io implementation [IOCallbackManager].
abstract class CallbackManager {
  /// The callback function to response the driver side input.
  Future<Map<String, dynamic>> callback(
      Map<String, String> params, IntegrationTestResults testRunner);

  /// Takes a screenshot of the application.
  /// Returns the data that is sent back to the host.
   Future<Map<String, dynamic>> takeScreenshot(String screenshot, [Map<String, Object?>? args]);

  /// Android only. Converts the Flutter surface to an image view.
  Future<void> convertFlutterSurfaceToImage();

  /// Cleanup and completers or locks used during the communication.
  void cleanup();
}

/// Interface that surfaces test results of integration tests.
///
/// Implemented by [IntegrationTestWidgetsFlutterBinding]s.
///
/// Any class which needs to access the test results but do not want to create
/// a cyclic dependency [IntegrationTestWidgetsFlutterBinding]s can use this
/// interface. Example [CallbackManager].
abstract class IntegrationTestResults {
  /// Stores failure details.
  ///
  /// Failed test method's names used as key.
  List<Failure> get failureMethodsDetails;

  /// The extra data for the reported result.
  Map<String, dynamic>? get reportData;

  /// Whether all the test methods completed successfully.
  ///
  /// Completes when the tests have finished. The boolean value will be true if
  /// all tests have passed, and false otherwise.
  Completer<bool> get allTestsPassed;
}