// 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/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/macos/migrations/flutter_application_migration.dart';
import 'package:flutter_tools/src/macos/migrations/macos_deployment_target_migration.dart';
import 'package:flutter_tools/src/macos/migrations/nsapplicationmain_deprecation_migration.dart';
import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart';
import 'package:flutter_tools/src/macos/migrations/secure_restorable_state_migration.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart';
import 'package:unified_analytics/unified_analytics.dart';

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

void main() {
  group('remove link and embed migration', () {
    late FakeAnalytics fakeAnalytics;
    late MemoryFileSystem memoryFileSystem;
    late BufferLogger testLogger;
    late FakeMacOSProject macOSProject;
    late File xcodeProjectInfoFile;

    setUp(() {
      memoryFileSystem = MemoryFileSystem.test();
      fakeAnalytics = getInitializedFakeAnalyticsInstance(
        fs: memoryFileSystem,
        fakeFlutterVersion: FakeFlutterVersion(),
      );
      xcodeProjectInfoFile = memoryFileSystem.file('project.pbxproj');
      testLogger = BufferLogger.test();
      macOSProject = FakeMacOSProject();
      macOSProject.xcodeProjectInfoFile = xcodeProjectInfoFile;
    });

    testWithoutContext('skipped if files are missing', () async {
      final macosProjectMigration = RemoveMacOSFrameworkLinkAndEmbeddingMigration(
        macOSProject,
        testLogger,
        fakeAnalytics,
      );
      await macosProjectMigration.migrate();
      expect(fakeAnalytics.sentEvents, isEmpty);

      expect(xcodeProjectInfoFile.existsSync(), isFalse);

      expect(
        testLogger.traceText,
        contains('Xcode project not found, skipping framework link and embedding migration'),
      );
      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if nothing to upgrade', () async {
      const contents = 'Nothing to upgrade';
      xcodeProjectInfoFile.writeAsStringSync(contents);
      final DateTime projectLastModified = xcodeProjectInfoFile.lastModifiedSync();

      final macosProjectMigration = RemoveMacOSFrameworkLinkAndEmbeddingMigration(
        macOSProject,
        testLogger,
        fakeAnalytics,
      );
      await macosProjectMigration.migrate();
      expect(fakeAnalytics.sentEvents, isEmpty);

      expect(xcodeProjectInfoFile.lastModifiedSync(), projectLastModified);
      expect(xcodeProjectInfoFile.readAsStringSync(), contents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skips migrating script with embed', () async {
      const contents = r'''
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
			''';
      xcodeProjectInfoFile.writeAsStringSync(contents);

      final macosProjectMigration = RemoveMacOSFrameworkLinkAndEmbeddingMigration(
        macOSProject,
        testLogger,
        fakeAnalytics,
      );
      await macosProjectMigration.migrate();
      expect(xcodeProjectInfoFile.readAsStringSync(), contents);
      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('Xcode project is migrated', () async {
      xcodeProjectInfoFile.writeAsStringSync(r'''
prefix D73912F022F37F9E000D13A0
D73912F222F3801D000D13A0 suffix
D73912EF22F37F9E000D13A0
keep this 1
  33D1A10422148B71006C7A3E spaces
33D1A10522148B93006C7A3E
			shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n";
keep this 2
''');

      final macosProjectMigration = RemoveMacOSFrameworkLinkAndEmbeddingMigration(
        macOSProject,
        testLogger,
        fakeAnalytics,
      );
      await macosProjectMigration.migrate();
      expect(fakeAnalytics.sentEvents, isEmpty);

      expect(xcodeProjectInfoFile.readAsStringSync(), r'''
keep this 1
			shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
keep this 2
''');
      expect(testLogger.statusText, contains('Upgrading project.pbxproj'));
    });

    testWithoutContext('migration fails with leftover App.framework reference', () {
      xcodeProjectInfoFile.writeAsStringSync('''
		D73912F022F37F9bogus /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912F022F37F9bogus /* App.framework */; };
''');

      final macosProjectMigration = RemoveMacOSFrameworkLinkAndEmbeddingMigration(
        macOSProject,
        testLogger,
        fakeAnalytics,
      );

      expect(
        macosProjectMigration.migrate,
        throwsToolExit(message: 'Your Xcode project requires migration'),
      );
      expect(
        fakeAnalytics.sentEvents,
        contains(
          Event.appleUsageEvent(
            workflow: 'macos-migration',
            parameter: 'remove-frameworks',
            result: 'failure',
          ),
        ),
      );
    });

    testWithoutContext('migration fails with leftover FlutterMacOS.framework reference', () {
      xcodeProjectInfoFile.writeAsStringSync('''
				33D1A10522148B93bogus /* FlutterMacOS.framework in Bundle Framework */,
''');

      final macosProjectMigration = RemoveMacOSFrameworkLinkAndEmbeddingMigration(
        macOSProject,
        testLogger,
        fakeAnalytics,
      );
      expect(
        macosProjectMigration.migrate,
        throwsToolExit(message: 'Your Xcode project requires migration'),
      );
      expect(
        fakeAnalytics.sentEvents,
        contains(
          Event.appleUsageEvent(
            workflow: 'macos-migration',
            parameter: 'remove-frameworks',
            result: 'failure',
          ),
        ),
      );
    });
  });

  group('update deployment target version', () {
    late MemoryFileSystem memoryFileSystem;
    late BufferLogger testLogger;
    late FakeMacOSProject project;
    late File xcodeProjectInfoFile;
    late File podfile;

    setUp(() {
      memoryFileSystem = MemoryFileSystem();
      testLogger = BufferLogger.test();
      project = FakeMacOSProject();
      xcodeProjectInfoFile = memoryFileSystem.file('project.pbxproj');
      project.xcodeProjectInfoFile = xcodeProjectInfoFile;

      podfile = memoryFileSystem.file('Podfile');
      project.podfile = podfile;
    });

    testWithoutContext('skipped if files are missing', () async {
      final macOSProjectMigration = MacOSDeploymentTargetMigration(project, testLogger);
      await macOSProjectMigration.migrate();
      expect(xcodeProjectInfoFile.existsSync(), isFalse);
      expect(podfile.existsSync(), isFalse);

      expect(
        testLogger.traceText,
        contains('Xcode project not found, skipping macOS deployment target version migration'),
      );
      expect(
        testLogger.traceText,
        contains('Podfile not found, skipping global platform macOS version migration'),
      );
      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if nothing to upgrade', () async {
      const xcodeProjectInfoFileContents = 'MACOSX_DEPLOYMENT_TARGET = 10.15;';
      xcodeProjectInfoFile.writeAsStringSync(xcodeProjectInfoFileContents);

      final DateTime projectLastModified = xcodeProjectInfoFile.lastModifiedSync();

      const podfileFileContents = "# platform :osx, '10.15'";
      podfile.writeAsStringSync(podfileFileContents);
      final DateTime podfileLastModified = podfile.lastModifiedSync();

      final macOSProjectMigration = MacOSDeploymentTargetMigration(project, testLogger);
      await macOSProjectMigration.migrate();

      expect(xcodeProjectInfoFile.lastModifiedSync(), projectLastModified);
      expect(xcodeProjectInfoFile.readAsStringSync(), xcodeProjectInfoFileContents);
      expect(podfile.lastModifiedSync(), podfileLastModified);
      expect(podfile.readAsStringSync(), podfileFileContents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('Xcode project is migrated from 10.11 to 10.15', () async {
      xcodeProjectInfoFile.writeAsStringSync('''
 				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 10.11;
 				MTL_ENABLE_DEBUG_INFO = YES;
''');

      podfile.writeAsStringSync('''
# platform :osx, '10.11'
platform :osx, '10.11'
''');

      final macOSProjectMigration = MacOSDeploymentTargetMigration(project, testLogger);
      await macOSProjectMigration.migrate();

      expect(xcodeProjectInfoFile.readAsStringSync(), '''
 				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 10.15;
 				MTL_ENABLE_DEBUG_INFO = YES;
''');

      expect(podfile.readAsStringSync(), '''
# platform :osx, '10.15'
platform :osx, '10.15'
''');
      // Only print once even though 2 lines were changed.
      expect(
        'Updating minimum macOS deployment target to 10.15'
            .allMatches(testLogger.statusText)
            .length,
        1,
      );
    });

    testWithoutContext('Xcode project is migrated from 10.14 to 10.15', () async {
      xcodeProjectInfoFile.writeAsStringSync('''
 				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 10.14;
 				MTL_ENABLE_DEBUG_INFO = YES;
''');

      podfile.writeAsStringSync('''
# platform :osx, '10.14'
platform :osx, '10.14'
''');

      final macOSProjectMigration = MacOSDeploymentTargetMigration(project, testLogger);
      await macOSProjectMigration.migrate();

      expect(xcodeProjectInfoFile.readAsStringSync(), '''
 				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 10.15;
 				MTL_ENABLE_DEBUG_INFO = YES;
''');

      expect(podfile.readAsStringSync(), '''
# platform :osx, '10.15'
platform :osx, '10.15'
''');
      // Only print once even though 2 lines were changed.
      expect(
        'Updating minimum macOS deployment target to 10.15'
            .allMatches(testLogger.statusText)
            .length,
        1,
      );
    });

    testWithoutContext('Xcode project is migrated from 10.13 to 10.15', () async {
      xcodeProjectInfoFile.writeAsStringSync('''
 				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 10.13;
 				MTL_ENABLE_DEBUG_INFO = YES;
''');

      podfile.writeAsStringSync('''
# platform :osx, '10.13'
platform :osx, '10.13'
''');

      final macOSProjectMigration = MacOSDeploymentTargetMigration(project, testLogger);
      await macOSProjectMigration.migrate();

      expect(xcodeProjectInfoFile.readAsStringSync(), '''
 				GCC_WARN_UNUSED_VARIABLE = YES;
				MACOSX_DEPLOYMENT_TARGET = 10.15;
 				MTL_ENABLE_DEBUG_INFO = YES;
''');

      expect(podfile.readAsStringSync(), '''
# platform :osx, '10.15'
platform :osx, '10.15'
''');
      // Only print once even though 2 lines were changed.
      expect(
        'Updating minimum macOS deployment target to 10.15'
            .allMatches(testLogger.statusText)
            .length,
        1,
      );
    });
  });

  group('update NSPrincipalClass from FlutterApplication to NSApplication', () {
    late MemoryFileSystem memoryFileSystem;
    late BufferLogger testLogger;
    late FakeMacOSProject project;
    late File infoPlistFile;
    late FakePlistParser fakePlistParser;
    late FlutterProjectFactory flutterProjectFactory;

    setUp(() {
      memoryFileSystem = MemoryFileSystem();
      fakePlistParser = FakePlistParser();
      testLogger = BufferLogger.test();
      project = FakeMacOSProject();
      infoPlistFile = memoryFileSystem.file('Info.plist');
      project.defaultHostInfoPlist = infoPlistFile;
      flutterProjectFactory = FlutterProjectFactory(
        fileSystem: memoryFileSystem,
        logger: testLogger,
      );
    });

    void testWithMocks(String description, Future<void> Function() testMethod) {
      testUsingContext(
        description,
        testMethod,
        overrides: <Type, Generator>{
          FileSystem: () => memoryFileSystem,
          ProcessManager: () => FakeProcessManager.any(),
          PlistParser: () => fakePlistParser,
          FlutterProjectFactory: () => flutterProjectFactory,
        },
      );
    }

    testWithMocks('skipped if files are missing', () async {
      final macOSProjectMigration = FlutterApplicationMigration(project, testLogger);
      await macOSProjectMigration.migrate();
      expect(infoPlistFile.existsSync(), isFalse);

      expect(testLogger.traceText, isEmpty);
      expect(testLogger.statusText, isEmpty);
    });

    testWithMocks('skipped if no NSPrincipalClass key exists to upgrade', () async {
      final macOSProjectMigration = FlutterApplicationMigration(project, testLogger);
      infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
      await macOSProjectMigration.migrate();
      expect(
        fakePlistParser.getValueFromFile<String>(
          infoPlistFile.path,
          PlistParser.kNSPrincipalClassKey,
        ),
        isNull,
      );
      expect(testLogger.statusText, isEmpty);
    });

    testWithMocks('skipped if already de-upgraded (or never migrated)', () async {
      fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'NSApplication');
      final macOSProjectMigration = FlutterApplicationMigration(project, testLogger);
      infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
      await macOSProjectMigration.migrate();
      expect(
        fakePlistParser.getValueFromFile<String>(
          infoPlistFile.path,
          PlistParser.kNSPrincipalClassKey,
        ),
        'NSApplication',
      );
      expect(testLogger.statusText, isEmpty);
    });

    testWithMocks('Info.plist migrated to use NSApplication', () async {
      fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, 'FlutterApplication');
      final macOSProjectMigration = FlutterApplicationMigration(project, testLogger);
      infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
      await macOSProjectMigration.migrate();
      expect(
        fakePlistParser.getValueFromFile<String>(
          infoPlistFile.path,
          PlistParser.kNSPrincipalClassKey,
        ),
        'NSApplication',
      );
      // Only print once.
      expect(
        'Updating ${infoPlistFile.basename} to use NSApplication instead of FlutterApplication.'
            .allMatches(testLogger.statusText)
            .length,
        1,
      );
    });

    testWithMocks('Skip if NSPrincipalClass is not NSApplication', () async {
      const differentApp = 'DIFFERENTApplication';
      fakePlistParser.setProperty(PlistParser.kNSPrincipalClassKey, differentApp);
      final macOSProjectMigration = FlutterApplicationMigration(project, testLogger);
      infoPlistFile.writeAsStringSync('contents'); // Just so it exists: parser is a fake.
      await macOSProjectMigration.migrate();
      expect(
        fakePlistParser.getValueFromFile<String>(
          infoPlistFile.path,
          PlistParser.kNSPrincipalClassKey,
        ),
        differentApp,
      );
      expect(testLogger.traceText, isEmpty);
    });
  });

  group('migrate @NSApplicationMain attribute to @main', () {
    late MemoryFileSystem memoryFileSystem;
    late BufferLogger testLogger;
    late FakeMacOSProject project;
    late File appDelegateFile;

    setUp(() {
      memoryFileSystem = MemoryFileSystem();
      testLogger = BufferLogger.test();
      project = FakeMacOSProject();
      appDelegateFile = memoryFileSystem.file('AppDelegate.swift');
      project.appDelegateSwift = appDelegateFile;
    });

    testWithoutContext('skipped if files are missing', () async {
      final migration = NSApplicationMainDeprecationMigration(project, testLogger);
      await migration.migrate();
      expect(appDelegateFile.existsSync(), isFalse);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if nothing to upgrade', () async {
      const appDelegateContents = '''
import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}
''';
      appDelegateFile.writeAsStringSync(appDelegateContents);
      final DateTime lastModified = appDelegateFile.lastModifiedSync();

      final migration = NSApplicationMainDeprecationMigration(project, testLogger);
      await migration.migrate();

      expect(appDelegateFile.lastModifiedSync(), lastModified);
      expect(appDelegateFile.readAsStringSync(), appDelegateContents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('updates AppDelegate.swift', () async {
      appDelegateFile.writeAsStringSync('''
import Cocoa
import FlutterMacOS

@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}
''');

      final migration = NSApplicationMainDeprecationMigration(project, testLogger);
      await migration.migrate();

      expect(appDelegateFile.readAsStringSync(), '''
import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}
''');
      expect(
        testLogger.warningText,
        contains('uses the deprecated @NSApplicationMain attribute, updating'),
      );
    });
  });

  group('migrate AppDelegate to override applicationSupportsSecureRestorableState', () {
    late MemoryFileSystem memoryFileSystem;
    late BufferLogger testLogger;
    late FakeMacOSProject project;
    late File appDelegateFile;

    setUp(() {
      memoryFileSystem = MemoryFileSystem();
      testLogger = BufferLogger.test();
      project = FakeMacOSProject();
      appDelegateFile = memoryFileSystem.file('AppDelegate.swift');
      project.appDelegateSwift = appDelegateFile;
    });

    testWithoutContext('skipped if files are missing', () async {
      final migration = SecureRestorableStateMigration(project, testLogger);
      await migration.migrate();
      expect(appDelegateFile.existsSync(), isFalse);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if nothing to upgrade', () async {
      const appDelegateContents = '''
import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }

  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
    return true
  }
}
''';
      appDelegateFile.writeAsStringSync(appDelegateContents);
      final DateTime lastModified = appDelegateFile.lastModifiedSync();

      final migration = SecureRestorableStateMigration(project, testLogger);
      await migration.migrate();

      expect(appDelegateFile.lastModifiedSync(), lastModified);
      expect(appDelegateFile.readAsStringSync(), appDelegateContents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('skipped if override already present, but different', () async {
      const appDelegateContents = '''
import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }

  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
    return false
  }
}
''';
      appDelegateFile.writeAsStringSync(appDelegateContents);
      final DateTime lastModified = appDelegateFile.lastModifiedSync();

      final migration = SecureRestorableStateMigration(project, testLogger);
      await migration.migrate();

      expect(appDelegateFile.lastModifiedSync(), lastModified);
      expect(appDelegateFile.readAsStringSync(), appDelegateContents);

      expect(testLogger.statusText, isEmpty);
    });

    testWithoutContext('warns if override not present and cannot be applied cleanly', () async {
      const appDelegateContents = '''
import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  let ninetiesSong = "島人ぬ宝"

  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}
''';
      appDelegateFile.writeAsStringSync(appDelegateContents);
      final DateTime lastModified = appDelegateFile.lastModifiedSync();

      final migration = SecureRestorableStateMigration(project, testLogger);
      await migration.migrate();

      expect(appDelegateFile.lastModifiedSync(), lastModified);
      expect(appDelegateFile.readAsStringSync(), appDelegateContents);

      expect(
        testLogger.warningText,
        contains('has been modified and cannot be automatically migrated.'),
      );
    });

    testWithoutContext('updates AppDelegate.swift', () async {
      appDelegateFile.writeAsStringSync('''
import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}
''');

      final migration = SecureRestorableStateMigration(project, testLogger);
      await migration.migrate();

      expect(appDelegateFile.readAsStringSync(), '''
import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }

  override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
    return true
  }
}
''');
      expect(
        testLogger.warningText,
        contains('does not override applicationSupportsSecureRestorableState. Updating.'),
      );
    });
  });
}

class FakeMacOSProject extends Fake implements MacOSProject {
  @override
  File xcodeProjectInfoFile = MemoryFileSystem.test().file('xcodeProjectInfoFile');

  @override
  File defaultHostInfoPlist = MemoryFileSystem.test().file('InfoplistFile');

  @override
  File podfile = MemoryFileSystem.test().file('Podfile');

  @override
  File appDelegateSwift = MemoryFileSystem.test().file('AppDelegate.swift');
}
