// Copyright 2013 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:io';

import 'package:multicast_dns/multicast_dns.dart';
import 'package:test/fake.dart';
import 'package:test/test.dart';

void main() {
  test('Can inject datagram socket factory and configure mdns port', () async {
    late int lastPort;
    final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
    final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
        (dynamic host, int port,
            {bool reuseAddress = true,
            bool reusePort = true,
            int ttl = 1}) async {
      lastPort = port;
      return datagramSocket;
    });

    await client.start(
        mDnsPort: 1234,
        interfacesFactory: (InternetAddressType type) async =>
            <NetworkInterface>[]);

    expect(lastPort, 1234);
  });

  test('Closes IPv4 sockets', () async {
    final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
    final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
        (dynamic host, int port,
            {bool reuseAddress = true,
            bool reusePort = true,
            int ttl = 1}) async {
      return datagramSocket;
    });

    await client.start(
        mDnsPort: 1234,
        interfacesFactory: (InternetAddressType type) async =>
            <NetworkInterface>[]);
    expect(datagramSocket.closed, false);
    client.stop();
    expect(datagramSocket.closed, true);
  });

  test('Closes IPv6 sockets', () async {
    final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
    datagramSocket.address = InternetAddress.anyIPv6;
    final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
        (dynamic host, int port,
            {bool reuseAddress = true,
            bool reusePort = true,
            int ttl = 1}) async {
      return datagramSocket;
    });

    await client.start(
        mDnsPort: 1234,
        interfacesFactory: (InternetAddressType type) async =>
            <NetworkInterface>[]);
    expect(datagramSocket.closed, false);
    client.stop();
    expect(datagramSocket.closed, true);
  });

  test('start() is idempotent', () async {
    final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();
    datagramSocket.address = InternetAddress.anyIPv4;
    final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
        (dynamic host, int port,
            {bool reuseAddress = true,
            bool reusePort = true,
            int ttl = 1}) async {
      return datagramSocket;
    });

    await client.start(
        interfacesFactory: (InternetAddressType type) async =>
            <NetworkInterface>[]);
    await client.start();
    await client.lookup(ResourceRecordQuery.serverPointer('_')).toList();
  });

  group('Bind a single socket to ANY IPv4 and more than one when IPv6', () {
    final List<Map<String, Object>> testCases = <Map<String, Object>>[
      <String, Object>{
        'name': 'IPv4',
        'datagramSocketType': InternetAddress.anyIPv4,
        'interfacePrefix': '192.168.2.'
      },
      <String, Object>{
        'name': 'IPv6',
        'datagramSocketType': InternetAddress.anyIPv6,
        'interfacePrefix': '2001:0db8:85a3:0000:0000:8a2e:7335:030'
      }
    ];

    for (final Map<String, Object> testCase in testCases) {
      test('Bind a single socket to ANY ${testCase["name"]}', () async {
        final FakeRawDatagramSocket datagramSocket = FakeRawDatagramSocket();

        datagramSocket.address =
            testCase['datagramSocketType']! as InternetAddress;

        final List<dynamic> selectedInterfacesForSendingPackets = <dynamic>[];
        final MDnsClient client = MDnsClient(rawDatagramSocketFactory:
            (dynamic host, int port,
                {bool reuseAddress = true,
                bool reusePort = true,
                int ttl = 1}) async {
          selectedInterfacesForSendingPackets.add(host);
          return datagramSocket;
        });

        const int numberOfFakeInterfaces = 10;
        Future<Iterable<NetworkInterface>> fakeNetworkInterfacesFactory(
            InternetAddressType type) async {
          final List<NetworkInterface> fakeInterfaces = <NetworkInterface>[];

          // Generate "fake" interfaces
          for (int i = 0; i < numberOfFakeInterfaces; i++) {
            fakeInterfaces.add(FakeNetworkInterface(
              'inetfake$i',
              <InternetAddress>[
                InternetAddress("${testCase['interfacePrefix']! as String}$i")
              ],
              0,
            ));
          }

          // ignore: always_specify_types
          return Future.value(fakeInterfaces);
        }

        final InternetAddress listenAddress =
            testCase['datagramSocketType']! as InternetAddress;

        await client.start(
            listenAddress: listenAddress,
            mDnsPort: 1234,
            interfacesFactory: fakeNetworkInterfacesFactory);
        client.stop();

        if (testCase['datagramSocketType'] == InternetAddress.anyIPv4) {
          expect(selectedInterfacesForSendingPackets.length, 1);
        } else {
          // + 1 because of unspecified address (::)
          expect(selectedInterfacesForSendingPackets.length,
              numberOfFakeInterfaces + 1);
        }
        expect(selectedInterfacesForSendingPackets[0], listenAddress.address);
      });
    }
  });

  test('Calls onError callback in case of socket error', () async {
    final FakeRawDatagramSocketThatSendsError datagramSocket =
        FakeRawDatagramSocketThatSendsError();
    final MDnsClient client = MDnsClient(
      rawDatagramSocketFactory: (
        dynamic host,
        int port, {
        bool reuseAddress = true,
        bool reusePort = true,
        int ttl = 1,
      }) async {
        return datagramSocket;
      },
    );

    final Completer<void> onErrorCalledCompleter = Completer<void>();
    await client.start(
      mDnsPort: 1234,
      interfacesFactory: (InternetAddressType type) async =>
          <NetworkInterface>[],
      onError: (Object e) {
        expect(e, 'Error');
        onErrorCalledCompleter.complete();
      },
    );

    await onErrorCalledCompleter.future.timeout(const Duration(seconds: 5));
  });
}

class FakeRawDatagramSocket extends Fake implements RawDatagramSocket {
  @override
  InternetAddress address = InternetAddress.anyIPv4;

  @override
  StreamSubscription<RawSocketEvent> listen(
      void Function(RawSocketEvent event)? onData,
      {Function? onError,
      void Function()? onDone,
      bool? cancelOnError}) {
    return const Stream<RawSocketEvent>.empty().listen(onData,
        onError: onError, cancelOnError: cancelOnError, onDone: onDone);
  }

  bool closed = false;

  @override
  void close() {
    closed = true;
  }

  @override
  int send(List<int> buffer, InternetAddress address, int port) {
    return buffer.length;
  }

  @override
  void joinMulticast(InternetAddress group, [NetworkInterface? interface]) {
    // nothing to do here
  }
  @override
  void setRawOption(RawSocketOption option) {
    // nothing to do here
  }
}

class FakeRawDatagramSocketThatSendsError extends Fake
    implements RawDatagramSocket {
  @override
  InternetAddress address = InternetAddress.anyIPv4;

  @override
  StreamSubscription<RawSocketEvent> listen(
    void Function(RawSocketEvent event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) {
    return Stream<RawSocketEvent>.error('Error').listen(
      onData,
      onError: onError,
      cancelOnError: cancelOnError,
      onDone: onDone,
    );
  }
}

class FakeNetworkInterface implements NetworkInterface {
  FakeNetworkInterface(this._name, this._addresses, this._index);

  final String _name;
  final List<InternetAddress> _addresses;
  final int _index;

  @override
  List<InternetAddress> get addresses => _addresses;

  @override
  String get name => _name;

  @override
  int get index => _index;
}