// 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/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/device_port_forwarder.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/ios/application_package.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';

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

final Platform macosPlatform = FakePlatform(
  operatingSystem: 'macos',
  environment: <String, String>{'HOME': '/'},
);

void main() {
  late FakePlatform osx;
  late FileSystemUtils fsUtils;
  late MemoryFileSystem fileSystem;
  final Logger logger = FakeLogger();

  setUp(() {
    osx = FakePlatform(environment: <String, String>{}, operatingSystem: 'macos');
    fileSystem = MemoryFileSystem.test();
    fsUtils = FileSystemUtils(fileSystem: fileSystem, platform: osx);
  });

  group('_IOSSimulatorDevicePortForwarder', () {
    late FakeSimControl simControl;
    late Xcode xcode;

    setUp(() {
      simControl = FakeSimControl();
      xcode = Xcode.test(processManager: FakeProcessManager.any());
    });

    testUsingContext(
      'dispose() does not throw an exception',
      () async {
        final simulator = IOSSimulator(
          '123',
          name: 'iPhone 11',
          simControl: simControl,
          simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4',
          logger: logger,
        );
        final DevicePortForwarder portForwarder = simulator.portForwarder;
        await portForwarder.forward(123);
        await portForwarder.forward(124);
        expect(portForwarder.forwardedPorts.length, 2);
        try {
          await portForwarder.dispose();
        } on Exception catch (e) {
          fail('Encountered exception: $e');
        }
        expect(portForwarder.forwardedPorts.length, 0);
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
        Xcode: () => xcode,
      },
      testOn: 'posix',
    );
  });

  testUsingContext(
    'simulators only support debug mode',
    () async {
      final simulator = IOSSimulator(
        '123',
        name: 'iPhone 11',
        simControl: FakeSimControl(),
        simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4',
        logger: logger,
      );

      expect(simulator.supportsRuntimeMode(BuildMode.debug), true);
      expect(simulator.supportsRuntimeMode(BuildMode.profile), false);
      expect(simulator.supportsRuntimeMode(BuildMode.release), false);
      expect(simulator.supportsRuntimeMode(BuildMode.jitRelease), false);
    },
    overrides: <Type, Generator>{
      Platform: () => osx,
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.any(),
    },
  );

  group('logFilePath', () {
    late FakeSimControl simControl;

    setUp(() {
      simControl = FakeSimControl();
    });

    testUsingContext(
      'defaults to rooted from HOME',
      () {
        osx.environment['HOME'] = '/foo/bar';
        final simulator = IOSSimulator(
          '123',
          name: 'iPhone 11',
          simControl: simControl,
          simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4',
          logger: logger,
        );
        expect(simulator.logFilePath, '/foo/bar/Library/Logs/CoreSimulator/123/system.log');
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystemUtils: () => fsUtils,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
      testOn: 'posix',
    );

    testUsingContext(
      'respects IOS_SIMULATOR_LOG_FILE_PATH',
      () {
        osx.environment['HOME'] = '/foo/bar';
        osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = '/baz/qux/%{id}/system.log';
        final simulator = IOSSimulator(
          '456',
          name: 'iPhone 11',
          simControl: simControl,
          simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-14-4',
          logger: logger,
        );
        expect(simulator.logFilePath, '/baz/qux/456/system.log');
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystemUtils: () => fsUtils,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );
  });

  group('sdkMajorVersion', () {
    late FakeSimControl simControl;

    setUp(() {
      simControl = FakeSimControl();
    });

    // This new version string appears in SimulatorApp-850 CoreSimulator-518.16 beta.
    testWithoutContext('can be parsed from iOS-11-3', () async {
      final device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
        simControl: simControl,
        logger: logger,
      );

      expect(await device.sdkMajorVersion, 11);
    });

    testWithoutContext('can be parsed from iOS 11.2', () async {
      final device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.2',
        simControl: simControl,
        logger: logger,
      );

      expect(await device.sdkMajorVersion, 11);
    });

    testWithoutContext('Has a simulator category', () async {
      final device = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simulatorCategory: 'iOS 11.2',
        simControl: simControl,
        logger: logger,
      );

      expect(device.category, Category.mobile);
    });
  });

  group('IOSSimulator.isSupported', () {
    late FakeSimControl simControl;

    setUp(() {
      simControl = FakeSimControl();
    });

    testUsingContext(
      'Apple TV is unsupported',
      () async {
        final simulator = IOSSimulator(
          'x',
          name: 'Apple TV',
          simControl: simControl,
          simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.tvOS-14-5',
          logger: logger,
        );
        expect(await simulator.isSupported(), false);
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'Apple Watch is unsupported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'Apple Watch',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.watchOS-8-0',
            logger: logger,
          ).isSupported(),
          false,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'iPad 2 is supported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'iPad 2',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
            logger: logger,
          ).isSupported(),
          true,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'iPad Retina is supported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'iPad Retina',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
            logger: logger,
          ).isSupported(),
          true,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'iPhone 5 is supported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'iPhone 5',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
            logger: logger,
          ).isSupported(),
          true,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'iPhone 5s is supported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'iPhone 5s',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
            logger: logger,
          ).isSupported(),
          true,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'iPhone SE is supported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'iPhone SE',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
            logger: logger,
          ).isSupported(),
          true,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'iPhone 7 Plus is supported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'iPhone 7 Plus',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
            logger: logger,
          ).isSupported(),
          true,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );

    testUsingContext(
      'iPhone X is supported',
      () async {
        expect(
          await IOSSimulator(
            'x',
            name: 'iPhone X',
            simControl: simControl,
            simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
            logger: logger,
          ).isSupported(),
          true,
        );
      },
      overrides: <Type, Generator>{
        Platform: () => osx,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
      },
    );
  });

  group('Simulator screenshot', () {
    testWithoutContext('supports screenshots', () async {
      final xcode = Xcode.test(processManager: FakeProcessManager.any());
      final Logger logger = BufferLogger.test();
      final fakeProcessManager = FakeProcessManager.list(<FakeCommand>[
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'io', 'x', 'screenshot', 'screenshot.png'],
        ),
      ]);

      // Test a real one. Screenshot doesn't require instance states.
      final simControl = SimControl(
        processManager: fakeProcessManager,
        logger: logger,
        xcode: xcode,
      );
      // Doesn't matter what the device is.
      final deviceUnderTest = IOSSimulator(
        'x',
        name: 'iPhone SE',
        simControl: simControl,
        simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
        logger: logger,
      );

      final File screenshot = MemoryFileSystem.test().file('screenshot.png');
      await deviceUnderTest.takeScreenshot(screenshot);
      expect(fakeProcessManager, hasNoRemainingExpectations);
    });
  });

  group('device log tool', () {
    late FakeProcessManager fakeProcessManager;
    late FakeSimControl simControl;

    setUp(() {
      fakeProcessManager = FakeProcessManager.empty();
      simControl = FakeSimControl();
    });

    testUsingContext(
      'syslog uses tail',
      () async {
        final device = IOSSimulator(
          'x',
          name: 'iPhone SE',
          simulatorCategory: 'iOS 9.3',
          simControl: simControl,
          logger: logger,
        );
        fakeProcessManager.addCommand(
          const FakeCommand(
            command: <String>['tail', '-n', '0', '-F', '/Library/Logs/CoreSimulator/x/system.log'],
          ),
        );
        await launchDeviceSystemLogTool(device);
        expect(fakeProcessManager, hasNoRemainingExpectations);
      },
      overrides: <Type, Generator>{
        ProcessManager: () => fakeProcessManager,
        FileSystem: () => fileSystem,
        Platform: () => macosPlatform,
        FileSystemUtils: () => FileSystemUtils(fileSystem: fileSystem, platform: macosPlatform),
      },
    );

    testUsingContext(
      'unified logging with app name',
      () async {
        final device = IOSSimulator(
          'x',
          name: 'iPhone SE',
          simulatorCategory: 'iOS 11.0',
          simControl: simControl,
          logger: logger,
        );
        const expectedPredicate =
            'eventType = logEvent AND '
            'processImagePath ENDSWITH "My Super Awesome App" AND '
            '(senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" OR processImageUUID == senderImageUUID) AND '
            'NOT(eventMessage CONTAINS ": could not find icon for representation -> com.apple.") AND '
            'NOT(eventMessage BEGINSWITH "assertion failed: ") AND '
            'NOT(eventMessage CONTAINS " libxpc.dylib ")';
        fakeProcessManager.addCommand(
          const FakeCommand(
            command: <String>[
              'xcrun',
              'simctl',
              'spawn',
              'x',
              'log',
              'stream',
              '--style',
              'json',
              '--predicate',
              expectedPredicate,
            ],
          ),
        );

        await launchDeviceUnifiedLogging(device, 'My Super Awesome App');
        expect(fakeProcessManager, hasNoRemainingExpectations);
      },
      overrides: <Type, Generator>{
        ProcessManager: () => fakeProcessManager,
        FileSystem: () => fileSystem,
      },
    );

    testUsingContext(
      'unified logging without app name',
      () async {
        final device = IOSSimulator(
          'x',
          name: 'iPhone SE',
          simulatorCategory: 'iOS 11.0',
          simControl: simControl,
          logger: logger,
        );
        const expectedPredicate =
            'eventType = logEvent AND '
            '(senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" OR processImageUUID == senderImageUUID) AND '
            'NOT(eventMessage CONTAINS ": could not find icon for representation -> com.apple.") AND '
            'NOT(eventMessage BEGINSWITH "assertion failed: ") AND '
            'NOT(eventMessage CONTAINS " libxpc.dylib ")';
        fakeProcessManager.addCommand(
          const FakeCommand(
            command: <String>[
              'xcrun',
              'simctl',
              'spawn',
              'x',
              'log',
              'stream',
              '--style',
              'json',
              '--predicate',
              expectedPredicate,
            ],
          ),
        );

        await launchDeviceUnifiedLogging(device, null);
        expect(fakeProcessManager, hasNoRemainingExpectations);
      },
      overrides: <Type, Generator>{
        ProcessManager: () => fakeProcessManager,
        FileSystem: () => fileSystem,
      },
    );
  });

  group('log reader', () {
    late FakeProcessManager fakeProcessManager;
    late FakeIosProject mockIosProject;
    late FakeSimControl simControl;
    late Xcode xcode;

    setUp(() {
      fakeProcessManager = FakeProcessManager.empty();
      mockIosProject = FakeIosProject();
      simControl = FakeSimControl();
      xcode = Xcode.test(processManager: FakeProcessManager.any());
    });

    group('syslog', () {
      setUp(() {
        final File syslog = fileSystem.file('system.log')..createSync();
        osx.environment['IOS_SIMULATOR_LOG_FILE_PATH'] = syslog.path;
      });

      testUsingContext(
        'simulator can parse Xcode 8/iOS 10-style logs',
        () async {
          fakeProcessManager
            ..addCommand(
              const FakeCommand(
                command: <String>['tail', '-n', '0', '-F', 'system.log'],
                stdout: '''
Dec 20 17:04:32 md32-11-vm1 My Super Awesome App[88374]: flutter: The Dart VM service is listening on http://127.0.0.1:64213/1Uoeu523990=/
Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''',
              ),
            )
            ..addCommand(
              const FakeCommand(
                command: <String>['tail', '-n', '0', '-F', '/private/var/log/system.log'],
              ),
            );

          final device = IOSSimulator(
            '123456',
            name: 'iPhone 11',
            simulatorCategory: 'iOS 10.0',
            simControl: simControl,
            logger: logger,
          );
          final DeviceLogReader logReader = device.getLogReader(
            app: await BuildableIOSApp.fromProject(mockIosProject, null),
          );

          final List<String> lines = await logReader.logLines.toList();
          expect(lines, <String>[
            'flutter: The Dart VM service is listening on http://127.0.0.1:64213/1Uoeu523990=/',
          ]);
          expect(fakeProcessManager, hasNoRemainingExpectations);
        },
        overrides: <Type, Generator>{
          ProcessManager: () => fakeProcessManager,
          FileSystem: () => fileSystem,
          Platform: () => osx,
          Xcode: () => xcode,
        },
      );

      testUsingContext(
        'simulator can output `)`',
        () async {
          fakeProcessManager
            ..addCommand(
              const FakeCommand(
                command: <String>['tail', '-n', '0', '-F', 'system.log'],
                stdout: '''
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) The Dart VM service is listening on http://127.0.0.1:57701/
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) ))))))))))
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) #0      Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)''',
              ),
            )
            ..addCommand(
              const FakeCommand(
                command: <String>['tail', '-n', '0', '-F', '/private/var/log/system.log'],
              ),
            );

          final device = IOSSimulator(
            '123456',
            name: 'iPhone 11',
            simulatorCategory: 'iOS 10.3',
            simControl: simControl,
            logger: logger,
          );
          final DeviceLogReader logReader = device.getLogReader(
            app: await BuildableIOSApp.fromProject(mockIosProject, null),
          );

          final List<String> lines = await logReader.logLines.toList();
          expect(lines, <String>[
            'The Dart VM service is listening on http://127.0.0.1:57701/',
            '))))))))))',
            '#0      Object.noSuchMethod (dart:core-patch/dart:core/object_patch.dart:46)',
          ]);
          expect(fakeProcessManager, hasNoRemainingExpectations);
        },
        overrides: <Type, Generator>{
          ProcessManager: () => fakeProcessManager,
          FileSystem: () => fileSystem,
          Platform: () => osx,
          Xcode: () => xcode,
        },
      );

      testUsingContext(
        'multiline messages',
        () async {
          fakeProcessManager
            ..addCommand(
              const FakeCommand(
                command: <String>['tail', '-n', '0', '-F', 'system.log'],
                stdout: '''
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Single line message
2017-09-13 15:26:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Multi line message
  continues...
  continues...
2020-03-11 15:58:28.207175-0700  localhost My Super Awesome App[72166]: (libnetwork.dylib) [com.apple.network:] [28 www.googleapis.com:443 stream, pid: 72166, tls] cancelled
	[28.1 64A98447-EABF-4983-A387-7DB9D0C1785F 10.0.1.200.57912<->172.217.6.74:443]
	Connected Path: satisfied (Path is satisfied), interface: en18
	Duration: 0.271s, DNS @0.000s took 0.001s, TCP @0.002s took 0.019s, TLS took 0.046s
	bytes in/out: 4468/1933, packets in/out: 11/10, rtt: 0.016s, retransmitted packets: 0, out-of-order packets: 0
2017-09-13 15:36:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Multi line message again
  and it goes...
  and goes...
2017-09-13 15:36:57.228948-0700  localhost My Super Awesome App[37195]: (Flutter) Single line message, not the part of the above
''',
              ),
            )
            ..addCommand(
              const FakeCommand(
                command: <String>['tail', '-n', '0', '-F', '/private/var/log/system.log'],
              ),
            );

          final device = IOSSimulator(
            '123456',
            name: 'iPhone 11',
            simulatorCategory: 'iOS 10.3',
            simControl: simControl,
            logger: logger,
          );
          final DeviceLogReader logReader = device.getLogReader(
            app: await BuildableIOSApp.fromProject(mockIosProject, null),
          );

          final List<String> lines = await logReader.logLines.toList();
          expect(lines, <String>[
            'Single line message',
            'Multi line message',
            '  continues...',
            '  continues...',
            'Multi line message again',
            '  and it goes...',
            '  and goes...',
            'Single line message, not the part of the above',
          ]);
          expect(fakeProcessManager, hasNoRemainingExpectations);
        },
        overrides: <Type, Generator>{
          ProcessManager: () => fakeProcessManager,
          FileSystem: () => fileSystem,
          Platform: () => osx,
          Xcode: () => xcode,
        },
      );
    });

    group('unified logging', () {
      late BufferLogger logger;

      setUp(() {
        logger = BufferLogger.test();
      });

      testUsingContext(
        'log reader handles escaped multiline messages',
        () async {
          const logPredicate =
              'eventType = logEvent AND processImagePath ENDSWITH "My Super Awesome App" '
              'AND (senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" '
              'OR processImageUUID == senderImageUUID) AND NOT(eventMessage CONTAINS ": could not find icon '
              'for representation -> com.apple.") AND NOT(eventMessage BEGINSWITH "assertion failed: ") '
              'AND NOT(eventMessage CONTAINS " libxpc.dylib ")';
          fakeProcessManager.addCommand(
            const FakeCommand(
              command: <String>[
                'xcrun',
                'simctl',
                'spawn',
                '123456',
                'log',
                'stream',
                '--style',
                'json',
                '--predicate',
                logPredicate,
              ],
              stdout: r'''
},{
  "traceID" : 37579774151491588,
  "eventMessage" : "Single line message",
  "eventType" : "logEvent"
},{
  "traceID" : 37579774151491588,
  "eventMessage" : "Multi line message\n  continues...\n  continues..."
},{
  "traceID" : 37579774151491588,
  "eventMessage" : "Single line message, not the part of the above",
  "eventType" : "logEvent"
},{
''',
            ),
          );

          final device = IOSSimulator(
            '123456',
            name: 'iPhone 11',
            simulatorCategory: 'iOS 11.0',
            simControl: simControl,
            logger: logger,
          );
          final DeviceLogReader logReader = device.getLogReader(
            app: await BuildableIOSApp.fromProject(mockIosProject, null),
          );

          final List<String> lines = await logReader.logLines.toList();
          expect(lines, <String>[
            'Single line message',
            'Multi line message\n  continues...\n  continues...',
            'Single line message, not the part of the above',
          ]);
          expect(fakeProcessManager, hasNoRemainingExpectations);
        },
        overrides: <Type, Generator>{
          ProcessManager: () => fakeProcessManager,
          FileSystem: () => fileSystem,
        },
      );

      testUsingContext(
        'log reader handles bad output',
        () async {
          const logPredicate =
              'eventType = logEvent AND processImagePath ENDSWITH "My Super Awesome App" '
              'AND (senderImagePath ENDSWITH "/Flutter" OR senderImagePath ENDSWITH "/libswiftCore.dylib" '
              'OR processImageUUID == senderImageUUID) AND NOT(eventMessage CONTAINS ": could not find icon '
              'for representation -> com.apple.") AND NOT(eventMessage BEGINSWITH "assertion failed: ") '
              'AND NOT(eventMessage CONTAINS " libxpc.dylib ")';
          fakeProcessManager.addCommand(
            const FakeCommand(
              command: <String>[
                'xcrun',
                'simctl',
                'spawn',
                '123456',
                'log',
                'stream',
                '--style',
                'json',
                '--predicate',
                logPredicate,
              ],
              stdout: '"eventMessage" : "message with incorrect escaping""',
            ),
          );

          final device = IOSSimulator(
            '123456',
            name: 'iPhone 11',
            simulatorCategory: 'iOS 11.0',
            simControl: simControl,
            logger: logger,
          );
          final DeviceLogReader logReader = device.getLogReader(
            app: await BuildableIOSApp.fromProject(mockIosProject, null),
          );

          final List<String> lines = await logReader.logLines.toList();
          expect(lines, isEmpty);
          expect(logger.errorText, contains('Logger returned non-JSON response'));
          expect(fakeProcessManager, hasNoRemainingExpectations);
        },
        overrides: <Type, Generator>{
          ProcessManager: () => fakeProcessManager,
          FileSystem: () => fileSystem,
          Logger: () => logger,
        },
      );
    });
  });

  group('SimControl', () {
    const validSimControlOutput = '''
{
  "devices" : {
    "com.apple.CoreSimulator.SimRuntime.iOS-14-0" : [
      {
        "dataPathSize" : 1734569984,
        "udid" : "iPhone 11-UDID",
        "isAvailable" : true,
        "logPathSize" : 9506816,
        "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-11",
        "state" : "Booted",
        "name" : "iPhone 11"
      }
    ],
    "com.apple.CoreSimulator.SimRuntime.iOS-13-0" : [
    ],
    "com.apple.CoreSimulator.SimRuntime.iOS-12-4" : [
    ],
    "com.apple.CoreSimulator.SimRuntime.tvOS-16-0" : [
    ],
    "com.apple.CoreSimulator.SimRuntime.watchOS-9-0" : [
    ],
    "com.apple.CoreSimulator.SimRuntime.iOS-16-0" : [
      {
        "dataPathSize" : 552366080,
        "udid" : "Phone w Watch-UDID",
        "isAvailable" : true,
        "logPathSize" : 90112,
        "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-11",
        "state" : "Booted",
        "name" : "Phone w Watch"
      },
      {
        "dataPathSize" : 2186457088,
        "udid" : "iPhone 13-UDID",
        "isAvailable" : true,
        "logPathSize" : 151552,
        "deviceTypeIdentifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-13",
        "state" : "Booted",
        "name" : "iPhone 13"
      }
    ]
  }
}
    ''';

    late FakeProcessManager fakeProcessManager;
    Xcode xcode;
    Xcode xcodeBadSimctl;
    late SimControl simControl;
    late IOSSimulatorUtils simulatorUtils;
    late IOSSimulatorUtils simulatorUtilsBadSimctl;
    late BufferLogger logger;
    const deviceId = 'smart-phone';
    const appId = 'flutterApp';

    setUp(() {
      fakeProcessManager = FakeProcessManager.empty();
      xcode = Xcode.test(processManager: FakeProcessManager.any());

      final fakeProcessManagerBadSimctl = FakeProcessManager.list(<FakeCommand>[
        const FakeCommand(command: <String>['which', 'sysctl']),
        const FakeCommand(
          command: <String>['sysctl', 'hw.optional.arm64'],
          stdout: 'hw.optional.arm64: 0',
        ),
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted'],
          stderr: 'failed to run',
          exitCode: 1,
        ),
      ]);
      xcodeBadSimctl = Xcode.test(processManager: fakeProcessManagerBadSimctl);
      logger = BufferLogger.test();
      simControl = SimControl(logger: logger, processManager: fakeProcessManager, xcode: xcode);
      simulatorUtils = IOSSimulatorUtils(
        logger: logger,
        processManager: fakeProcessManager,
        xcode: xcode,
      );
      simulatorUtilsBadSimctl = IOSSimulatorUtils(
        logger: logger,
        processManager: fakeProcessManager,
        xcode: xcodeBadSimctl,
      );
    });

    testWithoutContext('getConnectedDevices succeeds', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted', 'iOS', '--json'],
          stdout: validSimControlOutput,
        ),
      );

      final List<BootedSimDevice> devices = await simControl.getConnectedDevices();

      final BootedSimDevice phone1 = devices[0];
      expect(phone1.category, 'com.apple.CoreSimulator.SimRuntime.iOS-14-0');
      expect(phone1.name, 'iPhone 11');
      expect(phone1.udid, 'iPhone 11-UDID');

      final BootedSimDevice phone2 = devices[1];
      expect(phone2.category, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0');
      expect(phone2.name, 'Phone w Watch');
      expect(phone2.udid, 'Phone w Watch-UDID');

      final BootedSimDevice phone3 = devices[2];
      expect(phone3.category, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0');
      expect(phone3.name, 'iPhone 13');
      expect(phone3.udid, 'iPhone 13-UDID');
      expect(fakeProcessManager, hasNoRemainingExpectations);
    });

    testWithoutContext('IOSSimulatorUtils.getAttachedDevices succeeds', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted', 'iOS', '--json'],
          stdout: validSimControlOutput,
        ),
      );

      final List<IOSSimulator> devices = await simulatorUtils.getAttachedDevices();

      final IOSSimulator phone1 = devices[0];
      expect(phone1.category, Category.mobile);
      expect(phone1.name, 'iPhone 11');
      expect(phone1.simulatorCategory, 'com.apple.CoreSimulator.SimRuntime.iOS-14-0');

      final IOSSimulator phone2 = devices[1];
      expect(phone2.category, Category.mobile);
      expect(phone2.name, 'Phone w Watch');
      expect(phone2.simulatorCategory, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0');

      final IOSSimulator phone3 = devices[2];
      expect(phone3.category, Category.mobile);
      expect(phone3.name, 'iPhone 13');
      expect(phone3.simulatorCategory, 'com.apple.CoreSimulator.SimRuntime.iOS-16-0');
      expect(fakeProcessManager, hasNoRemainingExpectations);
    });

    testWithoutContext('getConnectedDevices handles bad simctl output', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'list', 'devices', 'booted', 'iOS', '--json'],
          stdout: 'Install Started',
        ),
      );

      final List<BootedSimDevice> devices = await simControl.getConnectedDevices();

      expect(devices, isEmpty);
      expect(fakeProcessManager, hasNoRemainingExpectations);
    });

    testWithoutContext(
      'IOSSimulatorUtils.getAttachedDevices handles simctl not properly installed',
      () async {
        final List<IOSSimulator> devices = await simulatorUtilsBadSimctl.getAttachedDevices();

        expect(devices, isEmpty);
        expect(fakeProcessManager, hasNoRemainingExpectations);
      },
    );

    testWithoutContext('sdkMajorVersion defaults to 11 when sdkNameAndVersion is junk', () async {
      final iosSimulatorA = IOSSimulator(
        'x',
        name: 'Testo',
        simulatorCategory: 'NaN',
        simControl: simControl,
        logger: logger,
      );

      expect(await iosSimulatorA.sdkMajorVersion, 11);
    });

    testWithoutContext('.install() handles exceptions', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'install', deviceId, appId],
          exception: ProcessException('xcrun', <String>[]),
        ),
      );

      expect(
        () async => simControl.install(deviceId, appId),
        throwsToolExit(message: r'Unable to install'),
      );
    });

    testWithoutContext('.uninstall() handles exceptions', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'uninstall', deviceId, appId],
          exception: ProcessException('xcrun', <String>[]),
        ),
      );

      expect(
        () async => simControl.uninstall(deviceId, appId),
        throwsToolExit(message: r'Unable to uninstall'),
      );
    });

    testWithoutContext('.launch() handles exceptions', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'launch', deviceId, appId],
          exception: ProcessException('xcrun', <String>[]),
        ),
      );

      expect(
        () async => simControl.launch(deviceId, appId),
        throwsToolExit(message: r'Unable to launch'),
      );
    });

    testWithoutContext('.stopApp() handles exceptions', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'terminate', deviceId, appId],
          exception: ProcessException('xcrun', <String>[]),
        ),
      );

      expect(
        () async => simControl.stopApp(deviceId, appId),
        throwsToolExit(message: 'Unable to terminate'),
      );
      expect(fakeProcessManager, hasNoRemainingExpectations);
    });

    testWithoutContext('simulator stopApp handles null app package', () async {
      final iosSimulator = IOSSimulator(
        'x',
        name: 'Testo',
        simulatorCategory: 'NaN',
        simControl: simControl,
        logger: logger,
      );

      expect(await iosSimulator.stopApp(null), isFalse);
    });

    testWithoutContext('listAvailableIOSRuntimes succeeds', () async {
      const validRuntimesOutput = '''
{
  "runtimes" : [
    {
      "bundlePath" : "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime",
      "buildversion" : "19E240",
      "platform" : "iOS",
      "runtimeRoot" : "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime/Contents/Resources/RuntimeRoot",
      "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-15-4",
      "version" : "15.4",
      "isInternal" : false,
      "isAvailable" : true,
      "name" : "iOS 15.4",
      "supportedDeviceTypes" : [
        {
          "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 6s.simdevicetype",
          "name" : "iPhone 6s",
          "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s",
          "productFamily" : "iPhone"
        },
        {
          "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 6s Plus.simdevicetype",
          "name" : "iPhone 6s Plus",
          "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s-Plus",
          "productFamily" : "iPhone"
        }
      ]
    },
    {
      "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime",
      "buildversion" : "20E247",
      "platform" : "iOS",
      "runtimeRoot" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot",
      "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-16-4",
      "version" : "16.4",
      "isInternal" : false,
      "isAvailable" : true,
      "name" : "iOS 16.4",
      "supportedDeviceTypes" : [
        {
          "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8.simdevicetype",
          "name" : "iPhone 8",
          "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8",
          "productFamily" : "iPhone"
        },
        {
          "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8 Plus.simdevicetype",
          "name" : "iPhone 8 Plus",
          "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus",
          "productFamily" : "iPhone"
        }
      ]
    },
    {
      "bundlePath" : "/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime",
      "buildversion" : "21A5268h",
      "platform" : "iOS",
      "runtimeRoot" : "/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime/Contents/Resources/RuntimeRoot",
      "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-17-0",
      "version" : "17.0",
      "isInternal" : false,
      "isAvailable" : true,
      "name" : "iOS 17.0",
      "supportedDeviceTypes" : [
        {
          "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8.simdevicetype",
          "name" : "iPhone 8",
          "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8",
          "productFamily" : "iPhone"
        },
        {
          "bundlePath" : "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 8 Plus.simdevicetype",
          "name" : "iPhone 8 Plus",
          "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus",
          "productFamily" : "iPhone"
        }
      ]
    }
  ]
}

''';
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'list', 'runtimes', 'available', 'iOS', '--json'],
          stdout: validRuntimesOutput,
        ),
      );

      final List<IOSSimulatorRuntime> runtimes = await simControl.listAvailableIOSRuntimes();

      final IOSSimulatorRuntime runtime1 = runtimes[0];
      expect(
        runtime1.bundlePath,
        '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime',
      );
      expect(runtime1.buildVersion, '19E240');
      expect(runtime1.platform, 'iOS');
      expect(
        runtime1.runtimeRoot,
        '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 15.4.simruntime/Contents/Resources/RuntimeRoot',
      );
      expect(runtime1.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-15-4');
      expect(runtime1.version, Version(15, 4, null));
      expect(runtime1.isInternal, false);
      expect(runtime1.isAvailable, true);
      expect(runtime1.name, 'iOS 15.4');

      final IOSSimulatorRuntime runtime2 = runtimes[1];
      expect(
        runtime2.bundlePath,
        '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime',
      );
      expect(runtime2.buildVersion, '20E247');
      expect(runtime2.platform, 'iOS');
      expect(
        runtime2.runtimeRoot,
        '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot',
      );
      expect(runtime2.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-16-4');
      expect(runtime2.version, Version(16, 4, null));
      expect(runtime2.isInternal, false);
      expect(runtime2.isAvailable, true);
      expect(runtime2.name, 'iOS 16.4');

      final IOSSimulatorRuntime runtime3 = runtimes[2];
      expect(
        runtime3.bundlePath,
        '/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime',
      );
      expect(runtime3.buildVersion, '21A5268h');
      expect(runtime3.platform, 'iOS');
      expect(
        runtime3.runtimeRoot,
        '/Library/Developer/CoreSimulator/Volumes/iOS_21A5268h/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 17.0.simruntime/Contents/Resources/RuntimeRoot',
      );
      expect(runtime3.identifier, 'com.apple.CoreSimulator.SimRuntime.iOS-17-0');
      expect(runtime3.version, Version(17, 0, null));
      expect(runtime3.isInternal, false);
      expect(runtime3.isAvailable, true);
      expect(runtime3.name, 'iOS 17.0');
    });

    testWithoutContext('listAvailableIOSRuntimes handles bad simctl output', () async {
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['xcrun', 'simctl', 'list', 'runtimes', 'available', 'iOS', '--json'],
          stdout: 'Install Started',
        ),
      );

      final List<IOSSimulatorRuntime> runtimes = await simControl.listAvailableIOSRuntimes();

      expect(runtimes, isEmpty);
      expect(logger.errorText, contains('simctl returned non-JSON response:'));
      expect(fakeProcessManager, hasNoRemainingExpectations);
    });

    testWithoutContext(
      'IOSSimulatorUtils.getAvailableIOSRuntimes handles simctl not properly installed',
      () async {
        final List<IOSSimulatorRuntime> runtimes = await simulatorUtilsBadSimctl
            .getAvailableIOSRuntimes();

        expect(runtimes, isEmpty);
        expect(fakeProcessManager, hasNoRemainingExpectations);
      },
    );
  });

  group('startApp', () {
    late FakePlistParser testPlistParser;
    late FakeSimControl simControl;
    late Xcode xcode;
    late BufferLogger logger;

    setUp(() {
      simControl = FakeSimControl();
      xcode = Xcode.test(processManager: FakeProcessManager.any());
      testPlistParser = FakePlistParser();
      logger = BufferLogger.test();
    });

    testUsingContext(
      "startApp uses compiled app's Info.plist to find CFBundleIdentifier",
      () async {
        final device = IOSSimulator(
          'x',
          name: 'iPhone SE',
          simulatorCategory: 'iOS 11.2',
          simControl: simControl,
          logger: logger,
        );
        testPlistParser.setProperty('CFBundleIdentifier', 'correct');

        final Directory mockDir = globals.fs.currentDirectory;
        final IOSApp package = PrebuiltIOSApp(
          projectBundleId: 'incorrect',
          bundleName: 'name',
          uncompressedBundle: mockDir,
          applicationPackage: mockDir,
        );

        const mockInfo = BuildInfo(
          BuildMode.debug,
          'flavor',
          treeShakeIcons: false,
          packageConfigPath: '.dart_tool/package_config.json',
        );
        final mockOptions = DebuggingOptions.disabled(mockInfo);
        await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions);

        expect(simControl.requests.single.appIdentifier, 'correct');
      },
      overrides: <Type, Generator>{
        PlistParser: () => testPlistParser,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
        Xcode: () => xcode,
      },
    );

    testUsingContext(
      'startApp fails when cannot find CFBundleIdentifier',
      () async {
        final device = IOSSimulator(
          'x',
          name: 'iPhone SE',
          simulatorCategory: 'iOS 11.2',
          simControl: simControl,
          logger: logger,
        );

        final Directory mockDir = globals.fs.currentDirectory;
        final IOSApp package = PrebuiltIOSApp(
          projectBundleId: 'incorrect',
          bundleName: 'name',
          uncompressedBundle: mockDir,
          applicationPackage: mockDir,
        );

        const mockInfo = BuildInfo(
          BuildMode.debug,
          'flavor',
          treeShakeIcons: false,
          packageConfigPath: '.dart_tool/package_config.json',
        );
        final mockOptions = DebuggingOptions.disabled(mockInfo);
        final LaunchResult result = await device.startApp(
          package,
          prebuiltApplication: true,
          debuggingOptions: mockOptions,
        );

        expect(result.started, isFalse);
        expect(simControl.requests, isEmpty);
        expect(
          logger.errorText,
          contains('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier'),
        );
      },
      overrides: <Type, Generator>{
        PlistParser: () => testPlistParser,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
        Logger: () => logger,
        Xcode: () => xcode,
      },
    );

    testUsingContext(
      'startApp forwards all supported debugging options',
      () async {
        final device = IOSSimulator(
          'x',
          name: 'iPhone SE',
          simulatorCategory: 'iOS 11.2',
          simControl: simControl,
          logger: logger,
        );
        testPlistParser.setProperty('CFBundleIdentifier', 'correct');

        final Directory mockDir = globals.fs.currentDirectory;
        final IOSApp package = PrebuiltIOSApp(
          projectBundleId: 'correct',
          bundleName: 'name',
          uncompressedBundle: mockDir,
          applicationPackage: mockDir,
        );

        const mockInfo = BuildInfo(
          BuildMode.debug,
          'flavor',
          treeShakeIcons: false,
          packageConfigPath: '.dart_tool/package_config.json',
        );
        final mockOptions = DebuggingOptions.enabled(
          mockInfo,
          enableSoftwareRendering: true,
          traceSystrace: true,
          traceToFile: 'path/to/trace.binpb',
          startPaused: true,
          disableServiceAuthCodes: true,
          skiaDeterministicRendering: true,
          useTestFonts: true,
          traceSkia: true,
          traceAllowlist: 'foo,bar',
          traceSkiaAllowlist: 'skia.a,skia.b',
          endlessTraceBuffer: true,
          profileMicrotasks: true,
          verboseSystemLogs: true,
          purgePersistentCache: true,
          dartFlags: '--baz',
          enableImpeller: ImpellerStatus.disabled,
          hostVmServicePort: 0,
        );

        await device.startApp(package, prebuiltApplication: true, debuggingOptions: mockOptions);
        expect(
          simControl.requests.single.launchArgs,
          unorderedEquals(<String>[
            '--enable-dart-profiling',
            '--enable-checked-mode',
            '--verify-entry-points',
            '--enable-software-rendering',
            '--trace-systrace',
            '--trace-to-file="path/to/trace.binpb"',
            '--start-paused',
            '--disable-service-auth-codes',
            '--skia-deterministic-rendering',
            '--use-test-fonts',
            '--trace-skia',
            '--trace-allowlist="foo,bar"',
            '--trace-skia-allowlist="skia.a,skia.b"',
            '--endless-trace-buffer',
            '--profile-microtasks',
            '--verbose-logging',
            '--purge-persistent-cache',
            '--enable-impeller=false',
            '--dart-flags=--baz',
            '--vm-service-port=0',
          ]),
        );
      },
      overrides: <Type, Generator>{
        PlistParser: () => testPlistParser,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
        Xcode: () => xcode,
      },
    );

    testUsingContext(
      'startApp using route',
      () async {
        final device = IOSSimulator(
          'x',
          name: 'iPhone SE',
          simulatorCategory: 'iOS 11.2',
          simControl: simControl,
          logger: logger,
        );
        testPlistParser.setProperty('CFBundleIdentifier', 'correct');

        final Directory mockDir = globals.fs.currentDirectory;
        final IOSApp package = PrebuiltIOSApp(
          projectBundleId: 'correct',
          bundleName: 'name',
          uncompressedBundle: mockDir,
          applicationPackage: mockDir,
        );

        const mockInfo = BuildInfo(
          BuildMode.debug,
          'flavor',
          treeShakeIcons: false,
          packageConfigPath: '.dart_tool/package_config.json',
        );
        final mockOptions = DebuggingOptions.enabled(mockInfo, enableSoftwareRendering: true);
        await device.startApp(
          package,
          prebuiltApplication: true,
          debuggingOptions: mockOptions,
          route: '/animation',
        );

        expect(simControl.requests.single.launchArgs, contains('--route=/animation'));
      },
      overrides: <Type, Generator>{
        PlistParser: () => testPlistParser,
        FileSystem: () => fileSystem,
        ProcessManager: () => FakeProcessManager.any(),
        Xcode: () => xcode,
      },
    );
  });

  group('IOSDevice.isSupportedForProject', () {
    late FakeSimControl simControl;
    late Xcode xcode;

    setUp(() {
      simControl = FakeSimControl();
      xcode = Xcode.test(processManager: FakeProcessManager.any());
    });

    testUsingContext(
      'is true on module project',
      () async {
        globals.fs.file('pubspec.yaml')
          ..createSync()
          ..writeAsStringSync(r'''
name: example

flutter:
  module: {}
''');
        final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(
          globals.fs.currentDirectory,
        );

        final simulator = IOSSimulator(
          'test',
          name: 'iPhone 11',
          simControl: simControl,
          simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
          logger: logger,
        );
        expect(simulator.isSupportedForProject(flutterProject), true);
      },
      overrides: <Type, Generator>{
        FileSystem: () => MemoryFileSystem.test(),
        ProcessManager: () => FakeProcessManager.any(),
        Xcode: () => xcode,
      },
    );

    testUsingContext(
      'is true with editable host app',
      () async {
        globals.fs.file('pubspec.yaml').createSync();
        globals.fs.directory('ios').createSync();
        final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(
          globals.fs.currentDirectory,
        );

        final simulator = IOSSimulator(
          'test',
          name: 'iPhone 11',
          simControl: simControl,
          simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
          logger: logger,
        );
        expect(simulator.isSupportedForProject(flutterProject), true);
      },
      overrides: <Type, Generator>{
        FileSystem: () => MemoryFileSystem.test(),
        ProcessManager: () => FakeProcessManager.any(),
        Xcode: () => xcode,
      },
    );

    testUsingContext(
      'is false with no host app and no module',
      () async {
        globals.fs.file('pubspec.yaml').createSync();
        final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(
          globals.fs.currentDirectory,
        );

        final simulator = IOSSimulator(
          'test',
          name: 'iPhone 11',
          simControl: simControl,
          simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
          logger: logger,
        );
        expect(simulator.isSupportedForProject(flutterProject), false);
      },
      overrides: <Type, Generator>{
        FileSystem: () => MemoryFileSystem.test(),
        ProcessManager: () => FakeProcessManager.any(),
        Xcode: () => xcode,
      },
    );

    testUsingContext('createDevFSWriter returns a LocalDevFSWriter', () {
      final simulator = IOSSimulator(
        'test',
        name: 'iPhone 11',
        simControl: simControl,
        simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3',
        logger: logger,
      );

      expect(simulator.createDevFSWriter(null, ''), isA<LocalDevFSWriter>());
    });
  });
}

class FakeIosProject extends Fake implements IosProject {
  @override
  Future<String> productBundleIdentifier(BuildInfo? buildInfo) async => 'com.example.test';

  @override
  Future<String> productName(BuildInfo? buildInfo) async => 'My Super Awesome App';
}

class FakeSimControl extends Fake implements SimControl {
  final requests = <LaunchRequest>[];

  @override
  Future<RunResult> launch(
    String deviceId,
    String appIdentifier, [
    List<String>? launchArgs,
  ]) async {
    requests.add(LaunchRequest(appIdentifier, launchArgs));
    return RunResult(ProcessResult(0, 0, '', ''), <String>['test']);
  }

  @override
  Future<RunResult> install(String deviceId, String appPath) async {
    return RunResult(ProcessResult(0, 0, '', ''), <String>['test']);
  }
}

class LaunchRequest {
  const LaunchRequest(this.appIdentifier, this.launchArgs);

  final String appIdentifier;
  final List<String>? launchArgs;
}
