import ProjectSpec
import Spectre
import XCTest
import PathKit
@testable import XcodeGenKit
import TestSupport

class CarthageDependencyResolverTests: XCTestCase {

    override func setUp() {}

    func testBaseBuildPath() {
        describe {
            $0.it("provides the default base build path") {
                let resolver = CarthageDependencyResolver(project: makeTestProject())

                try expect(resolver.buildPath) == "Carthage/Build"
            }

            $0.it("provides the base build path specified by the project specs") {
                let customPath = "MyCustomBuildPath/Test"
                let options = SpecOptions(carthageBuildPath: customPath)
                let resolver = CarthageDependencyResolver(project: makeTestProject(options: options))

                try expect(resolver.buildPath) == customPath
            }
        }
    }

    func testExecutablePath() {
        describe {
            $0.it("provides the default executable path for carthage") {
                let resolver = CarthageDependencyResolver(project: makeTestProject())

                try expect(resolver.executable) == "carthage"
            }

            $0.it("provides the executable path for carthage as specified by the project specs") {
                let customPath = "MyCustomBuildPath/Test/carthage"
                let options = SpecOptions(carthageExecutablePath: customPath)
                let resolver = CarthageDependencyResolver(project: makeTestProject(options: options))

                try expect(resolver.executable) == customPath
            }
        }
    }

    func testBuildPathForPlatform() {
        describe {
            $0.it("generates the build path for a given platform") {
                let resolver = CarthageDependencyResolver(project: makeTestProject())
                let allPlatforms = Platform.allCases
                let expectedByPlatform: [Platform: String] = allPlatforms.reduce(into: [:]) { result, next in
                    result[next] = "\(resolver.buildPath)/\(next.carthageName)"
                }

                try allPlatforms.forEach { platform in
                    try expect(expectedByPlatform[platform]) == resolver.buildPath(for: platform, linkType: .dynamic)
                }
            }
        }
    }

    func testRelatedDependenciesForPlatform() {

        let carthageBuildPath = fixturePath + "CarthageProject/Carthage/Build"

        describe {
            $0.it("fetches related dependencies for a given platform, sorted alphabetically") {

                let options = SpecOptions(carthageBuildPath: carthageBuildPath.string)
                let resolver = CarthageDependencyResolver(project: makeTestProject(options: options))
                let dependency = Dependency(type: .carthage(findFrameworks: true, linkType: .dynamic), reference: "CarthageTestFixture")
                let expectedDependencies: [Platform: [String]] = [
                    .macOS: ["DependencyFixtureB", "DependencyFixtureA", "CarthageTestFixture"],
                    .watchOS: ["DependencyFixtureA", "DependencyFixtureB", "CarthageTestFixture"],
                    .tvOS: ["CarthageTestFixture", "DependencyFixtureA", "DependencyFixtureB"],
                    .iOS: ["CarthageTestFixture", "DependencyFixtureA", "DependencyFixtureB"],
                ]

                try Platform.allCases.forEach { platform in
                    let expected = expectedDependencies[platform] ?? []
                    let related = resolver.relatedDependencies(for: dependency, in: platform)
                    try expect(related.map { $0.reference }) == expected.sorted(by: { $0 < $1 })
                }
            }

            $0.it("returns the main dependency when no related dependencies are found") {
                let resolver = CarthageDependencyResolver(project: makeTestProject())
                let dependency = Dependency(type: .carthage(findFrameworks: true, linkType: .dynamic), reference: "RandomDependency")

                let related = resolver.relatedDependencies(for: dependency, in: .iOS)

                try expect(related.map { $0.reference }) == ["RandomDependency"]
            }

            $0.it("de-duplicates dependencies") {
                let resolver = CarthageDependencyResolver(project: makeTestProject())
                let dependency = Dependency(type: .carthage(findFrameworks: true, linkType: .dynamic), reference: "ReactiveSwift")

                let related = resolver.relatedDependencies(for: dependency, in: .iOS)

                try expect(related.map { $0.reference }) == ["ReactiveSwift"]
            }
        }
    }

    func testDependenciesForTopLevelTarget() {

        let dependencyFixtureName = "CarthageTestFixture"
        let carthageBuildPath = fixturePath + "TestProject/Carthage/Build"

        describe {

            $0.it("overrides the findFrameworks dependency global flag when specified") {
                let options = SpecOptions(carthageBuildPath: carthageBuildPath.string, findCarthageFrameworks: true)
                let dependency = Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: dependencyFixtureName)

                let resolver = CarthageDependencyResolver(project: makeTestProject(options: options))
                let target = Target(name: "1", type: .application, platform: .iOS, dependencies: [dependency])
                let dependencies = resolver.dependencies(for: target)

                let expectedDependencies = [dependency].map {
                    ResolvedCarthageDependency(dependency: $0, isFromTopLevelTarget: true)
                }

                try expect(dependencies) == expectedDependencies
            }

            $0.it("fetches all carthage dependencies for a given target, sorted alphabetically") {
                let unsortedDependencyReferences = ["RxSwift", "RxCocoa", "RxBlocking", "RxTest", "RxAtomic"]
                let dependencies = unsortedDependencyReferences.map {
                    Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: $0)
                }
                let nonCarthageDependencies = unsortedDependencyReferences.map { Dependency(type: .target, reference: $0) }
                let target = Target(name: "1", type: .application, platform: .iOS, dependencies: dependencies + nonCarthageDependencies)
                let resolver = CarthageDependencyResolver(project: makeTestProject(with: [target]))

                let related = resolver.dependencies(for: target)

                let expectedDependencies = dependencies
                    .sorted(by: { $0.reference < $1.reference })
                    .map {
                        ResolvedCarthageDependency(dependency: $0, isFromTopLevelTarget: true)
                    }

                try expect(related) == expectedDependencies
            }

            $0.it("skips dependencies which are not embedded") {
                let resolver = CarthageDependencyResolver(project: makeTestProject())

                let target = Target(name: "1", type: .application, platform: .iOS, dependencies: [
                    Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: dependencyFixtureName, embed: false, link: false)
                ])
                try expect(resolver.dependencies(for: target)) == []
            }

            $0.it("skips dependencies nested in targets which are not embedded") {
                let nestedTarget = Target(name: "1", type: .framework, platform: .iOS, dependencies: [
                    Dependency(type: .carthage(findFrameworks: false, linkType: .dynamic), reference: dependencyFixtureName)
                ])

                let resolver = CarthageDependencyResolver(project: makeTestProject(with: [nestedTarget]))

                let target = Target(name: "2", type: .application, platform: .iOS, dependencies: [
                    Dependency(type: .target, reference: "1", embed: false, link: false)
                ])
                try expect(resolver.dependencies(for: target)) == []

            }
        }
    }

}

private func makeTestProject(with targets: [Target] = [], options: SpecOptions = SpecOptions()) -> Project {
    Project(name: "Test Project", targets: targets, options: options)

}
