import Foundation
import JSONUtilities
import PathKit
import XcodeProj

public typealias BuildType = XCScheme.BuildAction.Entry.BuildFor

public struct Scheme: Equatable {

    public var name: String
    public var build: Build
    public var run: Run?
    public var archive: Archive?
    public var analyze: Analyze?
    public var test: Test?
    public var profile: Profile?
    public var management: Management?

    public init(
        name: String,
        build: Build,
        run: Run? = nil,
        test: Test? = nil,
        profile: Profile? = nil,
        analyze: Analyze? = nil,
        archive: Archive? = nil,
        management: Management? = nil
    ) {
        self.name = name
        self.build = build
        self.run = run
        self.test = test
        self.profile = profile
        self.analyze = analyze
        self.archive = archive
        self.management = management
    }

    public struct Management: Equatable {
        public static let sharedDefault = true

        public var shared: Bool
        public var orderHint: Int?
        public var isShown: Bool?

        public init?(
            shared: Bool = Scheme.Management.sharedDefault,
            orderHint: Int? = nil,
            isShown: Bool? = nil
        ) {
            if shared == Scheme.Management.sharedDefault, orderHint == nil, isShown == nil {
                return nil
            }

            self.shared = shared
            self.orderHint = orderHint
            self.isShown = isShown
        }
    }

    public struct SimulateLocation: Equatable {
        public enum ReferenceType: String {
            case predefined = "1"
            case gpx = "0"
        }

        public var allow: Bool
        public var defaultLocation: String?

        public var referenceType: ReferenceType? {
            guard let defaultLocation = self.defaultLocation else {
                return nil
            }

            if defaultLocation.contains(".gpx") {
                return .gpx
            }
            return .predefined
        }

        public init(allow: Bool, defaultLocation: String) {
            self.allow = allow
            self.defaultLocation = defaultLocation
        }
    }

    public struct ExecutionAction: Equatable {
        public var script: String
        public var name: String
        public var settingsTarget: String?
        public var shell: String?
        public init(name: String, script: String, shell: String? = nil, settingsTarget: String? = nil) {
            self.script = script
            self.name = name
            self.settingsTarget = settingsTarget
            self.shell = shell
        }
    }

    public struct Build: Equatable {
        public static let parallelizeBuildDefault = true
        public static let buildImplicitDependenciesDefault = true
        public static let runPostActionsOnFailureDefault = false

        public var targets: [BuildTarget]
        public var parallelizeBuild: Bool
        public var buildImplicitDependencies: Bool
        public var preActions: [ExecutionAction]
        public var postActions: [ExecutionAction]
        public var runPostActionsOnFailure: Bool

        public init(
            targets: [BuildTarget],
            parallelizeBuild: Bool = parallelizeBuildDefault,
            buildImplicitDependencies: Bool = buildImplicitDependenciesDefault,
            preActions: [ExecutionAction] = [],
            postActions: [ExecutionAction] = [],
            runPostActionsOnFailure: Bool = false
        ) {
            self.targets = targets
            self.parallelizeBuild = parallelizeBuild
            self.buildImplicitDependencies = buildImplicitDependencies
            self.preActions = preActions
            self.postActions = postActions
            self.runPostActionsOnFailure = runPostActionsOnFailure
        }
    }

    public struct Run: BuildAction {
        public static let enableAddressSanitizerDefault = false
        public static let enableASanStackUseAfterReturnDefault = false
        public static let enableThreadSanitizerDefault = false
        public static let enableUBSanitizerDefault = false
        public static let disableMainThreadCheckerDefault = false
        public static let stopOnEveryMainThreadCheckerIssueDefault = false
        public static let disableThreadPerformanceCheckerDefault = false
        public static let debugEnabledDefault = true
        public static let enableGPUValidationModeDefault = true

        public var config: String?
        public var commandLineArguments: [String: Bool]
        public var preActions: [ExecutionAction]
        public var postActions: [ExecutionAction]
        public var environmentVariables: [XCScheme.EnvironmentVariable]
        public var enableGPUFrameCaptureMode: XCScheme.LaunchAction.GPUFrameCaptureMode
        public var enableGPUValidationMode: Bool
        public var enableAddressSanitizer: Bool
        public var enableASanStackUseAfterReturn: Bool
        public var enableThreadSanitizer: Bool
        public var enableUBSanitizer: Bool
        public var disableMainThreadChecker: Bool
        public var stopOnEveryMainThreadCheckerIssue: Bool
        public var disableThreadPerformanceChecker: Bool
        public var language: String?
        public var region: String?
        public var askForAppToLaunch: Bool?
        public var launchAutomaticallySubstyle: String?
        public var debugEnabled: Bool
        public var simulateLocation: SimulateLocation?
        public var executable: String?
        public var storeKitConfiguration: String?
        public var customLLDBInit: String?
        public var macroExpansion: String?
        public var customWorkingDirectory: String?

