// Copyright (c) 2023, 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 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';

import 'common/service_test_common.dart';
import 'common/test_helper.dart';

const successServiceName = 'successService';
const errorServiceName = 'errorService';
const serviceAlias = 'serviceAlias';
const paramKey = 'pkey';
const paramValue = 'pvalue';
const resultKey = 'rkey';
const resultValue = 'rvalue';
const errorCode = 5000;
const errorKey = 'ekey';
const errorValue = 'evalue';
const repetition = 5;

Future<void> testSuccessService(
  VmService primaryClient,
  VmService secondaryClient,
) async {
  final successServiceRequests =
      <(Map<String, dynamic>, Completer<Map<String, dynamic>>)>[];
  final allRequestsReceivedCompleter = Completer<void>();

  final successServiceMethod = await registerServiceHelper(
    primaryClient,
    secondaryClient,
    successServiceName,
    (params) async {
      final completer = Completer<Map<String, dynamic>>();
      successServiceRequests.add((params, completer));
      if (successServiceRequests.length == repetition) {
        allRequestsReceivedCompleter.complete();
      }
      return await completer.future;
    },
  );

  // Testing parallel invocation of service which succeeds
  final results = <Future<Response>>[
    for (int i = 0; i < repetition; ++i)
      primaryClient.callServiceExtension(
        successServiceMethod,
        args: {
          paramKey + i.toString(): paramValue + i.toString(),
        },
      ),
  ];

  // Wait for all of the requests to be received before processing.
  await allRequestsReceivedCompleter.future;

  final completions = <Function>[];
  for (final request in successServiceRequests) {
    final (params, responseCompleter) = request;
    final iteration = successServiceRequests.indexOf(request);
    final end = iteration.toString();

    // check requests while they arrive
    expect(params[paramKey + end], paramValue + end);
    // answer later
    completions.add(
      () => responseCompleter.complete({
        'result': {
          resultKey + end: resultValue + end,
        },
      }),
    );
  }

  // Shuffle and respond out of order.
  completions.shuffle();
  for (final c in completions) {
    c();
  }

  final responses = await Future.wait(results);
  for (int i = 0; i < responses.length; ++i) {
    final response = responses[i];
    expect(response, isNotNull);
    expect(
      response.json![resultKey + i.toString()],
      resultValue + i.toString(),
    );
  }
}

Future<void> testErrorService(
  VmService primaryClient,
  VmService secondaryClient,
) async {
  final serviceRequests =
      <(Map<String, dynamic>, Completer<Map<String, dynamic>>)>[];
  final allRequestsReceivedCompleter = Completer<void>();

  final errorServiceMethod = await registerServiceHelper(
    primaryClient,
    secondaryClient,
    errorServiceName,
    (params) async {
      final completer = Completer<Map<String, dynamic>>();
      serviceRequests.add((params, completer));
      if (serviceRequests.length == repetition) {
        allRequestsReceivedCompleter.complete();
      }
      return await completer.future;
    },
  );

  // Testing parallel invocation of service which returns an error
  final results = <Future<Response>>[
    for (int i = 0; i < repetition; ++i)
      primaryClient.callServiceExtension(
        errorServiceMethod,
        args: {
          paramKey + i.toString(): paramValue + i.toString(),
        },
      )
        // We ignore these futures so that when they complete with an error
        // without being awaited or do not have an error handler registered
        // they won't cause an unhandled exception.
        ..ignore(),
  ];

  // Wait for all of the requests to be received before processing.
  await allRequestsReceivedCompleter.future;

  final completions = <Function>[];
  for (final request in serviceRequests) {
    final (params, responseCompleter) = request;
    final iteration = serviceRequests.indexOf(request);
    final end = iteration.toString();

    // check requests while they arrive
    expect(params[paramKey + end], paramValue + end);
    // answer later
    completions.add(
      () => responseCompleter.complete(
        {
          'error': {
            'code': errorCode + iteration,
            'data': {errorKey + end: errorValue + end},
            'message': 'error message',
          },
        },
      ),
    );
  }

  // Shuffle and respond out of order.
  completions.shuffle();
  for (final c in completions) {
    c();
  }

  for (int i = 0; i < results.length; ++i) {
    final response = results[i];
    try {
      await response;
      fail('Response should be an error');
    } on RPCError catch (e) {
      expect(
        e.data![errorKey + i.toString()],
        errorValue + i.toString(),
      );
    }
  }
}

final tests = <IsolateTest>[
  (VmService primaryClient, IsolateRef isolateRef) async {
    final secondaryClient = await vmServiceConnectUri(primaryClient.wsUri!);
    await testSuccessService(primaryClient, secondaryClient);
    await testErrorService(primaryClient, secondaryClient);
  },
];

void main([args = const <String>[]]) => runIsolateTests(
      args,
      tests,
      'external_service_asynchronous_invocation_test.dart',
    );
