// 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.

// @dart = 2.8

import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/build_info.dart';

import '../src/common.dart';
import '../src/darwin_common.dart';
import 'test_utils.dart';

void main() {
  group('iOS app validation', () {
    String flutterRoot;
    Directory pluginRoot;
    String projectRoot;
    String flutterBin;
    Directory tempDir;

    setUpAll(() {
      flutterRoot = getFlutterRoot();
      tempDir = createResolvedTempDirectorySync('ios_content_validation.');
      flutterBin = fileSystem.path.join(
        flutterRoot,
        'bin',
        'flutter',
      );

      // Test a plugin example app to allow plugins validation.
      processManager.runSync(<String>[
        flutterBin,
        ...getLocalEngineArguments(),
        'create',
        '--verbose',
        '--platforms=ios',
        '-t',
        'plugin',
        'hello',
      ], workingDirectory: tempDir.path);

      pluginRoot = tempDir.childDirectory('hello');
      projectRoot = pluginRoot.childDirectory('example').path;
    });

    tearDownAll(() {
      tryToDelete(tempDir);
    });

    for (final BuildMode buildMode in <BuildMode>[BuildMode.debug, BuildMode.release]) {
      group('build in ${buildMode.name} mode', () {
        Directory buildPath;
        Directory outputApp;
        Directory frameworkDirectory;
        Directory outputFlutterFramework;
        File outputFlutterFrameworkBinary;
        Directory outputAppFramework;
        File outputAppFrameworkBinary;
        File outputPluginFrameworkBinary;

        setUpAll(() {
          processManager.runSync(<String>[
            flutterBin,
            ...getLocalEngineArguments(),
            'build',
            'ios',
            '--verbose',
            '--no-codesign',
            '--${buildMode.name}',
            '--obfuscate',
            '--split-debug-info=foo debug info/',
          ], workingDirectory: projectRoot);

          buildPath = fileSystem.directory(fileSystem.path.join(
            projectRoot,
            'build',
            'ios',
            'iphoneos',
          ));

          outputApp = buildPath.childDirectory('Runner.app');

          frameworkDirectory = outputApp.childDirectory('Frameworks');
          outputFlutterFramework = frameworkDirectory.childDirectory('Flutter.framework');
          outputFlutterFrameworkBinary = outputFlutterFramework.childFile('Flutter');

          outputAppFramework = frameworkDirectory.childDirectory('App.framework');
          outputAppFrameworkBinary = outputAppFramework.childFile('App');

          outputPluginFrameworkBinary = frameworkDirectory.childDirectory('hello.framework').childFile('hello');
        });

        testWithoutContext('flutter build ios builds a valid app', () {
          expect(outputPluginFrameworkBinary, exists);

          expect(outputAppFrameworkBinary, exists);
          expect(outputAppFramework.childFile('Info.plist'), exists);

          final File vmSnapshot = fileSystem.file(fileSystem.path.join(
            outputAppFramework.path,
            'flutter_assets',
            'vm_snapshot_data',
          ));

          expect(vmSnapshot.existsSync(), buildMode == BuildMode.debug);

          // Archiving should contain a bitcode blob, but not building.
          // This mimics Xcode behavior and prevents a developer from having to install a
          // 300+MB app.
          expect(containsBitcode(outputFlutterFrameworkBinary.path, processManager), isFalse);
        });

        testWithoutContext('Info.plist dart observatory Bonjour service', () {
          final String infoPlistPath = fileSystem.path.join(
            outputApp.path,
            'Info.plist',
          );
          final ProcessResult bonjourServices = processManager.runSync(
            <String>[
              'plutil',
              '-extract',
              'NSBonjourServices',
              'xml1',
              '-o',
              '-',
              infoPlistPath,
            ],
          );
          final bool bonjourServicesFound = (bonjourServices.stdout as String).contains('_dartobservatory._tcp');
          expect(bonjourServicesFound, buildMode == BuildMode.debug);

          final ProcessResult localNetworkUsage = processManager.runSync(
            <String>[
              'plutil',
              '-extract',
              'NSLocalNetworkUsageDescription',
              'xml1',
              '-o',
              '-',
              infoPlistPath,
            ],
          );
          final bool localNetworkUsageFound = localNetworkUsage.exitCode == 0;
          expect(localNetworkUsageFound, buildMode == BuildMode.debug);
        });

        testWithoutContext('check symbols', () {
          final ProcessResult symbols = processManager.runSync(
            <String>[
              'nm',
              '-g',
              outputAppFrameworkBinary.path,
              '-arch',
              'arm64',
            ],
          );
          final bool aotSymbolsFound = (symbols.stdout as String).contains('_kDartVmSnapshot');
          expect(aotSymbolsFound, buildMode != BuildMode.debug);
        });

        testWithoutContext('xcode_backend embed_and_thin', () {
          outputFlutterFramework.deleteSync(recursive: true);
          outputAppFramework.deleteSync(recursive: true);
          expect(outputFlutterFrameworkBinary.existsSync(), isFalse);
          expect(outputAppFrameworkBinary.existsSync(), isFalse);

          final String xcodeBackendPath = fileSystem.path.join(
            flutterRoot,
            'packages',
            'flutter_tools',
            'bin',
            'xcode_backend.sh',
          );

          // Simulate a common Xcode build setting misconfiguration
          // where FLUTTER_APPLICATION_PATH is missing
          final ProcessResult xcodeBackendResult = processManager.runSync(
            <String>[
              xcodeBackendPath,
              'embed_and_thin',
            ],
            environment: <String, String>{
              'SOURCE_ROOT': fileSystem.path.join(projectRoot, 'ios'),
              'BUILT_PRODUCTS_DIR': fileSystem.path.join(
                projectRoot,
                'build',
                'ios',
                'Release-iphoneos',
              ),
              'TARGET_BUILD_DIR': buildPath.path,
              'FRAMEWORKS_FOLDER_PATH': 'Runner.app/Frameworks',
              'VERBOSE_SCRIPT_LOGGING': '1',
              'FLUTTER_BUILD_MODE': 'release',
              'ACTION': 'install',
              // Skip bitcode stripping since we just checked that above.
            },
          );
          printOnFailure('Output of xcode_backend.sh:');
          printOnFailure(xcodeBackendResult.stdout.toString());
          printOnFailure(xcodeBackendResult.stderr.toString());

          expect(xcodeBackendResult.exitCode, 0);
          expect(outputFlutterFrameworkBinary.existsSync(), isTrue);
          expect(outputAppFrameworkBinary.existsSync(), isTrue);
        }, skip: !platform.isMacOS || buildMode != BuildMode.release); // [intended] only makes sense on macos.

        testWithoutContext('validate obfuscation', () {
          // HelloPlugin class is present in project.
          ProcessResult grepResult = processManager.runSync(<String>[
            'grep',
            '-r',
            'HelloPlugin',
            pluginRoot.path,
          ]);
          // Matches exits 0.
          expect(grepResult.exitCode, 0);

          // Not present in binary.
          grepResult = processManager.runSync(<String>[
            'grep',
            'HelloPlugin',
            outputAppFrameworkBinary.path,
          ]);
          // Does not match exits 1.
          expect(grepResult.exitCode, 1);
        });
      });
    }

    testWithoutContext('builds all plugin architectures for simulator', () {
      final ProcessResult buildSimulator = processManager.runSync(
        <String>[
          flutterBin,
          ...getLocalEngineArguments(),
          'build',
          'ios',
          '--simulator',
          '--verbose',
          '--no-codesign',
        ],
        workingDirectory: projectRoot,
      );
      expect(buildSimulator.exitCode, 0);

      final File pluginFrameworkBinary = fileSystem.file(fileSystem.path.join(
        projectRoot,
        'build',
        'ios',
        'iphonesimulator',
        'Runner.app',
        'Frameworks',
        'hello.framework',
        'hello',
      ));
      expect(pluginFrameworkBinary, exists);
      final ProcessResult archs = processManager.runSync(
        <String>['file', pluginFrameworkBinary.path],
      );
      expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library x86_64'));
      expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library arm64'));
    });

    testWithoutContext('build for simulator with all available architectures', () {
      final ProcessResult buildSimulator = processManager.runSync(
        <String>[
          flutterBin,
          ...getLocalEngineArguments(),
          'build',
          'ios',
          '--simulator',
          '--verbose',
          '--no-codesign',
        ],
        workingDirectory: projectRoot,
        environment: <String, String>{
          'FLUTTER_XCODE_ONLY_ACTIVE_ARCH': 'NO',
        },
      );
      // This test case would fail if arm64 or x86_64 simulators could not build.
      expect(buildSimulator.exitCode, 0);

      final File simulatorAppFrameworkBinary = fileSystem.file(fileSystem.path.join(
        projectRoot,
        'build',
        'ios',
        'iphonesimulator',
        'Runner.app',
        'Frameworks',
        'App.framework',
        'App',
      ));
      expect(simulatorAppFrameworkBinary, exists);
      final ProcessResult archs = processManager.runSync(
        <String>['file', simulatorAppFrameworkBinary.path],
      );
      expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library x86_64'));
      expect(archs.stdout, contains('Mach-O 64-bit dynamically linked shared library arm64'));
    });
  }, skip: !platform.isMacOS, // [intended] only makes sense for macos platform.
     timeout: const Timeout(Duration(minutes: 7))
  );
}