        public init(
            config: String? = nil,
            executable: String? = nil,
            commandLineArguments: [String: Bool] = [:],
            preActions: [ExecutionAction] = [],
            postActions: [ExecutionAction] = [],
            environmentVariables: [XCScheme.EnvironmentVariable] = [],
            enableGPUFrameCaptureMode: XCScheme.LaunchAction.GPUFrameCaptureMode = XCScheme.LaunchAction.defaultGPUFrameCaptureMode,
            enableGPUValidationMode: Bool = enableGPUValidationModeDefault,
            enableAddressSanitizer: Bool = enableAddressSanitizerDefault,
            enableASanStackUseAfterReturn: Bool = enableASanStackUseAfterReturnDefault,
            enableThreadSanitizer: Bool = enableThreadSanitizerDefault,
            enableUBSanitizer: Bool = enableUBSanitizerDefault,
            disableMainThreadChecker: Bool = disableMainThreadCheckerDefault,
            stopOnEveryMainThreadCheckerIssue: Bool = stopOnEveryMainThreadCheckerIssueDefault,
            disableThreadPerformanceChecker: Bool = disableThreadPerformanceCheckerDefault,
            language: String? = nil,
            region: String? = nil,
            askForAppToLaunch: Bool? = nil,
            launchAutomaticallySubstyle: String? = nil,
            debugEnabled: Bool = debugEnabledDefault,
            simulateLocation: SimulateLocation? = nil,
            storeKitConfiguration: String? = nil,
            customLLDBInit: String? = nil,
            macroExpansion: String? = nil,
            customWorkingDirectory: String? = nil
        ) {
            self.config = config
            self.commandLineArguments = commandLineArguments
            self.preActions = preActions
            self.postActions = postActions
            self.environmentVariables = environmentVariables
            self.enableAddressSanitizer = enableAddressSanitizer
            self.enableASanStackUseAfterReturn = enableASanStackUseAfterReturn
            self.enableThreadSanitizer = enableThreadSanitizer
            self.enableUBSanitizer = enableUBSanitizer
            self.disableMainThreadChecker = disableMainThreadChecker
            self.enableGPUFrameCaptureMode = enableGPUFrameCaptureMode
            self.enableGPUValidationMode = enableGPUValidationMode
            self.stopOnEveryMainThreadCheckerIssue = stopOnEveryMainThreadCheckerIssue
            self.disableThreadPerformanceChecker = disableThreadPerformanceChecker
            self.language = language
            self.region = region
            self.askForAppToLaunch = askForAppToLaunch
            self.launchAutomaticallySubstyle = launchAutomaticallySubstyle
            self.debugEnabled = debugEnabled
            self.simulateLocation = simulateLocation
            self.storeKitConfiguration = storeKitConfiguration
            self.customLLDBInit = customLLDBInit
            self.macroExpansion = macroExpansion
            self.customWorkingDirectory = customWorkingDirectory
        }
    }

    public struct Test: BuildAction {
        public static let gatherCoverageDataDefault = false
        public static let enableAddressSanitizerDefault = false
        public static let enableASanStackUseAfterReturnDefault = false
        public static let enableThreadSanitizerDefault = false
        public static let enableUBSanitizerDefault = false
        public static let disableMainThreadCheckerDefault = false
        public static let debugEnabledDefault = true
        public static let captureScreenshotsAutomaticallyDefault = true
        public static let deleteScreenshotsWhenEachTestSucceedsDefault = true
        public static let preferredScreenCaptureFormatDefault = XCScheme.TestAction.ScreenCaptureFormat.screenRecording

        public var config: String?
        public var gatherCoverageData: Bool
        public var coverageTargets: [TestableTargetReference]
        public var enableAddressSanitizer: Bool
        public var enableASanStackUseAfterReturn: Bool
        public var enableThreadSanitizer: Bool
        public var enableUBSanitizer: Bool
        public var disableMainThreadChecker: Bool
        public var commandLineArguments: [String: Bool]
        public var targets: [TestTarget]
        public var preActions: [ExecutionAction]
        public var postActions: [ExecutionAction]
        public var environmentVariables: [XCScheme.EnvironmentVariable]
        public var language: String?
        public var region: String?
        public var debugEnabled: Bool
        public var customLLDBInit: String?
        public var captureScreenshotsAutomatically: Bool
        public var deleteScreenshotsWhenEachTestSucceeds: Bool
        public var testPlans: [TestPlan]
        public var macroExpansion: String?
        public var preferredScreenCaptureFormat: XCScheme.TestAction.ScreenCaptureFormat

        public struct TestTarget: Equatable, ExpressibleByStringLiteral {
            
            public static let randomExecutionOrderDefault = false
            public static let parallelizableDefault = false

            public var name: String { targetReference.name }
            public let targetReference: TestableTargetReference
            public var randomExecutionOrder: Bool
            public var parallelizable: Bool
            public var location: String?
            public var skipped: Bool
            public var skippedTests: [String]
            public var selectedTests: [String]

            public init(
                targetReference: TestableTargetReference,
                randomExecutionOrder: Bool = randomExecutionOrderDefault,
                parallelizable: Bool = parallelizableDefault,
                location: String? = nil,
                skipped: Bool = false,
                skippedTests: [String] = [],
                selectedTests: [String] = []
            ) {
                self.targetReference = targetReference
                self.randomExecutionOrder = randomExecutionOrder
                self.parallelizable = parallelizable
                self.location = location
                self.skipped = skipped
                self.skippedTests = skippedTests
                self.selectedTests = selectedTests
            }

            public init(stringLiteral value: String) {
                do {
                    targetReference = try TestableTargetReference(value)
                    randomExecutionOrder = false
                    parallelizable = false
                    location = nil
                    skipped = false
                    skippedTests = []
                    selectedTests = []
                } catch {
                    fatalError(SpecParsingError.invalidTargetReference(value).description)
                }
            }
        }

