import Foundation
import PathKit
import ProjectSpec
import Spectre
import XcodeProj
import XCTest
import Yams
import TestSupport
import Version

class SpecLoadingTests: XCTestCase {

    func testSpecLoaderDuplicateImports() {
        describe {
            $0.it("merges each file only once") {
                let path = fixturePath + "duplicated_include/duplicated_import_sut.yml"
                let project = try loadSpec(path: path)

                try expect(project.fileGroups) == ["First", "Second", "Third"]

                let sutTarget = project.targets.first
                try expect(sutTarget?.sources) == [TargetSource(path: "template")]
                try expect(sutTarget?.preBuildScripts) == [BuildScript(script: .script("swiftlint"), name: "Swiftlint")]
            }
        }
    }

    func testTargetDeclarationOrder() {
        describe {
            $0.it("preserves target declaration order from YAML") {
                let path = fixturePath + "target_ordering_test.yml"
                let project = try loadSpec(path: path)

                try expect(project.targets.map { $0.name }) == ["Zebra", "Apple", "Mango", "Banana"]
            }

            $0.it("preserves target declaration order from JSON") {
                let path = fixturePath + "target_ordering_test.json"
                let project = try loadSpec(path: path)

                try expect(project.targets.map { $0.name }) == ["Zebra", "Apple", "Mango", "Banana"]
            }

            $0.it("preserves declaration order for a template-supplied platform array") {
                let path = fixturePath + "target_ordering_template_test.yml"
                let project = try loadSpec(path: path)

                try expect(project.targets.map { $0.name }) == ["Zebra_iOS", "Zebra_tvOS", "Apple"]
            }

            $0.it("returns an empty order for contents that are not valid YAML") {
                try expect(loadOrderedTargetNames(contents: "\ttargets:\n\tZebra:")) == []
            }

            $0.it("preserves declaration order for a template-supplied target name") {
                let path = fixturePath + "target_ordering_template_name_test.yml"
                let project = try loadSpec(path: path)

                try expect(project.targets.map { $0.name }) == ["Zulu", "Apple"]
            }

            $0.it("preserves declaration order for a variable-expanded target key") {
                let path = fixturePath + "target_ordering_variable_test.yml"
                let project = try loadSpec(path: path, variables: ["FIRST_TARGET": "Zebra"])

                try expect(project.targets.map { $0.name }) == ["Zebra", "Apple"]
            }
        }
    }

