// 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:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/mdns_discovery.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:multicast_dns/multicast_dns.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';

import '../src/common.dart';
import '../src/fakes.dart';

void main() {
  group('mDNS Discovery', () {
    final int future = DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch;

    setUp(() {
      setNetworkInterfaceLister(
        ({
          bool? includeLoopback,
          bool? includeLinkLocal,
          InternetAddressType? type,
        }) async => <NetworkInterface>[],
      );
    });

    tearDown(() {
      resetNetworkInterfaceLister();
    });

    group('for attach', () {
      late MDnsClient emptyClient;

      setUp(() {
        emptyClient = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
      });

      testWithoutContext('Find result in preliminary client', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: emptyClient,
          preliminaryMDnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
        expect(result, isNotNull);
      });

      testWithoutContext('Do not find result in preliminary client, but find in main client', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
        expect(result, isNotNull);
      });

      testWithoutContext('Find multiple in preliminary client', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
            PtrResourceRecord('baz', future, domainName: 'fiz'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
            'fiz': <SrvResourceRecord>[
              SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: emptyClient,
          preliminaryMDnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        expect(portDiscovery.queryForAttach, throwsToolExit());
      });

      testWithoutContext('Find duplicates in preliminary client', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: emptyClient,
          preliminaryMDnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
        expect(result, isNotNull);
      });

      testWithoutContext('Find similar named in preliminary client', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
            PtrResourceRecord('foo', future, domainName: 'bar (2)'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
            'bar (2)': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: emptyClient,
          preliminaryMDnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        expect(portDiscovery.queryForAttach, throwsToolExit());
      });

      testWithoutContext('No ports available', () async {
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: emptyClient,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        final int? port = (await portDiscovery.queryForAttach())?.port;
        expect(port, isNull);
      });

      testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async {
        final BufferLogger logger = BufferLogger.test();
        final MemoryFileSystem fs = MemoryFileSystem.test();
        final FakeAnalytics fakeAnalytics = getInitializedFakeAnalyticsInstance(
          fs: fs,
          fakeFlutterVersion: FakeFlutterVersion(),
        );
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: emptyClient,
          preliminaryMDnsClient: emptyClient,
          logger: logger,
          flutterUsage: TestUsage(),
          analytics: fakeAnalytics,
        );
        final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
          '',
          FakeIOSDevice(),
        );
        expect(uri, isNull);
        expect(logger.errorText, contains('Personal Hotspot'));
        expect(fakeAnalytics.sentEvents, contains(
          Event.appleUsageEvent(
              workflow: 'ios-mdns',
              parameter: 'no-ipv4-link-local',
            )
        ));
      });

      testWithoutContext('One port available, no appId', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final int? port = (await portDiscovery.queryForAttach())?.port;
        expect(port, 123);
      });

      testWithoutContext('One port available, no appId, with authCode', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
          txtResponse: <String, List<TxtResourceRecord>>{
            'bar': <TxtResourceRecord>[
              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForAttach();
        expect(result?.port, 123);
        expect(result?.authCode, 'xyz/');
      });

      testWithoutContext('Multiple ports available, with appId', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
            PtrResourceRecord('baz', future, domainName: 'fiz'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
            'fiz': <SrvResourceRecord>[
              SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final int? port = (await portDiscovery.queryForAttach(applicationId: 'fiz'))?.port;
        expect(port, 321);
      });

      testWithoutContext('Multiple ports available per process, with appId', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
            PtrResourceRecord('baz', future, domainName: 'fiz'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
            'fiz': <SrvResourceRecord>[
              SrvResourceRecord('fiz', future, port: 4321, weight: 1, priority: 1, target: 'local'),
              SrvResourceRecord('fiz', future, port: 321, weight: 1, priority: 1, target: 'local'),
            ],
          },
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final int? port = (await portDiscovery.queryForAttach(applicationId: 'bar'))?.port;
        expect(port, 1234);
      });

      testWithoutContext('Throws Exception when client throws OSError on start', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[], <String, List<SrvResourceRecord>>{},
          osErrorOnStart: true,
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        expect(
          () async => portDiscovery.queryForAttach(),
          throwsException,
        );
      });

      testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
        );

        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final Uri? uri = await portDiscovery.getVMServiceUriForAttach('bar', device, hostVmservicePort: 0);
        expect(uri.toString(), 'http://127.0.0.1:123/');
      });

      testWithoutContext('Get wireless device IP (iPv4)', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
            ],
          },
          ipResponse: <String, List<IPAddressResourceRecord>>{
            'appId': <IPAddressResourceRecord>[
              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!),
            ],
          },
          txtResponse: <String, List<TxtResourceRecord>>{
            'bar': <TxtResourceRecord>[
              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
            ],
          },
        );

        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
          'bar',
          device,
          useDeviceIPAsHost: true,
        );
        expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
      });

      testWithoutContext('Get wireless device IP (iPv6)', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
            ],
          },
          ipResponse: <String, List<IPAddressResourceRecord>>{
            'appId': <IPAddressResourceRecord>[
              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!),
            ],
          },
          txtResponse: <String, List<TxtResourceRecord>>{
            'bar': <TxtResourceRecord>[
              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
            ],
          },
        );

        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final Uri? uri = await portDiscovery.getVMServiceUriForAttach(
          'bar',
          device,
          useDeviceIPAsHost: true,
        );
        expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
      });

      testWithoutContext('Throw error if unable to find VM service with app id and device port', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
          ],
          <String, List<SrvResourceRecord>>{
            'srv-foo': <SrvResourceRecord>[
              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
            ],
            'srv-bar': <SrvResourceRecord>[
              SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
            ],
            'srv-baz': <SrvResourceRecord>[
              SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
            ],
          },
        );
        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        expect(
          portDiscovery.getVMServiceUriForAttach(
            'srv-bar',
            device,
            deviceVmservicePort: 321,
          ),
          throwsToolExit(
            message: 'Did not find a Dart VM Service advertised for srv-bar on port 321.'
          ),
        );
      });

      testWithoutContext('Throw error if unable to find VM Service with app id', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
          ],
          <String, List<SrvResourceRecord>>{
            'srv-foo': <SrvResourceRecord>[
              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
            ],
          },
        );
        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          preliminaryMDnsClient: emptyClient,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        expect(
          portDiscovery.getVMServiceUriForAttach(
            'srv-asdf',
            device,
          ),
          throwsToolExit(
            message: 'Did not find a Dart VM Service advertised for srv-asdf.'
          ),
        );
      });
    });

    group('for launch', () {
      testWithoutContext('Ensure either port or device name are provided', () async {
        final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        expect(() async => portDiscovery.queryForLaunch(applicationId: 'app-id'), throwsAssertionError);
      });

      testWithoutContext('No ports available', () async {
        final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        final MDnsVmServiceDiscoveryResult? result = await portDiscovery.queryForLaunch(
          applicationId: 'app-id',
          deviceVmservicePort: 123,
        );

        expect(result, null);
      });

      testWithoutContext('Prints helpful message when there is no ipv4 link local address.', () async {
        final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
        final BufferLogger logger = BufferLogger.test();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: logger,
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
          '',
          FakeIOSDevice(),
          deviceVmservicePort: 0,
        );
        expect(uri, isNull);
        expect(logger.errorText, contains('Personal Hotspot'));
      });

      testWithoutContext('Throws Exception when client throws OSError on start', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[], <String, List<SrvResourceRecord>>{},
          osErrorOnStart: true,
        );

        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        expect(
          () async => portDiscovery.queryForLaunch(applicationId: 'app-id', deviceVmservicePort: 123),
          throwsException,
        );
      });

      testWithoutContext('Correctly builds VM Service URI with hostVmservicePort == 0', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 123, weight: 1, priority: 1, target: 'appId'),
            ],
          },
        );

        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
          'bar',
          device,
          hostVmservicePort: 0,
          deviceVmservicePort: 123,
        );
        expect(uri.toString(), 'http://127.0.0.1:123/');
      });

      testWithoutContext('Get wireless device IP (iPv4)', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
            ],
          },
          ipResponse: <String, List<IPAddressResourceRecord>>{
            'appId': <IPAddressResourceRecord>[
              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('111.111.111.111')!),
            ],
          },
          txtResponse: <String, List<TxtResourceRecord>>{
            'bar': <TxtResourceRecord>[
              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
            ],
          },
        );

        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
          'bar',
          device,
          useDeviceIPAsHost: true,
          deviceVmservicePort: 1234,
        );
        expect(uri.toString(), 'http://111.111.111.111:1234/xyz/');
      });

      testWithoutContext('Get wireless device IP (iPv6)', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'bar'),
          ],
          <String, List<SrvResourceRecord>>{
            'bar': <SrvResourceRecord>[
              SrvResourceRecord('bar', future, port: 1234, weight: 1, priority: 1, target: 'appId'),
            ],
          },
          ipResponse: <String, List<IPAddressResourceRecord>>{
            'appId': <IPAddressResourceRecord>[
              IPAddressResourceRecord('Device IP', 0, address: InternetAddress.tryParse('1111:1111:1111:1111:1111:1111:1111:1111')!),
            ],
          },
          txtResponse: <String, List<TxtResourceRecord>>{
            'bar': <TxtResourceRecord>[
              TxtResourceRecord('bar', future, text: 'authCode=xyz\n'),
            ],
          },
        );

        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
          'bar',
          device,
          useDeviceIPAsHost: true,
          deviceVmservicePort: 1234,
        );
        expect(uri.toString(), 'http://[1111:1111:1111:1111:1111:1111:1111:1111]:1234/xyz/');
      });

      testWithoutContext('Throw error if unable to find VM Service with app id and device port', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
          ],
          <String, List<SrvResourceRecord>>{
            'srv-foo': <SrvResourceRecord>[
              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
            ],
            'srv-bar': <SrvResourceRecord>[
              SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
            ],
            'srv-baz': <SrvResourceRecord>[
              SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
            ],
          },
        );
        final FakeIOSDevice device = FakeIOSDevice();
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        expect(
          portDiscovery.getVMServiceUriForLaunch(
            'srv-bar',
            device,
            deviceVmservicePort: 321,
          ),
          throwsToolExit(
              message:'Did not find a Dart VM Service advertised for srv-bar on port 321.'),
        );
      });

      testWithoutContext('Matches on application id and device name', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
          ],
          <String, List<SrvResourceRecord>>{
            'srv-bar': <SrvResourceRecord>[
              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'My-Phone.local'),
            ],
          },
        );
        final FakeIOSDevice device = FakeIOSDevice(
          name: 'My Phone',
        );
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
          'srv-bar',
          device,
        );
        expect(uri.toString(), 'http://127.0.0.1:123/');
      });

      testWithoutContext('Throw error if unable to find VM Service with app id and device name', () async {
        final MDnsClient client = FakeMDnsClient(
          <PtrResourceRecord>[
            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
          ],
          <String, List<SrvResourceRecord>>{
            'srv-foo': <SrvResourceRecord>[
              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
            ],
          },
        );
        final FakeIOSDevice device = FakeIOSDevice(
          name: 'My Phone',
        );
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: client,
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        expect(
          portDiscovery.getVMServiceUriForLaunch(
            'srv-bar',
            device,
          ),
          throwsToolExit(
              message:'Did not find a Dart VM Service advertised for srv-bar'),
        );
      });
    });

    group('deviceNameMatchesTargetName', () {
      testWithoutContext('compares case insensitive and without spaces, hypthens, .local', () {
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: FakeMDnsClient(
            <PtrResourceRecord>[],
            <String, List<SrvResourceRecord>>{},
          ),
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );

        expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone.local'), isTrue);
      });

      testWithoutContext('includes numbers in comparison', () {
        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
          mdnsClient: FakeMDnsClient(
            <PtrResourceRecord>[],
            <String, List<SrvResourceRecord>>{},
          ),
          logger: BufferLogger.test(),
          flutterUsage: TestUsage(),
          analytics: NoOpAnalytics(),
        );
        expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone-2.local'), isFalse);
      });
    });

    testWithoutContext('Find firstMatchingVmService with many available and no application id', () async {
      final MDnsClient client = FakeMDnsClient(
        <PtrResourceRecord>[
          PtrResourceRecord('foo', future, domainName: 'srv-foo'),
          PtrResourceRecord('bar', future, domainName: 'srv-bar'),
          PtrResourceRecord('baz', future, domainName: 'srv-boo'),
        ],
        <String, List<SrvResourceRecord>>{
          'srv-foo': <SrvResourceRecord>[
            SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
          ],
          'srv-bar': <SrvResourceRecord>[
            SrvResourceRecord('srv-bar', future, port: 123, weight: 1, priority: 1, target: 'target-bar'),
          ],
          'srv-baz': <SrvResourceRecord>[
            SrvResourceRecord('srv-baz', future, port: 123, weight: 1, priority: 1, target: 'target-baz'),
          ],
        },
      );

      final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
        mdnsClient: client,
        logger: BufferLogger.test(),
        flutterUsage: TestUsage(),
        analytics: NoOpAnalytics(),
      );
      final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(client);
      expect(result?.domainName, 'srv-foo');
    });

    testWithoutContext('Find firstMatchingVmService app id', () async {
      final MDnsClient client = FakeMDnsClient(
        <PtrResourceRecord>[
          PtrResourceRecord('foo', future, domainName: 'srv-foo'),
          PtrResourceRecord('bar', future, domainName: 'srv-bar'),
          PtrResourceRecord('baz', future, domainName: 'srv-boo'),
        ],
        <String, List<SrvResourceRecord>>{
          'srv-foo': <SrvResourceRecord>[
            SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'),
          ],
          'srv-bar': <SrvResourceRecord>[
            SrvResourceRecord('srv-bar', future, port: 222, weight: 1, priority: 1, target: 'target-bar'),
            SrvResourceRecord('srv-bar', future, port: 333, weight: 1, priority: 1, target: 'target-bar-2'),
          ],
          'srv-baz': <SrvResourceRecord>[
            SrvResourceRecord('srv-baz', future, port: 444, weight: 1, priority: 1, target: 'target-baz'),
          ],
        },
      );

      final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
        mdnsClient: client,
        logger: BufferLogger.test(),
        flutterUsage: TestUsage(),
        analytics: NoOpAnalytics(),
      );
      final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
        client,
        applicationId: 'srv-bar'
      );
      expect(result?.domainName, 'srv-bar');
      expect(result?.port, 222);
    });
    testWithoutContext('find with no txt record', () async {
      final MDnsClient client = FakeMDnsClient(
        <PtrResourceRecord>[
          PtrResourceRecord('foo', future, domainName: 'srv-foo'),
        ],
        <String, List<SrvResourceRecord>>{
          'srv-foo': <SrvResourceRecord>[
            SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'),
          ],
        },
        ipResponse: <String, List<IPAddressResourceRecord>>{
          'target-foo': <IPAddressResourceRecord>[
            IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
          ],
        },
      );

      final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
        mdnsClient: client,
        logger: BufferLogger.test(),
        flutterUsage: TestUsage(),
        analytics: NoOpAnalytics(),
      );
      final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
        client,
        applicationId: 'srv-foo',
        useDeviceIPAsHost: true,
      );
      expect(result?.domainName, 'srv-foo');
      expect(result?.port, 111);
      expect(result?.authCode, '');
      expect(result?.ipAddress?.address, '111.111.111.111');
    });
    testWithoutContext('find with empty txt record', () async {
      final MDnsClient client = FakeMDnsClient(
        <PtrResourceRecord>[
          PtrResourceRecord('foo', future, domainName: 'srv-foo'),
        ],
        <String, List<SrvResourceRecord>>{
          'srv-foo': <SrvResourceRecord>[
            SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'),
          ],
        },
        txtResponse: <String, List<TxtResourceRecord>>{
          'srv-foo': <TxtResourceRecord>[
            TxtResourceRecord('srv-foo', future, text: ''),
          ],
        },
        ipResponse: <String, List<IPAddressResourceRecord>>{
          'target-foo': <IPAddressResourceRecord>[
            IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
          ],
        },
      );

      final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
        mdnsClient: client,
        logger: BufferLogger.test(),
        flutterUsage: TestUsage(),
        analytics: NoOpAnalytics(),
      );
      final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
        client,
        applicationId: 'srv-foo',
        useDeviceIPAsHost: true,
      );
      expect(result?.domainName, 'srv-foo');
      expect(result?.port, 111);
      expect(result?.authCode, '');
      expect(result?.ipAddress?.address, '111.111.111.111');
    });
    testWithoutContext('find with valid txt record', () async {
      final MDnsClient client = FakeMDnsClient(
        <PtrResourceRecord>[
          PtrResourceRecord('foo', future, domainName: 'srv-foo'),
        ],
        <String, List<SrvResourceRecord>>{
          'srv-foo': <SrvResourceRecord>[
            SrvResourceRecord('srv-foo', future, port: 111, weight: 1, priority: 1, target: 'target-foo'),
          ],
        },
        txtResponse: <String, List<TxtResourceRecord>>{
          'srv-foo': <TxtResourceRecord>[
            TxtResourceRecord('srv-foo', future, text: 'authCode=xyz\n'),
          ],
        },
        ipResponse: <String, List<IPAddressResourceRecord>>{
          'target-foo': <IPAddressResourceRecord>[
            IPAddressResourceRecord('target-foo', 0, address: InternetAddress.tryParse('111.111.111.111')!),
          ],
        },
      );

      final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
        mdnsClient: client,
        logger: BufferLogger.test(),
        flutterUsage: TestUsage(),
        analytics: NoOpAnalytics(),
      );
      final MDnsVmServiceDiscoveryResult? result = await portDiscovery.firstMatchingVmService(
        client,
        applicationId: 'srv-foo',
        useDeviceIPAsHost: true,
      );
      expect(result?.domainName, 'srv-foo');
      expect(result?.port, 111);
      expect(result?.authCode, 'xyz/');
      expect(result?.ipAddress?.address, '111.111.111.111');
    });
  });
}