        public init(
            config: String? = nil,
            gatherCoverageData: Bool = gatherCoverageDataDefault,
            coverageTargets: [TestableTargetReference] = [],
            enableAddressSanitizer: Bool = enableAddressSanitizerDefault,
            enableASanStackUseAfterReturn: Bool = enableASanStackUseAfterReturnDefault,
            enableThreadSanitizer: Bool = enableThreadSanitizerDefault,
            enableUBSanitizer: Bool = enableUBSanitizerDefault,
            disableMainThreadChecker: Bool = disableMainThreadCheckerDefault,
            randomExecutionOrder: Bool = false,
            parallelizable: Bool = false,
            commandLineArguments: [String: Bool] = [:],
            targets: [TestTarget] = [],
            preActions: [ExecutionAction] = [],
            postActions: [ExecutionAction] = [],
            environmentVariables: [XCScheme.EnvironmentVariable] = [],
            testPlans: [TestPlan] = [],
            language: String? = nil,
            region: String? = nil,
            debugEnabled: Bool = debugEnabledDefault,
            customLLDBInit: String? = nil,
            captureScreenshotsAutomatically: Bool = captureScreenshotsAutomaticallyDefault,
            deleteScreenshotsWhenEachTestSucceeds: Bool = deleteScreenshotsWhenEachTestSucceedsDefault,
            macroExpansion: String? = nil,
            preferredScreenCaptureFormat: XCScheme.TestAction.ScreenCaptureFormat = preferredScreenCaptureFormatDefault
        ) {
            self.config = config
            self.gatherCoverageData = gatherCoverageData
            self.coverageTargets = coverageTargets
            self.enableAddressSanitizer = enableAddressSanitizer
            self.enableASanStackUseAfterReturn = enableASanStackUseAfterReturn
            self.enableThreadSanitizer = enableThreadSanitizer
            self.enableUBSanitizer = enableUBSanitizer
            self.disableMainThreadChecker = disableMainThreadChecker
            self.commandLineArguments = commandLineArguments
            self.targets = targets
            self.preActions = preActions
            self.postActions = postActions
            self.environmentVariables = environmentVariables
            self.testPlans = testPlans
            self.language = language
            self.region = region
            self.debugEnabled = debugEnabled
            self.customLLDBInit = customLLDBInit
            self.captureScreenshotsAutomatically = captureScreenshotsAutomatically
            self.deleteScreenshotsWhenEachTestSucceeds = deleteScreenshotsWhenEachTestSucceeds
            self.macroExpansion = macroExpansion
            self.preferredScreenCaptureFormat = preferredScreenCaptureFormat
        }

        public var shouldUseLaunchSchemeArgsEnv: Bool {
            commandLineArguments.isEmpty && environmentVariables.isEmpty
        }
    }

    public struct Analyze: BuildAction {
        public var config: String?
        public init(config: String) {
            self.config = config
        }
    }

    public struct Profile: BuildAction {
        public var config: String?
        public var commandLineArguments: [String: Bool]
        public var preActions: [ExecutionAction]
        public var postActions: [ExecutionAction]
        public var environmentVariables: [XCScheme.EnvironmentVariable]
        public var askForAppToLaunch: Bool?

        public init(
            config: String? = nil,
            commandLineArguments: [String: Bool] = [:],
            preActions: [ExecutionAction] = [],
            postActions: [ExecutionAction] = [],
            environmentVariables: [XCScheme.EnvironmentVariable] = [],
            askForAppToLaunch: Bool? = nil
        ) {
            self.config = config
            self.commandLineArguments = commandLineArguments
            self.preActions = preActions
            self.postActions = postActions
            self.environmentVariables = environmentVariables
            self.askForAppToLaunch = askForAppToLaunch
        }

        public var shouldUseLaunchSchemeArgsEnv: Bool {
            commandLineArguments.isEmpty && environmentVariables.isEmpty
        }
    }

    public struct Archive: BuildAction {
        public static let revealArchiveInOrganizerDefault = true

        public var config: String?
        public var customArchiveName: String?
        public var revealArchiveInOrganizer: Bool
        public var preActions: [ExecutionAction]
        public var postActions: [ExecutionAction]
        public init(
            config: String? = nil,
            customArchiveName: String? = nil,
            revealArchiveInOrganizer: Bool = revealArchiveInOrganizerDefault,
            preActions: [ExecutionAction] = [],
            postActions: [ExecutionAction] = []
        ) {
            self.config = config
            self.customArchiveName = customArchiveName
            self.revealArchiveInOrganizer = revealArchiveInOrganizer
            self.preActions = preActions
            self.postActions = postActions
        }
    }

    public struct BuildTarget: Equatable, Hashable {
        public var target: TestableTargetReference
        public var buildTypes: [BuildType]

        public init(target: TestableTargetReference, buildTypes: [BuildType] = BuildType.all) {
            self.target = target
            self.buildTypes = buildTypes
        }
    }
}

extension Scheme: PathContainer {

    static var pathProperties: [PathProperty] {
        [
            .dictionary([
                .object("test", Test.pathProperties),
            ]),
        ]
    }
}

protocol BuildAction: Equatable {
    var config: String? { get }
}