    func testSpecLoader() {
        describe {
            $0.it("merges includes") {
                let path = fixturePath + "include_test.yml"
                let project = try loadSpec(path: path, variables: [:])

                try expect(project.name) == "NewName"
                try expect(project.settingGroups) == [
                    "test": Settings(buildSettings: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3", "MY_SETTING4": "${SETTING4}"]),
                    "new": Settings(buildSettings: ["MY_SETTING": "VALUE"]),
                    "toReplace": Settings(buildSettings: ["MY_SETTING2": "VALUE2"]),
                ]
                try expect(project.targets) == [
                    Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"], dependencies: [Dependency(type: .package(products: []), reference: "Yams")]),
                    Target(name: "NewTarget", type: .application, platform: .iOS, sources: ["template", "target"]),
                ]
            }

            $0.it("merges includes with additional") {
                let path = fixturePath + "include_test.yml"
                let project = try loadSpec(path: path, variables: ["INCLUDE_ADDITIONAL_YAML": "YES"])

                try expect(project.name) == "NewName"
                try expect(project.settingGroups) == [
                    "test": Settings(buildSettings: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3", "MY_SETTING4": "${SETTING4}", "MY_SETTING5": "ADDITIONAL"]),
                    "new": Settings(buildSettings: ["MY_SETTING": "VALUE"]),
                    "toReplace": Settings(buildSettings: ["MY_SETTING2": "VALUE2"]),
                ]
                try expect(project.targets) == [
                    Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"], dependencies: [Dependency(type: .package(products: []), reference: "SwiftPM"), Dependency(type: .package(products: []), reference: "Yams")]),
                    Target(name: "NewTarget", type: .application, platform: .iOS, sources: ["template", "target"]),
                ]
            }

            $0.it("merges includes without additional by environment variable") {
                let path = fixturePath + "include_test.yml"
                let project = try loadSpec(path: path, variables: ["INCLUDE_ADDITIONAL_YAML": "NO"])

                try expect(project.name) == "NewName"
                try expect(project.settingGroups) == [
                    "test": Settings(buildSettings: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3", "MY_SETTING4": "${SETTING4}"]),
                    "new": Settings(buildSettings: ["MY_SETTING": "VALUE"]),
                    "toReplace": Settings(buildSettings: ["MY_SETTING2": "VALUE2"]),
                ]
                try expect(project.targets) == [
                    Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"], dependencies: [Dependency(type: .package(products: []), reference: "Yams")]),
                    Target(name: "NewTarget", type: .application, platform: .iOS, sources: ["template", "target"]),
                ]
            }

            $0.it("expands directories") {
                let path = fixturePath + "paths_test.yml"
                let project = try loadSpec(path: path)

                try expect(project.configFiles) == [
                    "IncludedConfig": "paths_test/config",
                    "NewConfig": "config",
                    "RecursiveConfig": "paths_test/recursive_test/config",
                ]

                try expect(project.options) == SpecOptions(
                    carthageBuildPath: "paths_test/recursive_test/carthage_build",
                    carthageExecutablePath: "carthage_executable"
                )

                try expect(project.projectReferences) == [
                    ProjectReference(name: "ProjX", path: "TestProject/Project.xcodeproj"),
                ]

                try expect(project.aggregateTargets) == [
                    AggregateTarget(
                        name: "IncludedAggregateTarget",
                        targets: ["IncludedTarget"],
                        configFiles: ["Config": "paths_test/config"],
                        buildScripts: [BuildScript(script: .path("paths_test/buildScript"))]
                    ),
                    AggregateTarget(
                        name: "NewAggregateTarget",
                        targets: ["NewTarget"],
                        configFiles: ["Config": "config"],
                        buildScripts: [BuildScript(script: .path("buildScript"))]
                    ),
                    AggregateTarget(
                        name: "RecursiveAggregateTarget",
                        targets: ["RecursiveTarget"],
                        configFiles: ["Config": "paths_test/recursive_test/config"],
                        buildScripts: [BuildScript(script: .path("paths_test/recursive_test/buildScript"))]
                    ),
                ]

                try expect(project.targets) == [
                    Target(
                        name: "RecursiveTarget",
                        type: .application,
                        platform: .macOS,
                        configFiles: ["Config": "paths_test/recursive_test/config"],
                        sources: ["paths_test/recursive_test/source"],
                        dependencies: [Dependency(type: .framework, reference: "paths_test/recursive_test/Framework")],
                        info: Plist(path: "paths_test/recursive_test/info"),
                        entitlements: Plist(path: "paths_test/recursive_test/entitlements"),
                        preBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/prebuildScript"))],
                        postCompileScripts: [BuildScript(script: .path("paths_test/recursive_test/postCompileScript"))],
                        postBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/postBuildScript"))]
                    ),
                    Target(
                        name: "target1",
                        type: .framework,
                        platform: .macOS,
                        sources: ["paths_test/same_relative_path_test/parent1/same/target1/source"]
                    ),
                    Target(
                        name: "target2",
                        type: .framework,
                        platform: .macOS,
                        sources: ["paths_test/same_relative_path_test/parent2/same/target2/source"]
                    ),
                    Target(
                        name: "app",
                        type: .application,
                        platform: .macOS,
                        sources: ["paths_test/same_relative_path_test/source"],
                        dependencies: [
                            Dependency(type: .target, reference: "target1"),
                            Dependency(type: .target, reference: "target2")
                        ]
                    ),
                    Target(
                        name: "IncludedTarget",
                        type: .application,
                        platform: .tvOS,
                        configFiles: ["Config": "paths_test/config"],
                        sources: [
                            "paths_test/simplesource",
                            TargetSource(path: "paths_test/source", excludes: ["file"]),
                        ],
                        dependencies: [Dependency(type: .framework, reference: "paths_test/Framework")],
                        info: Plist(path: "paths_test/info"),
                        entitlements: Plist(path: "paths_test/entitlements"),
                        preBuildScripts: [BuildScript(script: .path("paths_test/preBuildScript"))],
                        postCompileScripts: [BuildScript(script: .path("paths_test/postCompileScript"))],
                        postBuildScripts: [BuildScript(script: .path("paths_test/postBuildScript"))],
                        scheme: TargetScheme(testPlans: [.init(path: "paths_test/TestPlan.xctestplan")])
                    ),
                    Target(
                        name: "NewTarget",
                        type: .application,
                        platform: .iOS,
                        configFiles: ["Config": "config"],
                        sources: [
                            "paths_test/template_source",
                            "source",
                        ],
                        dependencies: [Dependency(type: .framework, reference: "Framework")],
                        info: Plist(path: "info"),
                        entitlements: Plist(path: "entitlements"),
                        preBuildScripts: [BuildScript(script: .path("preBuildScript"))],
                        postCompileScripts: [BuildScript(script: .path("postCompileScript"))],
                        postBuildScripts: [BuildScript(script: .path("postBuildScript"))]
                    ),
                ]

                try expect(project.schemes) == [
                    Scheme(
                        name: "Scheme",
                        build: .init(targets: [.init(target: "NewTarget")]),
                        test: .init(testPlans: [.init(path: "paths_test/TestPlan.xctestplan")])
                    )
                ]

                try expect(project.packages) == [
                    "LocalPackage": .local(path: "paths_test/relative_local_package/LocalPackage", group: nil, excludeFromProject: false),
                ]

                try expect(project.fileGroups.contains("paths_test/relative_file_groups/TestFile.md")) == true
            }

            $0.it("respects directory expansion preference") {
                let path = fixturePath + "legacy_paths_test.yml"
                let project = try loadSpec(path: path)

                try expect(project.configFiles) == [
                    "IncludedConfig": "config",
                ]

                try expect(project.options) == SpecOptions(
                    carthageBuildPath: "carthage_build",
                    carthageExecutablePath: "carthage_executable"
                )

                try expect(project.aggregateTargets) == [
                    AggregateTarget(
                        name: "IncludedAggregateTarget",
                        targets: ["IncludedTarget"],
                        configFiles: ["Config": "config"],
                        buildScripts: [BuildScript(script: .path("buildScript"))]
                    ),
                ]

                try expect(project.targets) == [
                    Target(
                        name: "IncludedTarget",
                        type: .application,
                        platform: .tvOS,
                        configFiles: ["Config": "config"],
                        sources: ["source"],
                        dependencies: [Dependency(type: .framework, reference: "Framework")],
                        info: Plist(path: "info"),
                        entitlements: Plist(path: "entitlements"),
                        preBuildScripts: [BuildScript(script: .path("preBuildScript"))],
                        postCompileScripts: [BuildScript(script: .path("postCompileScript"))],
                        postBuildScripts: [BuildScript(script: .path("postBuildScript"))]
                    ),
                ]
            }

            $0.it("parses yaml types") {
                let path = fixturePath + "yaml.yml"
                let dictionary = try loadYamlDictionary(path: path)
                let expectedDictionary: [String: Any] = [
                    "true": true,
                    "false": false,
                    "yes": true,
                    "no": false,
                    "yesQuote": "YES",
                    "noQuote": "NO",
                    "int": 1,
                    "intQuote": "1",
                    "float": 3.2,
                    "floatQuote": "10.10",
                    "string": "hello",
                    "stringQuote": "hello",
                    "space": " ",
                    "empty": "",
                    "emptyQuote": "",
                    "emptyDictionary": [String: Any](),
                    "arrayLiteral": [1, 2],
                    "arrayList": [1, 2],
                ]
                for (key, expectedValue) in expectedDictionary {
                    guard let parsedValue = dictionary[key] else {
                        throw failure("\(key) does not exist")
                    }
                    if String(describing: expectedValue) != String(describing: parsedValue) {
                        throw failure("\(key): \(parsedValue) does not equal \(expectedValue)")
                    }
                }
                if !(dictionary as NSDictionary).isEqual(expectedDictionary as NSDictionary) {
                    throw failure("parsed yaml types don't match:\n\nParsed:\n\t\(dictionary.map { "\($0.key): \($0.value)" }.joined(separator: "\n\t"))\nExpected:\n\t\(expectedDictionary.map { "\($0.key): \($0.value)" }.joined(separator: "\n\t"))")
                }
            }

            $0.it("expands variables") {
                let path = fixturePath + "variables_test.yml"
                let project = try loadSpec(path: path, variables: [
                    "SETTING1": "ENV VALUE1",
                    "SETTING4": "ENV VALUE4",
                    "variable": "doesWin",
                ])

                try expect(project.name) == "NewName"
                try expect(project.settingGroups) == [
                    "test": Settings(buildSettings: ["MY_SETTING1": "ENV VALUE1", "MY_SETTING2": "VALUE2", "MY_SETTING4": "ENV VALUE4"]),
                    "toReplace": Settings(buildSettings: ["MY_SETTING1": "VALUE1"]),
                ]
                try expect(project.targets.last?.sources) == ["SomeTarget", "doesWin", "templateVariable"]
            }
        }
    }

    func testSpecLoaderLoadingJSON() {
        describe {
            $0.it("merges includes") {
                let path = fixturePath + "include_test.json"
                let project = try loadSpec(path: path)

                try expect(project.name) == "NewName"
                try expect(project.settingGroups) == [
                    "test": Settings(buildSettings: ["MY_SETTING1": "NEW VALUE", "MY_SETTING2": "VALUE2", "MY_SETTING3": "VALUE3", "MY_SETTING4": "${SETTING4}"]),
                    "new": Settings(buildSettings: ["MY_SETTING": "VALUE"]),
                    "toReplace": Settings(buildSettings: ["MY_SETTING2": "VALUE2"]),
                ]
                try expect(project.targets) == [
                    Target(name: "IncludedTargetNew", type: .application, platform: .tvOS, sources: ["NewSource"]),
                    Target(name: "NewTarget", type: .application, platform: .iOS),
                ]
            }
        }
    }

    func testSpecWarningValidation() {
        describe {
            var path: Path!
            $0.before {
                path = Path(components: [NSTemporaryDirectory(), "\(NSUUID().uuidString).yaml"])

            }
            $0.after {
                try? FileManager.default.removeItem(atPath: path.string)
            }
            $0.it("fails validating warnings for deprecated placeholder usage") {
                let dictionary: [String: Any] = [
                    "name": "TestSpecWarningValidation",
                    "templates": [
                        "Framework": [
                            "type": "framework",
                            "sources": ["${target_name}/${platform}/Sources"],
                        ],
                    ],
                    "targets": [
                        "Framework": [
                            "type": "framework",
                            "platform": "iOS",
                            "templates": ["Framework"],
                        ],
                    ],
                ]
                try? Yams.dump(object: dictionary).write(toFile: path.string, atomically: true, encoding: .utf8)
                let specLoader = SpecLoader(version: "1.1.0")
                do {
                    _ = try specLoader.loadProject(path: path)
                } catch {
                    throw failure("\(error)")
                }
            }

            $0.it("successfully validates warnings for new placeholder usage") {
                let dictionary: [String: Any] = [
                    "name": "TestSpecWarningValidation",
                    "templates": [
                        "Framework": [
                            "type": "framework",
                            "sources": ["${target_name}/${platform}/Sources"],
                        ],
                    ],
                    "targets": [
                        "Framework": [
                            "type": "framework",
                            "platform": "iOS",
                            "templates": ["Framework"],
                        ],
                    ],
                ]
                try? Yams.dump(object: dictionary).write(toFile: path.string, atomically: true, encoding: .utf8)
                let specLoader = SpecLoader(version: "1.1.0")
                do {
                    _ = try specLoader.loadProject(path: path)
                } catch {
                    throw failure("\(error)")
                }
                do {
                    try specLoader.validateProjectDictionaryWarnings()
                } catch {
                    throw failure("Expected to not throw a validation error. Got: \(error)")
                }
            }
        }
    }

    func testProjectSpecParser() {
        let validTarget: [String: Any] = ["type": "application", "platform": "iOS"]
        let validBreakpoint: [String: Any] = ["type": "Exception", "scope": "All", "stopOnStyle": "Catch"]
        let invalid = "invalid"

        describe {

            $0.it("fails with incorrect platform") {
                var target = validTarget
                target["platform"] = invalid
                try expectTargetError(target, .unknownTargetPlatform(invalid))
            }

            $0.it("fails with incorrect product type") {
                var target = validTarget
                target["type"] = invalid
                try expectTargetError(target, .unknownTargetType(invalid))
            }

            $0.it("fails with invalid dependency") {
                var target = validTarget
                target["dependencies"] = [[invalid: "name"]]
                try expectTargetError(target, .invalidDependency([invalid: "name"]))
            }

            $0.it("fails with incorrect breakpoint type") {
                var breakpoint = validBreakpoint
                breakpoint["type"] = invalid
                try expectBreakpointError(breakpoint, .unknownBreakpointType(invalid))
            }

            $0.it("fails with incorrect breakpoint scope") {
                var target = validBreakpoint
                target["scope"] = invalid
                try expectBreakpointError(target, .unknownBreakpointScope(invalid))
            }

            $0.it("fails with incorrect breakpoint stop on style") {
                var target = validBreakpoint
                target["stopOnStyle"] = invalid
                try expectBreakpointError(target, .unknownBreakpointStopOnStyle(invalid))
            }

            $0.it("fails with incorrect breakpoint action type") {
                var breakpoint = validBreakpoint
                breakpoint["actions"] = [["type": invalid]]
                try expectBreakpointError(breakpoint, .unknownBreakpointActionType(invalid))
            }

            $0.it("fails with incorrect breakpoint action conveyance type") {
                var breakpoint = validBreakpoint
                breakpoint["actions"] = [["type": "Log", "conveyanceType": invalid]]
                try expectBreakpointError(breakpoint, .unknownBreakpointActionConveyanceType(invalid))
            }

            $0.it("fails with incorrect breakpoint action sound name") {
                var breakpoint = validBreakpoint
                breakpoint["actions"] = [["type": "Sound", "sound": invalid]]
                try expectBreakpointError(breakpoint, .unknownBreakpointActionSoundName(invalid))
            }

            $0.it("parses breakpoints") {
                let breakpointDictionaries = [
                    ["type": "File", "path": "Foo.swift", "line": 7, "column": 14, "condition": "bar == nil"],
                    ["type": "Exception", "scope": "All", "stopOnStyle": "Catch"],
                    ["type": "SwiftError", "enabled": false],
                    ["type": "OpenGLError", "ignoreCount": 2],
                    ["type": "Symbolic", "symbol": "UIViewAlertForUnsatisfiableConstraints", "module": "UIKitCore"],
                    ["type": "IDEConstraintError", "continueAfterRunningActions": true],
                    ["type": "IDETestFailure"],
                ]

                let project = try getProjectSpec(["breakpoints": breakpointDictionaries])

                let expectedBreakpoints = [
                    Breakpoint(type: .file(path: "Foo.swift", line: 7, column: 14), condition: "bar == nil"),
                    Breakpoint(type: .exception(.init(scope: .all, stopOnStyle: .catch))),
                    Breakpoint(type: .swiftError, enabled: false),
                    Breakpoint(type: .openGLError, ignoreCount: 2),
                    Breakpoint(type: .symbolic(symbol: "UIViewAlertForUnsatisfiableConstraints", module: "UIKitCore")),
                    Breakpoint(type: .ideConstraintError, continueAfterRunningActions: true),
                    Breakpoint(type: .ideTestFailure),
                ]

                try expect(project.breakpoints) == expectedBreakpoints
            }

            $0.it("parses breakpoint actions") {
                var breakpointDicationary = validBreakpoint
                breakpointDicationary["actions"] = [
                    ["type": "DebuggerCommand", "command": "po $arg1"],
                    ["type": "Log", "message": "message", "conveyanceType": "speak"],
                    ["type": "ShellCommand", "path": "script.sh", "arguments": "argument1, argument2", "waitUntilDone": true],
                    ["type": "GraphicsTrace"],
                    ["type": "AppleScript", "script": #"display alert "Hello!""#],
                    ["type": "Sound", "sound": "Hero"],
                ]

                let breakpoint = try Breakpoint(jsonDictionary: breakpointDicationary)

                let expectedActions: [Breakpoint.Action] = [
                    .debuggerCommand("po $arg1"),
                    .log(.init(message: "message", conveyanceType: .speak)),
                    .shellCommand(path: "script.sh", arguments: "argument1, argument2", waitUntilDone: true),
                    .graphicsTrace,
                    .appleScript(#"display alert "Hello!""#),
                    .sound(.hero),
                ]

                try expect(breakpoint.actions) == expectedActions
            }

            $0.it("parses sources") {
                var targetDictionary1 = validTarget
                targetDictionary1["sources"] = [
                    "sourceString",
                    ["path": "sourceObject"],
                    ["path": "sourceWithFlagsArray", "compilerFlags": ["-Werror"]],
                    ["path": "sourceWithFlagsString", "compilerFlags": "-Werror -Wextra"],
                    ["path": "sourceWithExcludes", "excludes": ["Foo.swift"]],
                    ["path": "sourceWithFileType", "type": "file"],
                    ["path": "sourceWithGroupType", "type": "group"],
                    ["path": "sourceWithFolderType", "type": "folder"],
                    ["path": "sourceWithResourceTags", "resourceTags": ["tag1", "tag2"]],
                ]
                var targetDictionary2 = validTarget
                targetDictionary2["sources"] = "source3"

                let target1 = try Target(name: "test", jsonDictionary: targetDictionary1)
                let target2 = try Target(name: "test", jsonDictionary: targetDictionary2)

                let target1SourcesExpect = [
                    TargetSource(path: "sourceString"),
                    TargetSource(path: "sourceObject"),
                    TargetSource(path: "sourceWithFlagsArray", compilerFlags: ["-Werror"]),
                    TargetSource(path: "sourceWithFlagsString", compilerFlags: ["-Werror", "-Wextra"]),
                    TargetSource(path: "sourceWithExcludes", excludes: ["Foo.swift"]),
                    TargetSource(path: "sourceWithFileType", type: .file),
                    TargetSource(path: "sourceWithGroupType", type: .group),
                    TargetSource(path: "sourceWithFolderType", type: .folder),
                    TargetSource(path: "sourceWithResourceTags", resourceTags: ["tag1", "tag2"]),
                ]

                try expect(target1.sources) == target1SourcesExpect
                try expect(target2.sources) == ["source3"]
            }

            $0.it("parses target dependencies") {
                var targetDictionary = validTarget
                targetDictionary["dependencies"] = [
                    ["target": "name", "embed": false, "platformFilter": "all"],
                    ["target": "project/name", "embed": false, "platformFilter": "macOS"],
                    ["carthage": "name", "findFrameworks": true, "platformFilter": "iOS"],
                    ["carthage": "name", "findFrameworks": true, "linkType": "static"],
                    ["framework": "path", "weak": true],
                    ["sdk": "Contacts.framework"],
                    [
                        "sdk": "Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework",
                        "root": "DEVELOPER_DIR",
                    ],
                    ["target": "conditionalMatch", "platforms": ["iOS"]],
                    ["target": "conditionalMiss", "platforms": ["watchOS"]],
                ]
                let target = try Target(name: "test", jsonDictionary: targetDictionary)
                try expect(target.dependencies.count) == 8
                try expect(target.dependencies[0]) == Dependency(type: .target, reference: "name", embed: false, platformFilter: .all)
                try expect(target.dependencies[1]) == Dependency(type: .target, reference: "project/name", embed: false, platformFilter: .macOS)
                try expect(target.dependencies[2]) == Dependency(type: .carthage(findFrameworks: true, linkType: .dynamic), reference: "name", platformFilter: .iOS)
                try expect(target.dependencies[3]) == Dependency(type: .carthage(findFrameworks: true, linkType: .static), reference: "name")
                try expect(target.dependencies[4]) == Dependency(type: .framework, reference: "path", weakLink: true)
                try expect(target.dependencies[5]) == Dependency(type: .sdk(root: nil), reference: "Contacts.framework")
                try expect(target.dependencies[6]) == Dependency(type: .sdk(root: "DEVELOPER_DIR"), reference: "Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework")
                try expect(target.dependencies[7]) == Dependency(type: .target, reference: "conditionalMatch", platforms: [.iOS])
            }

            $0.it("parses info plist") {
                var targetDictionary = validTarget
                targetDictionary["info"] = [
                    "path": "Info.plist",
                    "properties": [
                        "CFBundleName": "MyAppName",
                        "UIBackgroundModes": ["fetch"],
                    ],
                ]

                let target = try Target(name: "", jsonDictionary: targetDictionary)
                try expect(target.info) == Plist(path: "Info.plist", attributes: [
                    "CFBundleName": "MyAppName",
                    "UIBackgroundModes": ["fetch"],
                ])
            }

            $0.it("parses entitlement plist") {
                var targetDictionary = validTarget
                targetDictionary["entitlements"] = [
                    "path": "app.entitlements",
                    "properties": [
                        "com.apple.security.application-groups": "com.group",
                    ],
                ]

                let target = try Target(name: "", jsonDictionary: targetDictionary)
                try expect(target.entitlements) == Plist(path: "app.entitlements", attributes: [
                    "com.apple.security.application-groups": "com.group",
                ])
            }

            $0.it("parses cross platform targets") {
                let targetDictionary: [String: Any] = [
                    "platform": ["iOS", "tvOS"],
                    "deploymentTarget": ["iOS": 9.0, "tvOS": "10.0"],
                    "type": "framework",
                    "sources": ["Framework", "Framework ${platform}"],
                    "settings": ["SETTING": "value_${platform}"],
                ]

                let project = try getProjectSpec(["targets": ["Framework": targetDictionary]])
                var target_iOS = Target(name: "Framework_iOS", type: .framework, platform: .iOS)
                var target_tvOS = Target(name: "Framework_tvOS", type: .framework, platform: .tvOS)

                target_iOS.sources = ["Framework", "Framework iOS"]
                target_tvOS.sources = ["Framework", "Framework tvOS"]
                target_iOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_iOS"]
                target_tvOS.settings = ["PRODUCT_NAME": "Framework", "SETTING": "value_tvOS"]
                target_iOS.deploymentTarget = Version(major: 9, minor: 0, patch: 0)
                target_tvOS.deploymentTarget = Version(major: 10, minor: 0, patch: 0)

                try expect(project.targets) == [target_iOS, target_tvOS]
            }
            
            $0.it("parses no platform fallbacks to auto if we are using supported destinations") {
                let targetDictionary: [String: Any] = [
                    "type": "framework",
                    "supportedDestinations": ["iOS", "tvOS"]
                ]

                let project = try getProjectSpec(["targets": ["Framework": targetDictionary]])
                let target = Target(name: "Framework", type: .framework, platform: .auto)
                
                try expect(project.targets) == [target]
            }
            
            $0.it("parses no platform fails if we are not using supported destinations") {
                let expectedError = SpecParsingError.unknownTargetPlatform("")
                
                let projectDictionary: [String: Any] = [
                    "name": "test",
                    "targets": ["target1": [
                        "type": "application"
                    ] as [String : Any]]
                ]
                
                try expectError(expectedError) {
                    _ = try Project(jsonDictionary: projectDictionary)
                }
            }
            
            $0.it("parses supported destinations with macCatalyst but not iOS, we add iOS") {
                let targetDictionary: [String: Any] = [
                    "type": "framework",
                    "supportedDestinations": ["macCatalyst"]
                ]
                
                let project = try getProjectSpec(["targets": ["Framework": targetDictionary]])
                let target = Target(name: "Framework", type: .framework, platform: .auto)
                
                try expect(project.targets) == [target]
                try expect(project.targets.first?.supportedDestinations) == [.macCatalyst, .iOS]
            }
            
            $0.it("invalid target platform because platform is an array and supported destinations is in use") {
                let expectedError = SpecParsingError.invalidTargetPlatformAsArray
                
                let projectDictionary: [String: Any] = [
                    "name": "test",
                    "targets": ["target1": [
                        "type": "application",
                        "platform": ["iOS", "tvOS"],
                        "supportedDestinations": ["iOS", "tvOS"]
                    ] as [String : Any]]
                ]
                
                try expectError(expectedError) {
                    _ = try Project(jsonDictionary: projectDictionary)
                }
            }
            
            $0.it("parses target templates") {

                let targetDictionary: [String: Any] = [
                    "deploymentTarget": "1.2.0",
                    "sources": ["targetSource"],
                    "templates": ["temp2", "temp"],
                    "templateAttributes": [
                        "source": "replacedSource",
                    ],
                ]

                let project = try getProjectSpec([
                    "targets": ["Framework": targetDictionary],
                    "targetTemplates": [
                        "temp": [
                            "platform": "iOS",
                            "sources": [
                                "templateSource",
                                ["path": "Sources/${target_name}"],
                            ],
                        ],
                        "temp2": [
                            "type": "framework",
                            "platform": "tvOS",
                            "deploymentTarget": "1.1.0",
                            "configFiles": [
                                "debug": "Configs/${target_name}/debug.xcconfig",
                                "release": "Configs/${target_name}/release.xcconfig",
                            ],
                            "sources": ["${source}"],
                        ],
                    ],
                ])

                let target = project.targets.first!
                try expect(target.type) == .framework // uses value
                try expect(target.platform) == .iOS // uses latest value
                try expect(target.deploymentTarget) == Version("1.2.0") // keeps value
                try expect(target.sources) == ["replacedSource", "templateSource", "Sources/Framework", "targetSource"] // merges array in order and replace ${target_name}
                try expect(target.configFiles["debug"]) == "Configs/Framework/debug.xcconfig" // replaces $target_name
                try expect(target.configFiles["release"]) == "Configs/Framework/release.xcconfig" // replaces ${target_name}
            }

            $0.it("parses nested target templates") {

                let targetDictionary: [String: Any] = [
                    "deploymentTarget": "1.2.0",
                    "sources": ["targetSource"],
                    "templates": ["temp2"],
                ]

                let project = try getProjectSpec([
                    "targets": ["Framework": targetDictionary],
                    "targetTemplates": [
                        "temp": [
                            "type": "framework",
                            "platform": "iOS",
                            "sources": ["nestedTemplateSource1"],
                        ],
                        "temp1": [
                            "type": "application",
                            "sources": ["nestedTemplateSource2"],
                        ],
                        "temp2": [
                            "platform": "tvOS",
                            "deploymentTarget": "1.1.0",
                            "configFiles": ["debug": "Configs/${target_name}/debug.xcconfig"],
                            "templates": ["temp", "temp1"],
                            "sources": ["templateSource"],
                        ],
                    ],
                ])

                let target = project.targets.first!
                try expect(target.type) == .application // uses value of last nested template
                try expect(target.platform) == .tvOS // uses latest value
                try expect(target.deploymentTarget) == Version("1.2.0") // keeps value
                try expect(target.sources) == ["nestedTemplateSource1", "nestedTemplateSource2", "templateSource", "targetSource"] // merges array in order
                try expect(target.configFiles["debug"]) == "Configs/Framework/debug.xcconfig" // replaces $target_name
            }

            $0.it("parses complex nested target templates") {

                let targetDictionary: [String: Any] = [
                    "type": "framework",
                    "platform": "iOS",
                    "templates": ["temp"],
                    "sources": ["target"],
                    "templateAttributes": [
                        "temp": "temp-by-target",
                        "a": "a-by-target",
                        "b": "b-by-target", // This should win over attributes defined in template "temp"
                    ],
                ]

                let project = try getProjectSpec([
                    "targets": ["Framework": targetDictionary],
                    "targetTemplates": [
                        "temp": [
                            "templates": ["a", "d"],
                            "sources": ["temp", "${temp}"],
                            "templateAttributes": [
                                "b": "b-by-temp",
                                "c": "c-by-temp",
                                "d": "d-by-temp",
                            ],
                        ],
                        "a": [
                            "templates": ["b", "c"],
                            "sources": ["a", "${a}"],
                            "templateAttributes": [
                                "c": "c-by-a",
                            ],
                        ],
                        "b": [
                            "sources": ["b", "${b}"],
                        ],
                        "c": [
                            "sources": ["c", "${c}"],
                        ],
                        "d": [
                            "sources": ["d", "${d}"],
                            "templates": ["e"],
                            "templateAttributes": [
                                "e": "e-by-d",
                            ],
                        ],
                        "e": [
                            "sources": ["e", "${e}"],
                        ],

                    ],
                ])

                let target = project.targets.first!
                try expect(target.type) == .framework // uses value of last nested template
                try expect(target.platform) == .iOS // uses latest value
                try expect(target.sources) == ["b", "b-by-target",
                                               "c", "c-by-temp",
                                               "a", "a-by-target",
                                               "e", "e-by-d",
                                               "d", "d-by-temp",
                                               "temp", "temp-by-target",
                                               "target"] // merges array in order
            }

            $0.it("parses nested target templates with cycle") {

                let targetDictionary: [String: Any] = [
                    "deploymentTarget": "1.2.0",
                    "sources": ["targetSource"],
                    "templates": ["temp2"],
                ]

                let project = try getProjectSpec([
                    "targets": ["Framework": targetDictionary],
                    "targetTemplates": [
                        "temp": [
                            "type": "framework",
                            "platform": "iOS",
                            "templates": ["temp1"],
                            "sources": ["nestedTemplateSource1"],
                        ],
                        "temp1": [
                            "platform": "macOS",
                            "templates": ["temp2"],
                            "sources": ["nestedTemplateSource2"],
                        ],
                        "temp2": [
                            "platform": "tvOS",
                            "deploymentTarget": "1.1.0",
                            "configFiles": ["debug": "Configs/${target_name}/debug.xcconfig"],
                            "templates": ["temp", "temp1"],
                            "sources": ["templateSource"],
                        ],
                    ],
                ])

                let target = project.targets.first!
                try expect(target.type) == .framework // uses value
                try expect(target.platform) == .tvOS // uses latest value
                try expect(target.deploymentTarget) == Version("1.2.0") // keeps value
                try expect(target.sources) == ["nestedTemplateSource2", "nestedTemplateSource1", "templateSource", "targetSource"] // merges array in order
                try expect(target.configFiles["debug"]) == "Configs/Framework/debug.xcconfig" // replaces $target_name
            }

            $0.it("parses cross platform target templates") {

                let project = try getProjectSpec([
                    "targets": [
                        "Framework": [
                            "type": "framework",
                            "templates": ["temp"],
                        ],
                    ],
                    "targetTemplates": [
                        "temp": [
                            "platform": ["iOS", "tvOS"],
                        ],
                    ],
                ])

                let iOSTarget = project.targets.first { $0.platform == .iOS }
                let tvOSTarget = project.targets.first { $0.platform == .tvOS }
                try expect(iOSTarget?.type) == .framework
                try expect(tvOSTarget?.type) == .framework
            }

            $0.it("parses platform specific templates") {

                let project = try getProjectSpec([
                    "targets": [
                        "Framework": [
                            "type": "framework",
                            "platform": ["iOS", "tvOS"],
                            "templates": ["${platform}"],
                        ],
                    ],
                    "targetTemplates": [
                        "iOS": [
                            "sources": "A",
                        ],
                        "tvOS": [
                            "sources": "B",
                        ],
                    ],
                ])

                let iOSTarget = project.targets.first { $0.platform == .iOS }
                let tvOSTarget = project.targets.first { $0.platform == .tvOS }
                try expect(iOSTarget?.sources) == ["A"]
                try expect(tvOSTarget?.sources) == ["B"]
            }

            $0.it("parses aggregate targets") {
                let dictionary: [String: Any] = [
                    "targets": ["target_1", "target_2"],
                    "settings": ["SETTING": "VALUE"],
                    "configFiles": ["debug": "file.xcconfig"],
                ]

                let project = try getProjectSpec(["aggregateTargets": ["AggregateTarget": dictionary]])
                let expectedTarget = AggregateTarget(name: "AggregateTarget", targets: ["target_1", "target_2"], settings: ["SETTING": "VALUE"], configFiles: ["debug": "file.xcconfig"])
                try expect(project.aggregateTargets) == [expectedTarget]
            }

            $0.it("parses target schemes") {
                var targetDictionary = validTarget
                targetDictionary["scheme"] = [
                    "testTargets": ["t1", ["name": "t2"]],
                    "configVariants": ["dev", "app-store"],
                    "commandLineArguments": [
                        "ENV1": true,
                    ],
                    "gatherCoverageData": true,
                    "coverageTargets": ["t1"],
                    "storeKitConfiguration": "Configuration.storekit",
                    "language": "en",
                    "region": "US",
                    "disableMainThreadChecker": true,
                    "stopOnEveryMainThreadCheckerIssue": true,
                    "disableThreadPerformanceChecker": true,
                    "environmentVariables": [
                        "TEST_VAR": "TEST_VAL",
                    ],
                    "preActions": [
                        [
                            "script": "dothing",
                            "name": "Do Thing",
                            "settingsTarget": "test",
                        ],
                    ],
                    "postActions": [
                        [
                            "script": "hello",
                        ],
                    ],
                    "management": [
                        "shared": false,
                        "isShown": true,
                        "orderHint": 10
                    ],
                ]

                let target = try Target(name: "test", jsonDictionary: targetDictionary)

                let scheme = TargetScheme(
                    testTargets: ["t1", "t2"],
                    configVariants: ["dev", "app-store"],
                    gatherCoverageData: true,
                    coverageTargets: ["t1"],
                    storeKitConfiguration: "Configuration.storekit",
                    language: "en",
                    region: "US",
                    disableMainThreadChecker: true,
                    stopOnEveryMainThreadCheckerIssue: true,
                    disableThreadPerformanceChecker: true,
                    commandLineArguments: ["ENV1": true],
                    environmentVariables: [XCScheme.EnvironmentVariable(variable: "TEST_VAR", value: "TEST_VAL", enabled: true)],
                    preActions: [.init(name: "Do Thing", script: "dothing", settingsTarget: "test")],
                    postActions: [.init(name: "Run Script", script: "hello")],
                    management: Scheme.Management(shared: false, orderHint: 10, isShown: true)
                )

                try expect(target.scheme) == scheme
            }

            $0.it("parses schemes") {
                let schemeDictionary: [String: Any] = [
                    "build": [
                        "parallelizeBuild": false,
                        "buildImplicitDependencies": false,
                        "runPostActionsOnFailure": true,
                        "targets": [
                            "Target1": "all",
                            "Target2": "testing",
                            "Target3": "none",
                            "Target4": ["testing": true],
                            "Target5": ["testing": false],
                            "Target6": ["test", "analyze"],
                            "ExternalProject/Target7": ["run"],
                        ],
                        "preActions": [
                            [
                                "script": "echo Before Build",
                                "name": "Before Build",
                                "settingsTarget": "Target1",
                            ],
                        ],
                    ],
                    "run": [
                        "config": "debug",
                        "launchAutomaticallySubstyle": 2,
                        "enableGPUFrameCaptureMode": "disabled",
                        "storeKitConfiguration": "Configuration.storekit",
                        "disableThreadPerformanceChecker": true,
                    ],
                    "test": [
                        "config": "debug",
                        "targets": [
                            "Target1",
                            [
                                "name": "ExternalProject/Target2",
                                "parallelizable": true,
                                "skipped": true,
                                "location": "test.gpx",
                                "randomExecutionOrder": true,
                                "skippedTests": ["Test/testExample()"],
                            ],
                        ],
                        "gatherCoverageData": true,
                        "disableMainThreadChecker": true,
                        "stopOnEveryMainThreadCheckerIssue": true,
                        "testPlans": [
                            [
                                "path": "Path/Plan.xctestplan"
                            ],
                            [
                                "path": "Path/Plan2.xctestplan"
                            ]
                        ],
                        "preferredScreenCaptureFormat": "screenshots",
                    ],
                    "management": [
                        "isShown": false,
                        "orderHint": 4
                    ],
                ]
                let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)
                let expectedTargets: [Scheme.BuildTarget] = [
                    Scheme.BuildTarget(target: "Target1", buildTypes: BuildType.all),
                    Scheme.BuildTarget(target: "Target2", buildTypes: [.testing, .analyzing]),
                    Scheme.BuildTarget(target: "Target3", buildTypes: []),
                    Scheme.BuildTarget(target: "Target4", buildTypes: [.testing]),
                    Scheme.BuildTarget(target: "Target5", buildTypes: []),
                    Scheme.BuildTarget(target: "Target6", buildTypes: [.testing, .analyzing]),
                    Scheme.BuildTarget(target: "ExternalProject/Target7", buildTypes: [.running]),
                ]
                try expect(scheme.name) == "Scheme"
                try expect(scheme.build.targets) == expectedTargets
                try expect(scheme.build.preActions.first?.script) == "echo Before Build"
                try expect(scheme.build.preActions.first?.name) == "Before Build"
                try expect(scheme.build.preActions.first?.settingsTarget) == "Target1"

                try expect(scheme.build.parallelizeBuild) == false
                try expect(scheme.build.buildImplicitDependencies) == false
                try expect(scheme.build.runPostActionsOnFailure) == true

                let expectedRun = Scheme.Run(
                    config: "debug",
                    enableGPUFrameCaptureMode: .disabled,
                    disableThreadPerformanceChecker: true,
                    launchAutomaticallySubstyle: "2",
                    storeKitConfiguration: "Configuration.storekit"
                )
                try expect(scheme.run) == expectedRun

                let expectedTest = Scheme.Test(
                    config: "debug",
                    gatherCoverageData: true,
                    disableMainThreadChecker: true,
                    targets: [
                        "Target1",
                        Scheme.Test.TestTarget(
                            targetReference: "ExternalProject/Target2",
                            randomExecutionOrder: true,
                            parallelizable: true,
                            location: "test.gpx",
                            skipped: true,
                            skippedTests: ["Test/testExample()"]
                        ),
                    ],
                    testPlans: [
                        .init(path: "Path/Plan.xctestplan"),
                        .init(path: "Path/Plan2.xctestplan")
                    ],
                    preferredScreenCaptureFormat: .screenshots
                )
                try expect(scheme.test) == expectedTest

                let expectedManagement = Scheme.Management(shared: true, orderHint: 4, isShown: false)
                try expect(scheme.management) == expectedManagement
            }

            $0.it("parses alternate test schemes") {
                let schemeDictionary: [String: Any] = [
                    "build": [
                        "targets": ["Target1": "all"],
                    ],
                    "test": [
                        "config": "debug",
                        "targets": [
                            "Target1",
                            [
                                "name": "ExternalProject/Target2",
                                "parallelizable": true,
                                "location": "New York, NY, USA",
                                "randomExecutionOrder": true,
                                "selectedTests": ["Test/testExample()"],
                            ],
                        ],
                        "gatherCoverageData": true,
                        "disableMainThreadChecker": true,
                        "stopOnEveryMainThreadCheckerIssue": true,
                    ],
                    "management": [
                        "isShown": false
                    ],
                ]
                let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)

                let expectedTest = Scheme.Test(
                    config: "debug",
                    gatherCoverageData: true,
                    disableMainThreadChecker: true,
                    targets: [
                        "Target1",
                        Scheme.Test.TestTarget(
                            targetReference: "ExternalProject/Target2",
                            randomExecutionOrder: true,
                            parallelizable: true,
                            location: "New York, NY, USA",
                            selectedTests: ["Test/testExample()"]
                        ),
                    ]
                )
                try expect(scheme.test) == expectedTest

                let expectedManagement = Scheme.Management(shared: true, orderHint: nil, isShown: false)
                try expect(scheme.management) == expectedManagement
            }

            $0.it("parses schemes variables") {
                let schemeDictionary: [String: Any] = [
                    "build": [
                        "targets": ["Target1": "all"],
                    ],
                    "run": [
                        "environmentVariables": [
                            ["variable": "BOOL_TRUE", "value": true],
                            ["variable": "BOOL_YES", "value": "YES"],
                            ["variable": "ENVIRONMENT", "value": "VARIABLE"],
                            ["variable": "OTHER_ENV_VAR", "value": "VAL", "isEnabled": false],
                        ],
                        "launchAutomaticallySubstyle": "2",
                        "storeKitConfiguration": "Configuration.storekit",
                    ],
                    "test": [
                        "environmentVariables": [
                            "BOOL_TRUE": true,
                            "BOOL_YES": "YES",
                            "TEST": "VARIABLE",
                        ],
                    ],
                    "profile": [
                        "config": "Release",
                    ],
                ]

                let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)

                let expectedRunVariables = [
                    XCScheme.EnvironmentVariable(variable: "BOOL_TRUE", value: "YES", enabled: true),
                    XCScheme.EnvironmentVariable(variable: "BOOL_YES", value: "YES", enabled: true),
                    XCScheme.EnvironmentVariable(variable: "ENVIRONMENT", value: "VARIABLE", enabled: true),
                    XCScheme.EnvironmentVariable(variable: "OTHER_ENV_VAR", value: "VAL", enabled: false),
                ]

                let expectedTestVariables = [
                    XCScheme.EnvironmentVariable(variable: "BOOL_TRUE", value: "YES", enabled: true),
                    XCScheme.EnvironmentVariable(variable: "BOOL_YES", value: "YES", enabled: true),
                    XCScheme.EnvironmentVariable(variable: "TEST", value: "VARIABLE", enabled: true),
                ]

                try expect(scheme.run?.environmentVariables) == expectedRunVariables
                try expect(scheme.run?.launchAutomaticallySubstyle) == "2"
                try expect(scheme.run?.storeKitConfiguration) == "Configuration.storekit"
                try expect(scheme.test?.environmentVariables) == expectedTestVariables
                try expect(scheme.profile?.config) == "Release"
                try expect(scheme.profile?.environmentVariables.isEmpty) == true
            }

            $0.it("parses alternate schemes variables") {
                let schemeDictionary: [String: Any] = [
                    "build": [
                        "targets": ["Target1": "all"],
                    ],
                    "run": [
                        "launchAutomaticallySubstyle": 2, // Both integer and string supported
                        "storeKitConfiguration": "Configuration.storekit",
                    ],
                ]

                let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)
                try expect(scheme.run?.launchAutomaticallySubstyle) == "2"
                try expect(scheme.run?.storeKitConfiguration) == "Configuration.storekit"
            }

            $0.it("parses scheme templates") {
                let targetDictionary: [String: Any] = [
                    "deploymentTarget": "1.2.0",
                    "sources": ["targetSource"],
                    "templates": ["temp2", "temp"],
                    "templateAttributes": [
                        "source": "replacedSource",
                    ],
                ]

                let project = try getProjectSpec([
                    "targets": ["Framework": targetDictionary],
                    "targetTemplates": [
                        "temp": [
                            "platform": "iOS",
                            "sources": [
                                "templateSource",
                                ["path": "Sources/${target_name}"],
                            ],
                        ],
                        "temp2": [
                            "type": "framework",
                            "platform": "tvOS",
                            "deploymentTarget": "1.1.0",
                            "configFiles": [
                                "debug": "Configs/${target_name}/debug.xcconfig",
                                "release": "Configs/${target_name}/release.xcconfig",
                            ],
                            "sources": ["${source}"],
                        ],
                    ],
                    "schemeTemplates": [
                        "base_scheme": [
                            "build": [
                                "parallelizeBuild": false,
                                "buildImplicitDependencies": false,
                                "runPostActionsOnFailure": true,
                                "targets": [
                                    "Target${name_1}": "all",
                                    "Target2": "testing",
                                    "Target${name_3}": "none",
                                    "Target4": ["testing": true],
                                    "Target5": ["testing": false],
                                    "Target6": ["test", "analyze"],
                                ],
                                "preActions": [
                                    [
                                        "script": "${pre-action-name}",
                                        "name": "Before Build ${scheme_name}",
                                        "settingsTarget": "Target${name_1}",
                                    ],
                                ],
                            ],
                            "run": [
                                "storeKitConfiguration": "Configuration.storekit",
                            ],
                            "test": [
                                "config": "debug",
                                "targets": [
                                    "Target${name_1}",
                                    [
                                        "name": "Target2",
                                        "parallelizable": true,
                                        "randomExecutionOrder": true,
                                        "skippedTests": ["Test/testExample()"],
                                    ],
                                ],
                                "gatherCoverageData": true,
                                "disableMainThreadChecker": true,
                                "stopOnEveryMainThreadCheckerIssue": false,
                            ],
                            "management": [
                                "shared": false,
                                "orderHint": 8
                            ],
                        ],
                    ],
                    "schemes": [
                        "temp2": [
                            "templates": ["base_scheme"],
                            "templateAttributes": [
                                "pre-action-name": "modified-name",
                                "name_1": "FirstTarget",
                                "name_3": "ThirdTarget",
                            ],
                        ],
                    ],
                ])

                let scheme = project.schemes.first!
                let expectedTargets: [Scheme.BuildTarget] = [
                    Scheme.BuildTarget(target: "TargetFirstTarget", buildTypes: BuildType.all),
                    Scheme.BuildTarget(target: "Target2", buildTypes: [.testing, .analyzing]),
                    Scheme.BuildTarget(target: "TargetThirdTarget", buildTypes: []),
                    Scheme.BuildTarget(target: "Target4", buildTypes: [.testing]),
                    Scheme.BuildTarget(target: "Target5", buildTypes: []),
                    Scheme.BuildTarget(target: "Target6", buildTypes: [.testing, .analyzing]),
                ]
                try expect(scheme.name) == "temp2"
                try expect(Set(scheme.build.targets)) == Set(expectedTargets)
                try expect(scheme.build.preActions.first?.script) == "modified-name"
                try expect(scheme.build.preActions.first?.name) == "Before Build temp2"
                try expect(scheme.build.preActions.first?.settingsTarget) == "TargetFirstTarget"

                try expect(scheme.build.parallelizeBuild) == false
                try expect(scheme.build.buildImplicitDependencies) == false
                try expect(scheme.build.runPostActionsOnFailure) == true

                try expect(scheme.run?.storeKitConfiguration) == "Configuration.storekit"

                let expectedTest = Scheme.Test(
                    config: "debug",
                    gatherCoverageData: true,
                    disableMainThreadChecker: true,
                    targets: [
                        "TargetFirstTarget",
                        Scheme.Test.TestTarget(
                            targetReference: "Target2",
                            randomExecutionOrder: true,
                            parallelizable: true,
                            skippedTests: ["Test/testExample()"]
                        ),
                    ]
                )
                try expect(scheme.test) == expectedTest

                let expectedManagement = Scheme.Management(shared: false, orderHint: 8, isShown: nil)
                try expect(scheme.management) == expectedManagement
            }

            $0.it("parses copy files on install") {
                var targetSource = validTarget
                targetSource["onlyCopyFilesOnInstall"] = true
                let target = try Target(name: "Embed Frameworks", jsonDictionary: targetSource)
                try expect(target.onlyCopyFilesOnInstall) == true
            }

            $0.it("parses put resources before Sources Build Phase") {
                var targetSource = validTarget
                targetSource["putResourcesBeforeSourcesBuildPhase"] = true
                let target = try Target(name: "Embed Frameworks", jsonDictionary: targetSource)
                try expect(target.putResourcesBeforeSourcesBuildPhase) == true
            }

            $0.it("parses settings") {
                let project = try Project(path: fixturePath + "settings_test.yml")
                let buildSettings: BuildSettings = ["SETTING": "value"]
                let configSettings: [String: Settings] = ["config1": Settings(buildSettings: ["SETTING1": "value"])]
                let groups = ["preset1"]

                let preset1 = Settings(buildSettings: buildSettings, configSettings: [:], groups: [])
                let preset2 = Settings(buildSettings: [:], configSettings: configSettings, groups: [])
                let preset3 = Settings(buildSettings: buildSettings, configSettings: configSettings, groups: [])
                let preset4 = Settings(buildSettings: buildSettings, configSettings: [:], groups: [])
                let preset5 = Settings(buildSettings: buildSettings, configSettings: [:], groups: groups)
                let preset6 = Settings(buildSettings: buildSettings, configSettings: configSettings, groups: groups)
                let preset7 = Settings(buildSettings: buildSettings, configSettings: ["config1": Settings(buildSettings: buildSettings, groups: groups)])
                let preset8 = Settings(buildSettings: [:], configSettings: ["config1": Settings(configSettings: configSettings)])

                try expect(project.settingGroups.count) == 8
                try expect(project.settingGroups["preset1"]) == preset1
                try expect(project.settingGroups["preset2"]) == preset2
                try expect(project.settingGroups["preset3"]) == preset3
                try expect(project.settingGroups["preset4"]) == preset4
                try expect(project.settingGroups["preset5"]) == preset5
                try expect(project.settingGroups["preset6"]) == preset6
                try expect(project.settingGroups["preset7"]) == preset7
                try expect(project.settingGroups["preset8"]) == preset8
            }

            $0.it("parses run scripts") {
                var target = validTarget
                let scripts: [[String: Any]] = [
                    ["path": "script.sh"],
                    ["script": "shell script\ndo thing", "name": "myscript", "inputFiles": ["file", "file2"], "outputFiles": ["file", "file2"], "shell": "bin/customshell", "runOnlyWhenInstalling": true],
                    ["script": "shell script\ndo thing", "name": "myscript", "inputFiles": ["file", "file2"], "outputFiles": ["file", "file2"], "shell": "bin/customshell", "showEnvVars": false],
                    ["script": "shell script\ndo thing", "name": "myscript", "inputFiles": ["file", "file2"], "outputFiles": ["file", "file2"], "shell": "bin/customshell", "basedOnDependencyAnalysis": false],
                    ["script": "shell script\nwith file lists", "name": "myscript", "inputFileLists": ["inputList.xcfilelist"], "outputFileLists": ["outputList.xcfilelist"], "shell": "bin/customshell", "runOnlyWhenInstalling": true],
                    ["script": "shell script\nwith file lists", "name": "myscript", "inputFileLists": ["inputList.xcfilelist"], "outputFileLists": ["outputList.xcfilelist"], "shell": "bin/customshell", "showEnvVars": false],
                    ["script": "shell script\nwith file lists", "name": "myscript", "inputFileLists": ["inputList.xcfilelist"], "outputFileLists": ["outputList.xcfilelist"], "shell": "bin/customshell", "basedOnDependencyAnalysis": false],
                ]
                target["preBuildScripts"] = scripts
                target["postCompileScripts"] = scripts
                target["postBuildScripts"] = scripts

                let expectedScripts = [
                    BuildScript(script: .path("script.sh")),
                    BuildScript(script: .script("shell script\ndo thing"), name: "myscript", inputFiles: ["file", "file2"], outputFiles: ["file", "file2"], shell: "bin/customshell", runOnlyWhenInstalling: true, showEnvVars: true, basedOnDependencyAnalysis: true),
                    BuildScript(script: .script("shell script\ndo thing"), name: "myscript", inputFiles: ["file", "file2"], outputFiles: ["file", "file2"], shell: "bin/customshell", runOnlyWhenInstalling: false, showEnvVars: false, basedOnDependencyAnalysis: true),
                    BuildScript(script: .script("shell script\ndo thing"), name: "myscript", inputFiles: ["file", "file2"], outputFiles: ["file", "file2"], shell: "bin/customshell", runOnlyWhenInstalling: false, showEnvVars: true, basedOnDependencyAnalysis: false),
                    BuildScript(script: .script("shell script\nwith file lists"), name: "myscript", inputFileLists: ["inputList.xcfilelist"], outputFileLists: ["outputList.xcfilelist"], shell: "bin/customshell", runOnlyWhenInstalling: true, showEnvVars: true, basedOnDependencyAnalysis: true),
                    BuildScript(script: .script("shell script\nwith file lists"), name: "myscript", inputFileLists: ["inputList.xcfilelist"], outputFileLists: ["outputList.xcfilelist"], shell: "bin/customshell", runOnlyWhenInstalling: false, showEnvVars: false, basedOnDependencyAnalysis: true),
                    BuildScript(script: .script("shell script\nwith file lists"), name: "myscript", inputFileLists: ["inputList.xcfilelist"], outputFileLists: ["outputList.xcfilelist"], shell: "bin/customshell", runOnlyWhenInstalling: false, showEnvVars: true, basedOnDependencyAnalysis: false),
                ]

                let parsedTarget = try Target(name: "test", jsonDictionary: target)
                try expect(parsedTarget.preBuildScripts) == expectedScripts
                try expect(parsedTarget.postCompileScripts) == expectedScripts
                try expect(parsedTarget.postBuildScripts) == expectedScripts
            }

            $0.it("parses build rules") {
                var target = validTarget
                let buildRules: [[String: Any]] = [
                    [
                        "name": "My Rule",
                        "script": "my script",
                        "filePattern": "*.swift",
                        "outputFiles": ["file1", "file2"],
                        "outputFilesCompilerFlags": ["-a", "-b"],
                    ],
                    [
                        "compilerSpec": "apple.tool",
                        "fileType": "sourcecode.swift",
                    ],
                ]
                target["buildRules"] = buildRules

                let expectedBuildRules = [
                    BuildRule(fileType: .pattern("*.swift"), action: .script("my script"), name: "My Rule", outputFiles: ["file1", "file2"], outputFilesCompilerFlags: ["-a", "-b"]),
                    BuildRule(fileType: .type("sourcecode.swift"), action: .compilerSpec("apple.tool")),
                ]

                let parsedTarget = try Target(name: "test", jsonDictionary: target)
                try expect(parsedTarget.buildRules) == expectedBuildRules
            }

            $0.it("parses options") {
                let options = SpecOptions(
                    carthageBuildPath: "../Carthage/Build",
                    carthageExecutablePath: "../bin/carthage",
                    createIntermediateGroups: true,
                    bundleIdPrefix: "com.test",
                    developmentLanguage: "ja",
                    deploymentTarget: DeploymentTarget(
                        iOS: "11.1",
                        tvOS: "10.0",
                        watchOS: "3.0",
                        macOS: "10.12.1"
                    ),
                    fileTypes: ["abc": FileType(
                        file: false,
                        buildPhase: .sources,
                        attributes: ["a1", "a2"],
                        resourceTags: ["r1", "r2"],
                        compilerFlags: ["c1", "c2"])],
                    findCarthageFrameworks: true,
                    preGenCommand: "swiftgen",
                    postGenCommand: "pod install",
                    schemePathPrefix: "../"
                )
                let expected = Project(name: "test", options: options)
                let dictionary: [String: Any] = ["options": [
                    "carthageBuildPath": "../Carthage/Build",
                    "carthageExecutablePath": "../bin/carthage",
                    "bundleIdPrefix": "com.test",
                    "createIntermediateGroups": true,
                    "developmentLanguage": "ja",
                    "deploymentTarget": ["iOS": 11.1, "tvOS": 10.0, "watchOS": "3", "macOS": "10.12.1"],
                    "findCarthageFrameworks": true,
                    "preGenCommand": "swiftgen",
                    "postGenCommand": "pod install",
                    "fileTypes": ["abc": [
                        "file": false,
                        "buildPhase": "sources",
                        "attributes": ["a1", "a2"],
                        "resourceTags": ["r1", "r2"],
                        "compilerFlags": ["c1", "c2"],
                        ]],
                    "schemePathPrefix": "../",
                ]]
                let parsedSpec = try getProjectSpec(dictionary)
                try expect(parsedSpec) == expected
            }

            $0.it("parses packages") {
                let project = Project(name: "spm", packages: [
                    "package1": .remote(url: "package.git", versionRequirement: .exact("1.2.2"), traits: ["FeatureA", "FeatureB"]),
                    "package2": .remote(url: "package.git", versionRequirement: .upToNextMajorVersion("1.2.2")),
                    "package3": .remote(url: "package.git", versionRequirement: .upToNextMinorVersion("1.2.2")),
                    "package4": .remote(url: "package.git", versionRequirement: .branch("master")),
                    "package5": .remote(url: "package.git", versionRequirement: .revision("x")),
                    "package6": .remote(url: "package.git", versionRequirement: .range(from: "1.2.0", to: "1.2.5")),
                    "package7": .remote(url: "package.git", versionRequirement: .exact("1.2.2")),
                    "package8": .remote(url: "package.git", versionRequirement: .upToNextMajorVersion("4.0.0-beta.5")),
                    "package9": .local(path: "package/package", group: nil, excludeFromProject: false, traits: ["NoUIFramework", "Networking"]),
                    "package10": .remote(url: "https://github.com/yonaskolb/XcodeGen", versionRequirement: .exact("1.2.2")),
                    "XcodeGen": .local(path: "../XcodeGen", group: nil, excludeFromProject: false),
                    "package11": .local(path: "../XcodeGen", group: "Packages/Feature", excludeFromProject: false),
                    "package12": .remote(url: "empty-traits.git", versionRequirement: .exact("1.0.0"), traits: []),
                ], options: .init(localPackagesGroup: "MyPackages"))

                let dictionary: [String: Any] = [
                    "name": "spm",
                    "options": [
                        "localPackagesGroup": "MyPackages",
                    ],
                    "packages": [
                        "package1": ["url": "package.git", "exactVersion": "1.2.2", "traits": ["FeatureA", "FeatureB"]],
                        "package2": ["url": "package.git", "majorVersion": "1.2.2"],
                        "package3": ["url": "package.git", "minorVersion": "1.2.2"],
                        "package4": ["url": "package.git", "branch": "master"],
                        "package5": ["url": "package.git", "revision": "x"],
                        "package6": ["url": "package.git", "minVersion": "1.2.0", "maxVersion": "1.2.5"],
                        "package7": ["url": "package.git", "version": "1.2.2"],
                        "package8": ["url": "package.git", "majorVersion": "4.0.0-beta.5"],
                        "package9": ["path": "package/package", "traits": ["NoUIFramework", "Networking"]],
                        "package10": ["github": "yonaskolb/XcodeGen", "exactVersion": "1.2.2"],
                        "package11": ["path": "../XcodeGen", "group": "Packages/Feature"],
                        "package12": ["url": "empty-traits.git", "exactVersion": "1.0.0", "traits": [String]()],
                    ],
                    "localPackages": ["../XcodeGen"],
                ]
                let parsedSpec = try getProjectSpec(dictionary)
                try expect(parsedSpec) == project
            }

            $0.it("encodes package traits while preserving omitted, empty, and non-empty values") {
                func jsonDictionary(for package: SwiftPackage) throws -> [String: Any] {
                    try unwrap(package.toJSONValue() as? [String: Any])
                }

                let remoteWithoutTraits = try jsonDictionary(for: .remote(
                    url: "package.git",
                    versionRequirement: .exact("1.0.0")
                ))
                let remoteWithEmptyTraits = try jsonDictionary(for: .remote(
                    url: "package.git",
                    versionRequirement: .exact("1.0.0"),
                    traits: []
                ))
                let remoteWithTraits = try jsonDictionary(for: .remote(
                    url: "package.git",
                    versionRequirement: .exact("1.0.0"),
                    traits: ["FeatureA", "FeatureB"]
                ))
                let localWithoutTraits = try jsonDictionary(for: .local(
                    path: "../Package",
                    group: nil,
                    excludeFromProject: false
                ))
                let localWithEmptyTraits = try jsonDictionary(for: .local(
                    path: "../Package",
                    group: nil,
                    excludeFromProject: false,
                    traits: []
                ))
                let localWithTraits = try jsonDictionary(for: .local(
                    path: "../Package",
                    group: nil,
                    excludeFromProject: false,
                    traits: ["NoUIFramework", "Networking"]
                ))

                XCTAssertFalse(remoteWithoutTraits.keys.contains("traits"))
                XCTAssertEqual(remoteWithEmptyTraits["traits"] as? [String], [])
                XCTAssertEqual(remoteWithTraits["traits"] as? [String], ["FeatureA", "FeatureB"])
                XCTAssertFalse(localWithoutTraits.keys.contains("traits"))
                XCTAssertEqual(localWithEmptyTraits["traits"] as? [String], [])
                XCTAssertEqual(localWithTraits["traits"] as? [String], ["NoUIFramework", "Networking"])
            }

            $0.it("rejects invalid package traits") {
                let invalidPackages: [[String: Any]] = [
                    [
                        "url": "package.git",
                        "exactVersion": "1.0.0",
                        "traits": "FeatureA",
                    ],
                    [
                        "path": "../Package",
                        "traits": ["FeatureA", 1],
                    ],
                ]

                for dictionary in invalidPackages {
                    try expect(expression: { _ = try SwiftPackage(jsonDictionary: dictionary) }).toThrow()
                }
            }

            $0.it("parses old local package format") {
                let project = Project(name: "spm", packages: [
                    "XcodeGen": .local(path: "../XcodeGen", group: nil, excludeFromProject: false),
                    "Yams": .local(path: "Yams", group: nil, excludeFromProject: false),
                ], options: .init(localPackagesGroup: "MyPackages"))

                let dictionary: [String: Any] = [
                    "name": "spm",
                    "options": [
                        "localPackagesGroup": "MyPackages",
                    ],
                    "localPackages": ["../XcodeGen", "Yams"],
                ]
                let parsedSpec = try getProjectSpec(dictionary)
                try expect(parsedSpec) == project
            }

            $0.it("parses TargetScheme storeKitConfiguration as string") {
                var targetDictionary = validTarget
                targetDictionary["scheme"] = [
                    "storeKitConfiguration": "Configuration.storekit",
                ]

                let target = try Target(name: "test", jsonDictionary: targetDictionary)

                let scheme = TargetScheme(
                    storeKitConfiguration: "Configuration.storekit"
                )

                try expect(target.scheme) == scheme
            }

            $0.it("parses Scheme.Run storeKitConfiguration as string") {
                let schemeDictionary: [String: Any] = [
                    "build": [
                        "targets": [:],
                    ],
                    "run": [
                        "config": "debug",
                        "storeKitConfiguration": "Configuration.storekit",
                    ],
                ]
                let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)

                let runAction = Scheme.Run(
                    config: "debug",
                    storeKitConfiguration: "Configuration.storekit"
                )

                try expect(scheme.run) == runAction
            }

            $0.it("parses buildToolPlugins") {
                var target = validTarget
                let buildToolPlugins: [[String: Any]] = [
                    [
                        "plugin": "FirstPlugin",
                        "package": "FirstPackage"
                    ],
                    [
                        "plugin": "SecondPlugin",
                        "package": "SecondPackage"
                    ]
                ]
                target["buildToolPlugins"] = buildToolPlugins

                let expectedBuildToolPlugins = [
                    BuildToolPlugin(plugin: "FirstPlugin", package: "FirstPackage"),
                    BuildToolPlugin(plugin: "SecondPlugin", package: "SecondPackage")
                ]

                let parsedTarget = try Target(name: "test", jsonDictionary: target)
                try expect(parsedTarget.buildToolPlugins) == expectedBuildToolPlugins
            }
        }
    }

