require 'shellwords'

module Fastlane
  module Actions
    module SharedValues
    end

    class VerifyXcodeAction < Action
      def self.run(params)
        UI.message("Verifying your Xcode installation at path '#{params[:xcode_path]}'...")

        # Check 1/2
        verify_codesign(params)

        # Check 2/2
        # More information https://developer.apple.com/news/?id=09222015a
        verify_gatekeeper(params)

        true
      end

      def self.verify_codesign(params)
        UI.message("Verifying Xcode was signed by Apple Inc.")

        codesign_output = Actions.sh("codesign --display --verbose=4 #{params[:xcode_path].shellescape}")

        # If the returned codesign info contains all entries for any one of these sets, we'll consider it valid
        accepted_codesign_detail_sets = [
          [ # Found on App Store installed Xcode installations
            "Identifier=com.apple.dt.Xcode",
            "Authority=Apple Mac OS Application Signing",
            "Authority=Apple Worldwide Developer Relations Certification Authority",
            "Authority=Apple Root CA",
            "TeamIdentifier=59GAB85EFG"
          ],
          [ # Found on App Store installed Xcode installations post-Xcode 11.3
            "Identifier=com.apple.dt.Xcode",
            "Authority=Apple Mac OS Application Signing",
            "Authority=Apple Worldwide Developer Relations Certification Authority",
            "Authority=Apple Root CA",
            "TeamIdentifier=APPLECOMPUTER"
          ],
          [ # Found on Xcode installations (pre-Xcode 8) downloaded from developer.apple.com
            "Identifier=com.apple.dt.Xcode",
            "Authority=Software Signing",
            "Authority=Apple Code Signing Certification Authority",
            "Authority=Apple Root CA",
            "TeamIdentifier=not set"
          ],
          [ # Found on Xcode installations (post-Xcode 8) downloaded from developer.apple.com
            "Identifier=com.apple.dt.Xcode",
            "Authority=Software Signing",
            "Authority=Apple Code Signing Certification Authority",
            "Authority=Apple Root CA",
            "TeamIdentifier=59GAB85EFG"
          ]
        ]

        # Map the accepted details sets into an equal number of sets collecting the details for which
        # the output of codesign did not have matches
        missing_details_sets = accepted_codesign_detail_sets.map do |accepted_details_set|
          accepted_details_set.reject { |detail| codesign_output.include?(detail) }
        end

        # If any of the sets is empty, it means that all details were matched, and the check is successful
        show_and_raise_error(nil, params[:xcode_path]) unless missing_details_sets.any?(&:empty?)

        UI.success("Successfully verified the code signature ✅")
      end

      def self.verify_gatekeeper(params)
        UI.message("Verifying Xcode using GateKeeper...")
        UI.message("This will take up to a few minutes, now is a great time to go for a coffee ☕...")

        command = "/usr/sbin/spctl --assess --verbose #{params[:xcode_path].shellescape}"
        must_includes = ['accepted']

        output = verify(command: command, must_includes: must_includes, params: params)

        if output.include?("source=Mac App Store") || output.include?("source=Apple") || output.include?("source=Apple System")
          UI.success("Successfully verified Xcode installation at path '#{params[:xcode_path]}' 🎧")
        else
          show_and_raise_error("Invalid Download Source of Xcode: #{output}", params[:xcode_path])
        end
      end

      def self.verify(command: nil, must_includes: nil, params: nil)
        output = Actions.sh(command)

        errors = []
        must_includes.each do |current|
          next if output.include?(current)
          errors << current
        end

        if errors.count > 0
          show_and_raise_error(errors.join("\n"), params[:xcode_path])
        end

        return output
      end

      def self.show_and_raise_error(error, xcode_path)
        UI.error("Attention: Your Xcode Installation could not be verified.")
        UI.error("If you believe that your Xcode is valid, please submit an issue on GitHub")
        if error
          UI.error("The following information couldn't be found:")
          UI.error(error)
        end
        UI.user_error!("The Xcode installation at path '#{xcode_path}' could not be verified.")
      end

      #####################################################
      # @!group Documentation
      #####################################################

      def self.description
        "Verifies that the Xcode installation is properly signed by Apple"
      end

      def self.details
        "This action was implemented after the recent Xcode attack to make sure you're not using a [hacked Xcode installation](http://researchcenter.paloaltonetworks.com/2015/09/novel-malware-xcodeghost-modifies-xcode-infects-apple-ios-apps-and-hits-app-store/)."
      end

      def self.available_options
        [
          FastlaneCore::ConfigItem.new(key: :xcode_path,
                                       env_name: "FL_VERIFY_XCODE_XCODE_PATH",
                                       description: "The path to the Xcode installation to test",
                                       code_gen_sensitive: true,
                                       default_value: File.expand_path('../../', FastlaneCore::Helper.xcode_path),
                                       default_value_dynamic: true,
                                       verify_block: proc do |value|
                                         UI.user_error!("Couldn't find Xcode at path '#{value}'") unless File.exist?(value)
                                       end)
        ]
      end

      def self.authors
        ["KrauseFx"]
      end

      def self.is_supported?(platform)
        [:ios, :mac].include?(platform)
      end

      def self.example_code
        [
          'verify_xcode',
          'verify_xcode(xcode_path: "/Applications/Xcode.app")'
        ]
      end

      def self.category
        :building
      end
    end
  end
end