extension Scheme.ExecutionAction: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        script = try jsonDictionary.json(atKeyPath: "script")
        name = jsonDictionary.json(atKeyPath: "name") ?? "Run Script"
        settingsTarget = jsonDictionary.json(atKeyPath: "settingsTarget")
        shell = jsonDictionary.json(atKeyPath: "shell")
    }
}

extension Scheme.ExecutionAction: JSONEncodable {
    public func toJSONValue() -> Any {
        [
            "script": script,
            "name": name,
            "settingsTarget": settingsTarget,
            "shell": shell
        ]
    }
}

extension Scheme.SimulateLocation: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        allow = try jsonDictionary.json(atKeyPath: "allow")
        defaultLocation = jsonDictionary.json(atKeyPath: "defaultLocation")
    }
}

extension Scheme.SimulateLocation: JSONEncodable {
    public func toJSONValue() -> Any {
        var dict: [String: Any] = [
            "allow": allow,
        ]

        if let defaultLocation = defaultLocation {
            dict["defaultLocation"] = defaultLocation
        }

        return dict
    }
}

extension Scheme.Management: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        shared = jsonDictionary.json(atKeyPath: "shared") ?? Scheme.Management.sharedDefault
        orderHint = jsonDictionary.json(atKeyPath: "orderHint")
        isShown = jsonDictionary.json(atKeyPath: "isShown")
    }
}

extension Scheme.Management: JSONEncodable {
    public func toJSONValue() -> Any {
        var dict: [String: Any?] = [:]

        if shared != Scheme.Management.sharedDefault {
            dict["shared"] = shared
        }

        if let isShown = isShown {
            dict["isShown"] = isShown
        }

        if let orderHint = orderHint {
            dict["orderHint"] = orderHint
        }

        return dict
    }
}

extension Scheme.Run: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        config = jsonDictionary.json(atKeyPath: "config")
        commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
        preActions = jsonDictionary.json(atKeyPath: "preActions") ?? []
        postActions = jsonDictionary.json(atKeyPath: "postActions") ?? []
        environmentVariables = try XCScheme.EnvironmentVariable.parseAll(jsonDictionary: jsonDictionary)
        if let gpuFrameCaptureMode: String = jsonDictionary.json(atKeyPath: "enableGPUFrameCaptureMode") {
            enableGPUFrameCaptureMode = XCScheme.LaunchAction.GPUFrameCaptureMode.fromJSONValue(gpuFrameCaptureMode)
        } else {
            enableGPUFrameCaptureMode = XCScheme.LaunchAction.defaultGPUFrameCaptureMode
        }

        // support deprecated gpuValidationMode enum that was removed from XcodeProj
        if let gpuValidationMode: String = jsonDictionary.json(atKeyPath: "enableGPUValidationMode") {
            switch gpuValidationMode {
            case "enabled", "extended": enableGPUValidationMode = true
            case "disabled": enableGPUValidationMode = false
            default: enableGPUValidationMode = Scheme.Run.enableGPUValidationModeDefault
            }
        } else {
            enableGPUValidationMode = jsonDictionary.json(atKeyPath: "enableGPUValidationMode") ?? Scheme.Run.enableGPUValidationModeDefault
        }
        enableAddressSanitizer = jsonDictionary.json(atKeyPath: "enableAddressSanitizer") ?? Scheme.Run.enableAddressSanitizerDefault
        enableASanStackUseAfterReturn = jsonDictionary.json(atKeyPath: "enableASanStackUseAfterReturn") ?? Scheme.Run.enableASanStackUseAfterReturnDefault
        enableThreadSanitizer = jsonDictionary.json(atKeyPath: "enableThreadSanitizer") ?? Scheme.Run.enableThreadSanitizerDefault
        enableUBSanitizer = jsonDictionary.json(atKeyPath: "enableUBSanitizer") ?? Scheme.Run.enableUBSanitizerDefault
        disableMainThreadChecker = jsonDictionary.json(atKeyPath: "disableMainThreadChecker") ?? Scheme.Run.disableMainThreadCheckerDefault
        stopOnEveryMainThreadCheckerIssue = jsonDictionary.json(atKeyPath: "stopOnEveryMainThreadCheckerIssue") ?? Scheme.Run.stopOnEveryMainThreadCheckerIssueDefault
        disableThreadPerformanceChecker = jsonDictionary.json(atKeyPath: "disableThreadPerformanceChecker") ?? Scheme.Run.disableThreadPerformanceCheckerDefault
        language = jsonDictionary.json(atKeyPath: "language")
        region = jsonDictionary.json(atKeyPath: "region")
        debugEnabled = jsonDictionary.json(atKeyPath: "debugEnabled") ?? Scheme.Run.debugEnabledDefault
        simulateLocation = jsonDictionary.json(atKeyPath: "simulateLocation")
        storeKitConfiguration = jsonDictionary.json(atKeyPath: "storeKitConfiguration")
        executable = jsonDictionary.json(atKeyPath: "executable")

        // launchAutomaticallySubstyle is defined as a String in XcodeProj but its value is often
        // an integer. Parse both to be nice.
        if let int: Int = jsonDictionary.json(atKeyPath: "launchAutomaticallySubstyle") {
            launchAutomaticallySubstyle = String(int)
        } else if let string: String = jsonDictionary.json(atKeyPath: "launchAutomaticallySubstyle") {
            launchAutomaticallySubstyle = string
        }

        if let askLaunch: Bool = jsonDictionary.json(atKeyPath: "askForAppToLaunch") {
            askForAppToLaunch = askLaunch
        }
        customLLDBInit = jsonDictionary.json(atKeyPath: "customLLDBInit")
        macroExpansion = jsonDictionary.json(atKeyPath: "macroExpansion")
        customWorkingDirectory = jsonDictionary.json(atKeyPath: "customWorkingDirectory")
    }
}