    func testPackagesVersion() {
        describe {
            let invalidPackages = [
                ["url": "package.git", "majorVersion": "master"],
                ["url": "package.git", "from": "develop"],
                ["url": "package.git", "minVersion": "feature/swift5.2", "maxVersion": "9.1.0"],
                ["url": "package.git", "minorVersion": "x.1.2"],
                ["url": "package.git", "exactVersion": "1.2.3.1"],
                ["url": "package.git", "version": "foo-bar"],
            ]

            $0.it("is an invalid package version") {
                for dictionary in invalidPackages {
                    try expect(expression: { _ = try SwiftPackage(jsonDictionary: dictionary) }).toThrow()
                }
            }
        }
    }

    func testDecoding() throws {
        describe {
            $0.it("decodes dots in dictionary keys") {
                let dictionary: [String: Any] = [
                    "test": [
                        "one.two": true,
                    ],
                ]

                let booleans: [String: Bool] = try dictionary.json(atKeyPath: "test")
                try expect(booleans) == ["one.two": true]
            }
        }
    }
}

@discardableResult
private func getProjectSpec(_ project: [String: Any], file: String = #file, line: Int = #line) throws -> Project {
    var projectDictionary: [String: Any] = ["name": "test"]
    for (key, value) in project {
        projectDictionary[key] = value
    }
    do {
        return try Project(jsonDictionary: projectDictionary)
    } catch {
        throw failure("\(error)", file: file, line: line)
    }
}

private func loadSpec(path: Path, variables: [String: String] = [:], file: String = #file, line: Int = #line) throws -> Project {
    do {
        let specLoader = SpecLoader(version: "1.1.0")
        return try specLoader.loadProject(path: path, variables: variables)
    } catch {
        throw failure("\(error)", file: file, line: line)
    }
}

private func expectSpecError(_ project: [String: Any], _ expectedError: SpecParsingError, file: String = #file, line: Int = #line) throws {
    try expectError(expectedError, file: file, line: line) {
        try getProjectSpec(project)
    }
}

private func expectTargetError(_ target: [String: Any], _ expectedError: SpecParsingError, file: String = #file, line: Int = #line) throws {
    try expectError(expectedError, file: file, line: line) {
        _ = try Target(name: "test", jsonDictionary: target)
    }
}

private func expectBreakpointError(_ breakpoint: [String: Any], _ expectedError: SpecParsingError, file: String = #file, line: Int = #line) throws {
    try expectError(expectedError, file: file, line: line) {
        _ = try Breakpoint(jsonDictionary: breakpoint)
    }
}
