// 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 'package:mockito/mockito.dart';
import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;

import 'package:fuchsia_remote_debug_protocol/fuchsia_remote_debug_protocol.dart';

import 'common.dart';

void main() {
  group('FuchsiaRemoteConnection.connect', () {
    List<MockPortForwarder> forwardedPorts;
    List<MockPeer> mockPeerConnections;
    List<Uri> uriConnections;

    setUp(() {
      final List<Map<String, dynamic>> flutterViewCannedResponses =
          <Map<String, dynamic>>[
        <String, dynamic>{
          'views': <Map<String, dynamic>>[
            <String, dynamic>{
              'type': 'FlutterView',
              'id': 'flutterView0',
            },
          ],
        },
        <String, dynamic>{
          'views': <Map<String, dynamic>>[
            <String, dynamic>{
              'type': 'FlutterView',
              'id': 'flutterView1',
              'isolate': <String, dynamic>{
                'type': '@Isolate',
                'fixedId': 'true',
                'id': 'isolates/1',
                'name': 'file://flutterBinary1',
                'number': '1',
              },
            },
          ],
        },
        <String, dynamic>{
          'views': <Map<String, dynamic>>[
            <String, dynamic>{
              'type': 'FlutterView',
              'id': 'flutterView2',
              'isolate': <String, dynamic>{
                'type': '@Isolate',
                'fixedId': 'true',
                'id': 'isolates/2',
                'name': 'file://flutterBinary2',
                'number': '2',
              },
            },
          ],
        },
      ];

      forwardedPorts = <MockPortForwarder>[];
      mockPeerConnections = <MockPeer>[];
      uriConnections = <Uri>[];
      Future<json_rpc.Peer> mockVmConnectionFunction(
        Uri uri, {
        Duration timeout,
      }) {
        return Future<json_rpc.Peer>(() async {
          final MockPeer mp = MockPeer();
          mockPeerConnections.add(mp);
          uriConnections.add(uri);
          when(mp.sendRequest(any, any))
              // The local ports match the desired indices for now, so get the
              // canned response from the URI port.
              .thenAnswer((_) => Future<Map<String, dynamic>>(
                  () => flutterViewCannedResponses[uri.port]));
          return mp;
        });
      }

      fuchsiaVmServiceConnectionFunction = mockVmConnectionFunction;
    });

    tearDown(() {
      /// Most tests will mock out the port forwarding and connection
      /// functions.
      restoreFuchsiaPortForwardingFunction();
      restoreVmServiceConnectionFunction();
    });

    test('end-to-end with three vm connections and flutter view query', () async {
      int port = 0;
      Future<PortForwarder> mockPortForwardingFunction(
        String address,
        int remotePort, [
        String interface = '',
        String configFile,
      ]) {
        return Future<PortForwarder>(() {
          final MockPortForwarder pf = MockPortForwarder();
          forwardedPorts.add(pf);
          when(pf.port).thenReturn(port++);
          when(pf.remotePort).thenReturn(remotePort);
          return pf;
        });
      }

      fuchsiaPortForwardingFunction = mockPortForwardingFunction;
      final MockSshCommandRunner mockRunner = MockSshCommandRunner();
      // Adds some extra junk to make sure the strings will be cleaned up.
      when(mockRunner.run(argThat(startsWith('/bin/find')))).thenAnswer(
          (_) => Future<List<String>>.value(
              <String>['/hub/blah/blah/blah/vmservice-port\n']));
      when(mockRunner.run(argThat(startsWith('/bin/ls')))).thenAnswer(
          (_) => Future<List<String>>.value(
              <String>['123\n\n\n', '456  ', '789']));
      when(mockRunner.address).thenReturn('fe80::8eae:4cff:fef4:9247');
      when(mockRunner.interface).thenReturn('eno1');

      final FuchsiaRemoteConnection connection =
          await FuchsiaRemoteConnection.connectWithSshCommandRunner(mockRunner);

      // [mockPortForwardingFunction] will have returned three different
      // forwarded ports, incrementing the port each time by one. (Just a sanity
      // check that the forwarding port was called).
      expect(forwardedPorts.length, 3);
      expect(forwardedPorts[0].remotePort, 123);
      expect(forwardedPorts[1].remotePort, 456);
      expect(forwardedPorts[2].remotePort, 789);
      expect(forwardedPorts[0].port, 0);
      expect(forwardedPorts[1].port, 1);
      expect(forwardedPorts[2].port, 2);

      // VMs should be accessed via localhost ports given by
      // [mockPortForwardingFunction].
      expect(uriConnections[0],
        Uri(scheme:'ws', host:'[::1]', port:0, path:'/ws'));
      expect(uriConnections[1],
        Uri(scheme:'ws', host:'[::1]', port:1, path:'/ws'));
      expect(uriConnections[2],
        Uri(scheme:'ws', host:'[::1]', port:2, path:'/ws'));

      final List<FlutterView> views = await connection.getFlutterViews();
      expect(views, isNot(null));
      expect(views.length, 3);
      // Since name can be null, check for the ID on all of them.
      expect(views[0].id, 'flutterView0');
      expect(views[1].id, 'flutterView1');
      expect(views[2].id, 'flutterView2');

      expect(views[0].name, equals(null));
      expect(views[1].name, 'file://flutterBinary1');
      expect(views[2].name, 'file://flutterBinary2');

      // Ensure the ports are all closed after stop was called.
      await connection.stop();
      verify(forwardedPorts[0].stop());
      verify(forwardedPorts[1].stop());
      verify(forwardedPorts[2].stop());
    });

    test('end-to-end with three vms and remote open port', () async {
      int port = 0;
      Future<PortForwarder> mockPortForwardingFunction(
        String address,
        int remotePort, [
        String interface = '',
        String configFile,
      ]) {
        return Future<PortForwarder>(() {
          final MockPortForwarder pf = MockPortForwarder();
          forwardedPorts.add(pf);
          when(pf.port).thenReturn(port++);
          when(pf.remotePort).thenReturn(remotePort);
          when(pf.openPortAddress).thenReturn('fe80::1:2%eno2');
          return pf;
        });
      }

      fuchsiaPortForwardingFunction = mockPortForwardingFunction;
      final MockSshCommandRunner mockRunner = MockSshCommandRunner();
      // Adds some extra junk to make sure the strings will be cleaned up.
      when(mockRunner.run(argThat(startsWith('/bin/find')))).thenAnswer(
          (_) => Future<List<String>>.value(
              <String>['/hub/blah/blah/blah/vmservice-port\n']));
      when(mockRunner.run(argThat(startsWith('/bin/ls')))).thenAnswer(
          (_) => Future<List<String>>.value(
              <String>['123\n\n\n', '456  ', '789']));
      when(mockRunner.address).thenReturn('fe80::8eae:4cff:fef4:9247');
      when(mockRunner.interface).thenReturn('eno1');
      final FuchsiaRemoteConnection connection =
          await FuchsiaRemoteConnection.connectWithSshCommandRunner(mockRunner);

      // [mockPortForwardingFunction] will have returned three different
      // forwarded ports, incrementing the port each time by one. (Just a sanity
      // check that the forwarding port was called).
      expect(forwardedPorts.length, 3);
      expect(forwardedPorts[0].remotePort, 123);
      expect(forwardedPorts[1].remotePort, 456);
      expect(forwardedPorts[2].remotePort, 789);
      expect(forwardedPorts[0].port, 0);
      expect(forwardedPorts[1].port, 1);
      expect(forwardedPorts[2].port, 2);

      // VMs should be accessed via the alternate adddress given by
      // [mockPortForwardingFunction].
      expect(uriConnections[0],
        Uri(scheme:'ws', host:'[fe80::1:2%25eno2]', port:0, path:'/ws'));
      expect(uriConnections[1],
        Uri(scheme:'ws', host:'[fe80::1:2%25eno2]', port:1, path:'/ws'));
      expect(uriConnections[2],
        Uri(scheme:'ws', host:'[fe80::1:2%25eno2]', port:2, path:'/ws'));

      final List<FlutterView> views = await connection.getFlutterViews();
      expect(views, isNot(null));
      expect(views.length, 3);
      // Since name can be null, check for the ID on all of them.
      expect(views[0].id, 'flutterView0');
      expect(views[1].id, 'flutterView1');
      expect(views[2].id, 'flutterView2');

      expect(views[0].name, equals(null));
      expect(views[1].name, 'file://flutterBinary1');
      expect(views[2].name, 'file://flutterBinary2');

      // Ensure the ports are all closed after stop was called.
      await connection.stop();
      verify(forwardedPorts[0].stop());
      verify(forwardedPorts[1].stop());
      verify(forwardedPorts[2].stop());
    });

    test('end-to-end with three vms and ipv4', () async {
      int port = 0;
      Future<PortForwarder> mockPortForwardingFunction(
        String address,
        int remotePort, [
        String interface = '',
        String configFile,
      ]) {
        return Future<PortForwarder>(() {
          final MockPortForwarder pf = MockPortForwarder();
          forwardedPorts.add(pf);
          when(pf.port).thenReturn(port++);
          when(pf.remotePort).thenReturn(remotePort);
          return pf;
        });
      }

      fuchsiaPortForwardingFunction = mockPortForwardingFunction;
      final MockSshCommandRunner mockRunner = MockSshCommandRunner();
      // Adds some extra junk to make sure the strings will be cleaned up.
      when(mockRunner.run(argThat(startsWith('/bin/find')))).thenAnswer(
          (_) => Future<List<String>>.value(
              <String>['/hub/blah/blah/blah/vmservice-port\n']));
      when(mockRunner.run(argThat(startsWith('/bin/ls')))).thenAnswer(
          (_) => Future<List<String>>.value(
              <String>['123\n\n\n', '456  ', '789']));
      when(mockRunner.address).thenReturn('196.168.1.4');

      final FuchsiaRemoteConnection connection =
          await FuchsiaRemoteConnection.connectWithSshCommandRunner(mockRunner);

      // [mockPortForwardingFunction] will have returned three different
      // forwarded ports, incrementing the port each time by one. (Just a sanity
      // check that the forwarding port was called).
      expect(forwardedPorts.length, 3);
      expect(forwardedPorts[0].remotePort, 123);
      expect(forwardedPorts[1].remotePort, 456);
      expect(forwardedPorts[2].remotePort, 789);
      expect(forwardedPorts[0].port, 0);
      expect(forwardedPorts[1].port, 1);
      expect(forwardedPorts[2].port, 2);

      // VMs should be accessed via the ipv4 loopback.
      expect(uriConnections[0],
        Uri(scheme:'ws', host:'127.0.0.1', port:0, path:'/ws'));
      expect(uriConnections[1],
        Uri(scheme:'ws', host:'127.0.0.1', port:1, path:'/ws'));
      expect(uriConnections[2],
        Uri(scheme:'ws', host:'127.0.0.1', port:2, path:'/ws'));

      final List<FlutterView> views = await connection.getFlutterViews();
      expect(views, isNot(null));
      expect(views.length, 3);
      // Since name can be null, check for the ID on all of them.
      expect(views[0].id, 'flutterView0');
      expect(views[1].id, 'flutterView1');
      expect(views[2].id, 'flutterView2');

      expect(views[0].name, equals(null));
      expect(views[1].name, 'file://flutterBinary1');
      expect(views[2].name, 'file://flutterBinary2');

      // Ensure the ports are all closed after stop was called.
      await connection.stop();
      verify(forwardedPorts[0].stop());
      verify(forwardedPorts[1].stop());
      verify(forwardedPorts[2].stop());
    });

    test('env variable test without remote addr', () async {
      Future<void> failingFunction() async {
        await FuchsiaRemoteConnection.connect();
      }

      // Should fail as no env variable has been passed.
      expect(failingFunction,
          throwsA(isA<FuchsiaRemoteConnectionError>()));
    });
  });
}

class MockSshCommandRunner extends Mock implements SshCommandRunner {}

class MockPortForwarder extends Mock implements PortForwarder {}

class MockPeer extends Mock implements json_rpc.Peer {}