extension Scheme.Run: JSONEncodable {
    public func toJSONValue() -> Any {
        var dict: [String: Any?] = [
            "commandLineArguments": commandLineArguments,
            "preActions": preActions.map { $0.toJSONValue() },
            "postActions": postActions.map { $0.toJSONValue() },
            "environmentVariables": environmentVariables.map { $0.toJSONValue() },
            "config": config,
            "language": language,
            "region": region,
            "askForAppToLaunch": askForAppToLaunch,
            "launchAutomaticallySubstyle": launchAutomaticallySubstyle,
            "executable": executable,
            "macroExpansion": macroExpansion
        ]

        if enableGPUFrameCaptureMode != XCScheme.LaunchAction.defaultGPUFrameCaptureMode {
            dict["enableGPUFrameCaptureMode"] = enableGPUFrameCaptureMode.toJSONValue()
        }
        
        if enableGPUValidationMode != Scheme.Run.enableGPUValidationModeDefault {
            dict["enableGPUValidationMode"] = enableGPUValidationMode
        }

        if enableAddressSanitizer != Scheme.Run.enableAddressSanitizerDefault {
            dict["enableAddressSanitizer"] = enableAddressSanitizer
        }

        if enableASanStackUseAfterReturn != Scheme.Run.enableASanStackUseAfterReturnDefault {
            dict["enableASanStackUseAfterReturn"] = enableASanStackUseAfterReturn
        }

        if enableThreadSanitizer != Scheme.Run.enableThreadSanitizerDefault {
            dict["enableThreadSanitizer"] = enableThreadSanitizer
        }

        if enableUBSanitizer != Scheme.Run.enableUBSanitizerDefault {
            dict["enableUBSanitizer"] = enableUBSanitizer
        }

        if disableMainThreadChecker != Scheme.Run.disableMainThreadCheckerDefault {
            dict["disableMainThreadChecker"] = disableMainThreadChecker
        }

        if stopOnEveryMainThreadCheckerIssue != Scheme.Run.stopOnEveryMainThreadCheckerIssueDefault {
            dict["stopOnEveryMainThreadCheckerIssue"] = stopOnEveryMainThreadCheckerIssue
        }

        if disableThreadPerformanceChecker != Scheme.Run.disableThreadPerformanceCheckerDefault {
            dict["disableThreadPerformanceChecker"] = disableThreadPerformanceChecker
        }

        if debugEnabled != Scheme.Run.debugEnabledDefault {
            dict["debugEnabled"] = debugEnabled
        }

        if let simulateLocation = simulateLocation {
            dict["simulateLocation"] = simulateLocation.toJSONValue()
        }
        if let storeKitConfiguration = storeKitConfiguration {
            dict["storeKitConfiguration"] = storeKitConfiguration
        }
        if let customLLDBInit = customLLDBInit {
            dict["customLLDBInit"] = customLLDBInit
        }
        if let customWorkingDirectory = customWorkingDirectory {
            dict["customWorkingDirectory"] = customWorkingDirectory
        }
        return dict
    }
}

extension Scheme.Test: PathContainer {

    static var pathProperties: [PathProperty] {
        [
            .object("testPlans", TestPlan.pathProperties),
        ]
    }
}

extension Scheme.Test: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        config = jsonDictionary.json(atKeyPath: "config")
        gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? Scheme.Test.gatherCoverageDataDefault

        if let coverages = jsonDictionary["coverageTargets"] as? [Any] {
            coverageTargets = try coverages.compactMap { target in
                if let string = target as? String {
                    return try TestableTargetReference(string)
                } else if let dictionary = target as? JSONDictionary,
                          let target: TestableTargetReference = try? .init(jsonDictionary: dictionary) {
                    return target
                } else {
                    return nil
                }
            }
        } else {
            coverageTargets = []
        }

        enableAddressSanitizer = jsonDictionary.json(atKeyPath: "enableAddressSanitizer") ?? Scheme.Test.enableAddressSanitizerDefault
        enableASanStackUseAfterReturn = jsonDictionary.json(atKeyPath: "enableASanStackUseAfterReturn") ?? Scheme.Test.enableASanStackUseAfterReturnDefault
        enableThreadSanitizer = jsonDictionary.json(atKeyPath: "enableThreadSanitizer") ?? Scheme.Test.enableThreadSanitizerDefault
        enableUBSanitizer = jsonDictionary.json(atKeyPath: "enableUBSanitizer") ?? Scheme.Test.enableUBSanitizerDefault
        disableMainThreadChecker = jsonDictionary.json(atKeyPath: "disableMainThreadChecker") ?? Scheme.Test.disableMainThreadCheckerDefault
        commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
        if let targets = jsonDictionary["targets"] as? [Any] {
            self.targets = try targets.compactMap { target in
                if let string = target as? String {
                    return try TestTarget(targetReference: TestableTargetReference(string))
                } else if let dictionary = target as? JSONDictionary {
                    return try TestTarget(jsonDictionary: dictionary)
                } else {
                    return nil
                }
            }
        } else {
            targets = []
        }
        preActions = jsonDictionary.json(atKeyPath: "preActions") ?? []
        postActions = jsonDictionary.json(atKeyPath: "postActions") ?? []
        environmentVariables = try XCScheme.EnvironmentVariable.parseAll(jsonDictionary: jsonDictionary)
        testPlans = try (jsonDictionary.json(atKeyPath: "testPlans") ?? []).map { try TestPlan(jsonDictionary: $0) }
        language = jsonDictionary.json(atKeyPath: "language")
        region = jsonDictionary.json(atKeyPath: "region")
        debugEnabled = jsonDictionary.json(atKeyPath: "debugEnabled") ?? Scheme.Test.debugEnabledDefault
        customLLDBInit = jsonDictionary.json(atKeyPath: "customLLDBInit")
        captureScreenshotsAutomatically = jsonDictionary.json(atKeyPath: "captureScreenshotsAutomatically") ?? Scheme.Test.captureScreenshotsAutomaticallyDefault
        deleteScreenshotsWhenEachTestSucceeds = jsonDictionary.json(atKeyPath: "deleteScreenshotsWhenEachTestSucceeds") ?? Scheme.Test.deleteScreenshotsWhenEachTestSucceedsDefault
        macroExpansion = jsonDictionary.json(atKeyPath: "macroExpansion")
        preferredScreenCaptureFormat = jsonDictionary.json(atKeyPath: "preferredScreenCaptureFormat") ?? Scheme.Test.preferredScreenCaptureFormatDefault
    }
}

