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

// ignore_for_file: inference_failure_on_instance_creation

import 'dart:async';
import 'dart:convert';

import 'package:json_rpc_2/error_code.dart' as error_code;
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart';

void main() {
  late StreamSink incoming;
  late Stream outgoing;
  late json_rpc.Peer peer;

  setUp(() {
    var incomingController = StreamController();
    incoming = incomingController.sink;
    var outgoingController = StreamController();
    outgoing = outgoingController.stream;
    peer = json_rpc.Peer.withoutJson(
        StreamChannel(incomingController.stream, outgoingController));
  });

  group('like a client,', () {
    test('can send a message and receive a response', () {
      expect(outgoing.first.then((request) {
        expect(
            request,
            equals({
              'jsonrpc': '2.0',
              'method': 'foo',
              'params': {'bar': 'baz'},
              'id': 0
            }));
        incoming.add({'jsonrpc': '2.0', 'result': 'qux', 'id': 0});
      }), completes);

      peer.listen();
      expect(
          peer.sendRequest('foo', {'bar': 'baz'}), completion(equals('qux')));
    });

    test('can send a batch of messages and receive a batch of responses', () {
      expect(outgoing.first.then((request) {
        expect(
            request,
            equals([
              {
                'jsonrpc': '2.0',
                'method': 'foo',
                'params': {'bar': 'baz'},
                'id': 0
              },
              {
                'jsonrpc': '2.0',
                'method': 'a',
                'params': {'b': 'c'},
                'id': 1
              },
              {
                'jsonrpc': '2.0',
                'method': 'w',
                'params': {'x': 'y'},
                'id': 2
              }
            ]));

        incoming.add([
          {'jsonrpc': '2.0', 'result': 'qux', 'id': 0},
          {'jsonrpc': '2.0', 'result': 'd', 'id': 1},
          {'jsonrpc': '2.0', 'result': 'z', 'id': 2}
        ]);
      }), completes);

      peer.listen();

      peer.withBatch(() {
        expect(
            peer.sendRequest('foo', {'bar': 'baz'}), completion(equals('qux')));
        expect(peer.sendRequest('a', {'b': 'c'}), completion(equals('d')));
        expect(peer.sendRequest('w', {'x': 'y'}), completion(equals('z')));
      });
    });

    test('requests terminates when the channel is closed', () async {
      var incomingController = StreamController<void>();
      var channel = StreamChannel.withGuarantees(
        incomingController.stream,
        StreamController<void>(),
      );
      var peer = json_rpc.Peer.withoutJson(channel);
      unawaited(peer.listen());

      var response = peer.sendRequest('foo');
      await incomingController.close();

      expect(response, throwsStateError);
    });
  });

  test('can be closed', () async {
    var incomingController = StreamController();
    var channel = StreamChannel.withGuarantees(
      incomingController.stream,
      StreamController(),
    );
    var peer = json_rpc.Peer.withoutJson(channel);
    unawaited(peer.listen());
    await peer.close();
  });

  test('considered closed with misbehaving StreamChannel', () async {
    // If a StreamChannel does not enforce the guarantees stated in it's
    // contract - specifically that "Closing the sink causes the stream to close
    // before it emits any more events." - The `Peer` should still understand
    // when it has been closed manually.
    var channel = StreamChannel(
      StreamController().stream,
      StreamController(),
    );
    var peer = json_rpc.Peer.withoutJson(channel);
    unawaited(peer.listen());
    unawaited(peer.close());
    expect(peer.isClosed, true);
  });

  group('like a server,', () {
    test('can receive a call and return a response', () {
      expect(outgoing.first,
          completion(equals({'jsonrpc': '2.0', 'result': 'qux', 'id': 0})));

      peer.registerMethod('foo', (_) => 'qux');
      peer.listen();

      incoming.add({
        'jsonrpc': '2.0',
        'method': 'foo',
        'params': {'bar': 'baz'},
        'id': 0
      });
    });

    test('can receive a batch of calls and return a batch of responses', () {
      expect(
          outgoing.first,
          completion(equals([
            {'jsonrpc': '2.0', 'result': 'qux', 'id': 0},
            {'jsonrpc': '2.0', 'result': 'd', 'id': 1},
            {'jsonrpc': '2.0', 'result': 'z', 'id': 2}
          ])));

      peer.registerMethod('foo', (_) => 'qux');
      peer.registerMethod('a', (_) => 'd');
      peer.registerMethod('w', (_) => 'z');
      peer.listen();

      incoming.add([
        {
          'jsonrpc': '2.0',
          'method': 'foo',
          'params': {'bar': 'baz'},
          'id': 0
        },
        {
          'jsonrpc': '2.0',
          'method': 'a',
          'params': {'b': 'c'},
          'id': 1
        },
        {
          'jsonrpc': '2.0',
          'method': 'w',
          'params': {'x': 'y'},
          'id': 2
        }
      ]);
    });

    test('returns a response for malformed JSON', () {
      var incomingController = StreamController<String>();
      var outgoingController = StreamController<String>();
      var jsonPeer = json_rpc.Peer(
          StreamChannel(incomingController.stream, outgoingController));

      expect(
          outgoingController.stream.first.then(jsonDecode),
          completion({
            'jsonrpc': '2.0',
            'error': {
              'code': error_code.PARSE_ERROR,
              'message': startsWith('Invalid JSON: '),
              // TODO(nweiz): Always expect the source when sdk#25655 is fixed.
              'data': {
                'request': anyOf([isNull, '{invalid'])
              }
            },
            'id': null
          }));

      jsonPeer.listen();

      incomingController.add('{invalid');
    });

    test('returns a response for incorrectly-structured JSON', () {
      expect(
          outgoing.first,
          completion({
            'jsonrpc': '2.0',
            'error': {
              'code': error_code.INVALID_REQUEST,
              'message': 'Request must contain a "jsonrpc" key.',
              'data': {
                'request': {'completely': 'wrong'}
              }
            },
            'id': null
          }));

      peer.listen();

      incoming.add({'completely': 'wrong'});
    });
  });

  test('can notify on unhandled errors for if the method throws', () async {
    var exception = Exception('test exception');
    var incomingController = StreamController();
    var outgoingController = StreamController();
    final completer = Completer<Exception>();
    peer = json_rpc.Peer.withoutJson(
      StreamChannel(incomingController.stream, outgoingController),
      onUnhandledError: (error, stack) {
        completer.complete(error);
      },
    );
    peer
      ..registerMethod('foo', () => throw exception)
      // ignore: unawaited_futures
      ..listen();

    incomingController.add({'jsonrpc': '2.0', 'method': 'foo'});
    var receivedException = await completer.future;
    expect(receivedException, equals(exception));
  });
}