class FakeMDnsClient extends Fake implements MDnsClient {
  FakeMDnsClient(this.ptrRecords, this.srvResponse, {
    this.txtResponse = const <String, List<TxtResourceRecord>>{},
    this.ipResponse = const <String, List<IPAddressResourceRecord>>{},
    this.osErrorOnStart = false,
  });

  final List<PtrResourceRecord> ptrRecords;
  final Map<String, List<SrvResourceRecord>> srvResponse;
  final Map<String, List<TxtResourceRecord>> txtResponse;
  final Map<String, List<IPAddressResourceRecord>> ipResponse;
  final bool osErrorOnStart;

  @override
  Future<void> start({
    InternetAddress? listenAddress,
    NetworkInterfacesFactory? interfacesFactory,
    int mDnsPort = 5353,
    InternetAddress? mDnsAddress,
  }) async {
    if (osErrorOnStart) {
      throw const OSError('Operation not supported on socket', 102);
    }
  }

  @override
  Stream<T> lookup<T extends ResourceRecord>(
    ResourceRecordQuery query, {
    Duration timeout = const Duration(seconds: 5),
  }) {
    if (T == PtrResourceRecord && query.fullyQualifiedName == MDnsVmServiceDiscovery.dartVmServiceName) {
      return Stream<PtrResourceRecord>.fromIterable(ptrRecords) as Stream<T>;
    }
    if (T == SrvResourceRecord) {
      final String key = query.fullyQualifiedName;
      return Stream<SrvResourceRecord>.fromIterable(srvResponse[key] ?? <SrvResourceRecord>[]) as Stream<T>;
    }
    if (T == TxtResourceRecord) {
      final String key = query.fullyQualifiedName;
      return Stream<TxtResourceRecord>.fromIterable(txtResponse[key] ?? <TxtResourceRecord>[]) as Stream<T>;
    }
    if (T == IPAddressResourceRecord) {
      final String key = query.fullyQualifiedName;
      return Stream<IPAddressResourceRecord>.fromIterable(ipResponse[key] ?? <IPAddressResourceRecord>[]) as Stream<T>;
    }
    throw UnsupportedError('Unsupported query type $T');
  }

  @override
  void stop() {}
}

class FakeIOSDevice extends Fake implements IOSDevice {
  FakeIOSDevice({this.name = 'iPhone'});

  @override
  final String name;

  @override
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;

  @override
  bool isSupported() => true;

  @override
  bool isSupportedForProject(FlutterProject flutterProject) => true;

  @override
  DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();
}