extension Scheme.Test: JSONEncodable {
    public func toJSONValue() -> Any {
        var dict: [String: Any?] = [
            "commandLineArguments": commandLineArguments,
            "targets": targets.map { $0.toJSONValue() },
            "preActions": preActions.map { $0.toJSONValue() },
            "postActions": postActions.map { $0.toJSONValue() },
            "environmentVariables": environmentVariables.map { $0.toJSONValue() },
            "testPlans": testPlans.map { $0.toJSONValue() },
            "config": config,
            "language": language,
            "region": region,
            "coverageTargets": coverageTargets.map { $0.reference },
            "macroExpansion": macroExpansion
        ]

        if gatherCoverageData != Scheme.Test.gatherCoverageDataDefault {
            dict["gatherCoverageData"] = gatherCoverageData
        }

        if enableAddressSanitizer != Scheme.Test.enableAddressSanitizerDefault {
            dict["enableAddressSanitizer"] = enableAddressSanitizer
        }

        if enableASanStackUseAfterReturn != Scheme.Test.enableASanStackUseAfterReturnDefault {
            dict["enableASanStackUseAfterReturn"] = enableASanStackUseAfterReturn
        }

        if enableThreadSanitizer != Scheme.Test.enableThreadSanitizerDefault {
            dict["enableThreadSanitizer"] = enableThreadSanitizer
        }

        if enableUBSanitizer != Scheme.Test.enableUBSanitizerDefault {
            dict["enableUBSanitizer"] = enableUBSanitizer
        }

        if disableMainThreadChecker != Scheme.Test.disableMainThreadCheckerDefault {
            dict["disableMainThreadChecker"] = disableMainThreadChecker
        }

        if debugEnabled != Scheme.Run.debugEnabledDefault {
            dict["debugEnabled"] = debugEnabled
        }

        if let customLLDBInit = customLLDBInit {
            dict["customLLDBInit"] = customLLDBInit
        }

        if captureScreenshotsAutomatically != Scheme.Test.captureScreenshotsAutomaticallyDefault {
            dict["captureScreenshotsAutomatically"] = captureScreenshotsAutomatically
        }

        if deleteScreenshotsWhenEachTestSucceeds != Scheme.Test.deleteScreenshotsWhenEachTestSucceedsDefault {
            dict["deleteScreenshotsWhenEachTestSucceeds"] = deleteScreenshotsWhenEachTestSucceeds
        }

        if preferredScreenCaptureFormat != Scheme.Test.preferredScreenCaptureFormatDefault {
            dict["preferredScreenCaptureFormat"] = preferredScreenCaptureFormat.toJSONValue()
        }

        return dict
    }
}

extension Scheme.Test.TestTarget: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        if let name: String = jsonDictionary.json(atKeyPath: "name")  {
            targetReference = try TestableTargetReference(name)
        } else if let local: String = jsonDictionary.json(atKeyPath: "local") {
            self.targetReference = TestableTargetReference.local(local)
        } else if let project: String = jsonDictionary.json(atKeyPath: "project") {
            self.targetReference = TestableTargetReference.project(project)
        } else if let package: String = jsonDictionary.json(atKeyPath: "package") {
            self.targetReference = TestableTargetReference.package(package)
        } else {
            self.targetReference = try jsonDictionary.json(atKeyPath: "target")
        }
        randomExecutionOrder = jsonDictionary.json(atKeyPath: "randomExecutionOrder") ?? Scheme.Test.TestTarget.randomExecutionOrderDefault
        parallelizable = jsonDictionary.json(atKeyPath: "parallelizable") ?? Scheme.Test.TestTarget.parallelizableDefault
        location = jsonDictionary.json(atKeyPath: "location") ?? nil
        skipped = jsonDictionary.json(atKeyPath: "skipped") ?? false
        skippedTests = jsonDictionary.json(atKeyPath: "skippedTests") ?? []
        selectedTests = jsonDictionary.json(atKeyPath: "selectedTests") ?? []
    }
}

extension Scheme.Test.TestTarget: JSONEncodable {
    public func toJSONValue() -> Any {
        if randomExecutionOrder == Scheme.Test.TestTarget.randomExecutionOrderDefault,
            parallelizable == Scheme.Test.TestTarget.parallelizableDefault {
            return targetReference.reference
        }

        var dict: JSONDictionary = [
            "name": targetReference.reference,
        ]

        if randomExecutionOrder != Scheme.Test.TestTarget.randomExecutionOrderDefault {
            dict["randomExecutionOrder"] = randomExecutionOrder
        }
        if parallelizable != Scheme.Test.TestTarget.parallelizableDefault {
            dict["parallelizable"] = parallelizable
        }
        if let location = location {
            dict["location"] = location
        }
        if skipped {
            dict["skipped"] = skipped
        }

        return dict
    }
}

extension Scheme.Profile: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        config = jsonDictionary.json(atKeyPath: "config")
        commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
        preActions = jsonDictionary.json(atKeyPath: "preActions") ?? []
        postActions = jsonDictionary.json(atKeyPath: "postActions") ?? []
        environmentVariables = try XCScheme.EnvironmentVariable.parseAll(jsonDictionary: jsonDictionary)
        if let askLaunch: Bool = jsonDictionary.json(atKeyPath: "askForAppToLaunch") {
            askForAppToLaunch = askLaunch
        }
    }
}

extension Scheme.Profile: JSONEncodable {
    public func toJSONValue() -> Any {
        [
            "commandLineArguments": commandLineArguments,
            "preActions": preActions.map { $0.toJSONValue() },
            "postActions": postActions.map { $0.toJSONValue() },
            "environmentVariables": environmentVariables.map { $0.toJSONValue() },
            "config": config,
            "askForAppToLaunch": askForAppToLaunch,
        ] as [String: Any?]
    }
}

extension Scheme.Analyze: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        config = jsonDictionary.json(atKeyPath: "config")
    }
}

extension Scheme.Analyze: JSONEncodable {
    public func toJSONValue() -> Any {
        [
            "config": config,
        ]
    }
}

extension Scheme.Archive: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        config = jsonDictionary.json(atKeyPath: "config")
        customArchiveName = jsonDictionary.json(atKeyPath: "customArchiveName")
        revealArchiveInOrganizer = jsonDictionary.json(atKeyPath: "revealArchiveInOrganizer") ?? Scheme.Archive.revealArchiveInOrganizerDefault
        preActions = jsonDictionary.json(atKeyPath: "preActions") ?? []
        postActions = jsonDictionary.json(atKeyPath: "postActions") ?? []
    }
}

extension Scheme.Archive: JSONEncodable {
    public func toJSONValue() -> Any {
        var dict: [String: Any?] = [
            "preActions": preActions.map { $0.toJSONValue() },
            "postActions": postActions.map { $0.toJSONValue() },
            "config": config,
            "customArchiveName": customArchiveName,
        ]

        if revealArchiveInOrganizer != Scheme.Archive.revealArchiveInOrganizerDefault {
            dict["revealArchiveInOrganizer"] = revealArchiveInOrganizer
        }

        return dict
    }
}

extension Scheme: NamedJSONDictionaryConvertible {

    public init(name: String, jsonDictionary: JSONDictionary) throws {
        self.name = name
        build = try jsonDictionary.json(atKeyPath: "build")
        run = jsonDictionary.json(atKeyPath: "run")
        test = jsonDictionary.json(atKeyPath: "test")
        analyze = jsonDictionary.json(atKeyPath: "analyze")
        profile = jsonDictionary.json(atKeyPath: "profile")
        archive = jsonDictionary.json(atKeyPath: "archive")
        management = jsonDictionary.json(atKeyPath: "management")
    }
}

extension Scheme: JSONEncodable {
    public func toJSONValue() -> Any {
        [
            "build": build.toJSONValue(),
            "run": run?.toJSONValue(),
            "test": test?.toJSONValue(),
            "analyze": analyze?.toJSONValue(),
            "profile": profile?.toJSONValue(),
            "archive": archive?.toJSONValue(),
            "management": management?.toJSONValue(),
        ] as [String: Any?]
    }
}

extension Scheme.Build: JSONObjectConvertible {

    public init(jsonDictionary: JSONDictionary) throws {
        let targetDictionary: JSONDictionary = try jsonDictionary.json(atKeyPath: "targets")
        var targets: [Scheme.BuildTarget] = []
        for (targetRepr, possibleBuildTypes) in targetDictionary {
            let buildTypes: [BuildType]
            if let string = possibleBuildTypes as? String {
                switch string {
                case "all": buildTypes = BuildType.all
                case "none": buildTypes = []
                case "testing": buildTypes = [.testing, .analyzing]
                case "indexing": buildTypes = [.testing, .analyzing, .archiving]
                default: buildTypes = BuildType.all
                }
            } else if let enabledDictionary = possibleBuildTypes as? [String: Bool] {
                buildTypes = enabledDictionary.filter { $0.value }.compactMap { BuildType.from(jsonValue: $0.key) }
            } else if let array = possibleBuildTypes as? [String] {
                buildTypes = array.compactMap(BuildType.from)
            } else {
                buildTypes = BuildType.all
            }
            let target = try TestableTargetReference(targetRepr)
            targets.append(Scheme.BuildTarget(target: target, buildTypes: buildTypes))
        }
        self.targets = targets.sorted { $0.target.name < $1.target.name }
        preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? []
        postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? []
        parallelizeBuild = jsonDictionary.json(atKeyPath: "parallelizeBuild") ?? Scheme.Build.parallelizeBuildDefault
        buildImplicitDependencies = jsonDictionary.json(atKeyPath: "buildImplicitDependencies") ?? Scheme.Build.buildImplicitDependenciesDefault
        runPostActionsOnFailure = jsonDictionary.json(atKeyPath: "runPostActionsOnFailure") ?? Scheme.Build.runPostActionsOnFailureDefault
    }
}

extension Scheme.Build: JSONEncodable {
    public func toJSONValue() -> Any {
        let targetPairs = targets.map { ($0.target.reference, $0.buildTypes.map { $0.toJSONValue() }) }

        var dict: JSONDictionary = [
            "targets": Dictionary(uniqueKeysWithValues: targetPairs),
            "preActions": preActions.map { $0.toJSONValue() },
            "postActions": postActions.map { $0.toJSONValue() },
        ]

        if parallelizeBuild != Scheme.Build.parallelizeBuildDefault {
            dict["parallelizeBuild"] = parallelizeBuild
        }
        if buildImplicitDependencies != Scheme.Build.buildImplicitDependenciesDefault {
            dict["buildImplicitDependencies"] = buildImplicitDependencies
        }
        if runPostActionsOnFailure != Scheme.Build.runPostActionsOnFailureDefault {
            dict["runPostActionsOnFailure"] = runPostActionsOnFailure
        }

        return dict
    }
}

extension BuildType: JSONUtilities.JSONPrimitiveConvertible {

    public typealias JSONType = String

    public static func from(jsonValue: String) -> BuildType? {
        switch jsonValue {
        case "test", "testing": return .testing
        case "profile", "profiling": return .profiling
        case "run", "running": return .running
        case "archive", "archiving": return .archiving
        case "analyze", "analyzing": return .analyzing
        default: return nil
        }
    }

    public static var all: [BuildType] {
        [.running, .testing, .profiling, .analyzing, .archiving]
    }
}

extension BuildType: JSONEncodable {
    public func toJSONValue() -> Any {
        switch self {
        case .testing: return "testing"
        case .profiling: return "profiling"
        case .running: return "running"
        case .archiving: return "archiving"
        case .analyzing: return "analyzing"
        }
    }
}

extension XCScheme.EnvironmentVariable: JSONUtilities.JSONObjectConvertible {
    public static let enabledDefault = true

    private static func parseValue(_ value: Any) -> String {
        if let bool = value as? Bool {
            return bool ? "YES" : "NO"
        } else {
            return String(describing: value)
        }
    }

    public init(jsonDictionary: JSONDictionary) throws {

        let value: String
        if let jsonValue = jsonDictionary["value"] {
            value = XCScheme.EnvironmentVariable.parseValue(jsonValue)
        } else {
            // will throw error
            value = try jsonDictionary.json(atKeyPath: "value")
        }
        let variable: String = try jsonDictionary.json(atKeyPath: "variable")
        let enabled: Bool = jsonDictionary.json(atKeyPath: "isEnabled") ?? XCScheme.EnvironmentVariable.enabledDefault
        self.init(variable: variable, value: value, enabled: enabled)
    }

    static func parseAll(jsonDictionary: JSONDictionary) throws -> [XCScheme.EnvironmentVariable] {
        if let variablesDictionary: [String: Any] = jsonDictionary.json(atKeyPath: "environmentVariables") {
            return variablesDictionary.mapValues(parseValue)
                .map { XCScheme.EnvironmentVariable(variable: $0.key, value: $0.value, enabled: true) }
                .sorted { $0.variable < $1.variable }
        } else if let variablesArray: [JSONDictionary] = jsonDictionary.json(atKeyPath: "environmentVariables") {
            return try variablesArray.map(XCScheme.EnvironmentVariable.init)
        } else {
            return []
        }
    }
}

extension XCScheme.EnvironmentVariable: JSONEncodable {
    public func toJSONValue() -> Any {
        var dict: [String: Any] = [
            "variable": variable,
            "value": value,
        ]

        if enabled != XCScheme.EnvironmentVariable.enabledDefault {
            dict["isEnabled"] = enabled
        }

        return dict
    }
}

extension XCScheme.LaunchAction.GPUFrameCaptureMode: JSONEncodable {
    public func toJSONValue() -> Any {
        switch self {
        case .autoEnabled:
            return "autoEnabled"
        case .metal:
            return "metal"
        case .openGL:
            return "openGL"
        case .disabled:
            return "disabled"
        }
    }

    static func fromJSONValue(_ string: String) -> XCScheme.LaunchAction.GPUFrameCaptureMode {
        switch string {
        case "autoEnabled":
            return .autoEnabled
        case "metal":
            return .metal
        case "openGL":
            return .openGL
        case "disabled":
            return .disabled
        default:
            fatalError("Invalid enableGPUFrameCaptureMode value. Valid values are: autoEnabled, metal, openGL, disabled")
        }
    }
}

extension XCScheme.TestAction.ScreenCaptureFormat: JSONEncodable {
    public func toJSONValue() -> Any {
        rawValue
    }
}
