/*
 * Copyright 2007, 2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gradle.api.internal.tasks

import org.gradle.api.Buildable
import org.gradle.api.Task
import org.gradle.api.internal.provider.ProviderInternal
import org.gradle.api.internal.provider.ValueSupplier
import org.gradle.api.tasks.TaskDependency
import org.gradle.internal.typeconversion.UnsupportedNotationException
import org.gradle.test.fixtures.ExpectDeprecation
import org.gradle.util.Path
import org.gradle.util.internal.TextUtil
import spock.lang.Specification

import java.util.concurrent.Callable

import static org.gradle.util.internal.WrapUtil.toSet

class DefaultTaskDependencyTest extends Specification {
    private final TaskResolver resolver = Mock(TaskResolver.class)
    private final DefaultTaskDependency dependency = new DefaultTaskDependency(resolver, null)
    private Task task
    private Task otherTask

    def setup() {
        task = Mock(Task)
        otherTask = Mock(Task)
    }

    def "has no dependencies by default"() {
        expect:
        dependency.getDependencies(task) == emptySet()
    }

    def "can depend on char sequence"() {
        def input = new StringBuilder("other")

        given:
        1 * resolver.resolveTask(Path.path("other")) >> otherTask

        when:
        dependency.add(input)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "can depend on a task instance"() {
        when:
        dependency.add(otherTask)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "can depend on a task dependency"() {
        def otherDependency = Mock(TaskDependency)

        given:
        1 * otherDependency.getDependencies(task) >> toSet(otherTask)

        when:
        dependency.add(otherDependency);

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    @ExpectDeprecation("Accessing tasks provided to task dependency closures has been deprecated")
    def "can depend on a closure"() {
        Closure closure = Mock(Closure)
        when:
        dependency.add(closure)
        def result = dependency.getDependencies(task)

        then:
        result == toSet(otherTask)
        1 * closure.call(new Object[] { null }) >> { arg -> throw new NullPointerException() }
        1 * closure.call(task) >> { arg -> task }
    }

    def "can depend on a closure that returns null"() {
        when:
        dependency.add({ null })

        then:
        dependency.getDependencies(task) == emptySet()
    }

    def "can depend on a buildable"() {
        Buildable buildable = Mock(Buildable)
        TaskDependency otherDependency = Mock(TaskDependency)

        given:
        1 * buildable.getBuildDependencies() >> otherDependency
        1 * otherDependency.getDependencies(task) >> toSet(otherTask)

        when:
        dependency.add(buildable)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "can depend on an iterable"() {
        List tasks = [otherTask]
        Iterable iterable = { tasks.iterator() } as Iterable

        when:
        dependency.add(iterable)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "can depend on a callable"() {
        Callable callable = Mock(Callable)

        given:
        1 * callable.call() >> otherTask

        when:
        dependency.add(callable)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "can depend on a callable that returns null"() {
        Callable callable = Mock(Callable)

        given:
        1 * callable.call() >> null

        when:
        dependency.add(callable)

        then:
        dependency.getDependencies(task) == emptySet()
    }

    def "delegates to Provider to determine build dependencies"() {
        def provider = Mock(ProviderInternal)

        given:
        1 * provider.producer >> ValueSupplier.ValueProducer.task(otherTask)

        when:
        dependency.add(provider)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "uses Provider value to determine build dependencies when Provider does not know anything about the tasks that produce its value"() {
        def provider = Mock(ProviderInternal)

        given:
        1 * provider.producer >> ValueSupplier.ValueProducer.unknown()
        1 * provider.get() >> otherTask

        when:
        dependency.add(provider)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "delegates to TaskDependencyContainer to determine build dependencies"() {
        def dep = Mock(TaskDependencyContainer)

        given:
        1 * dep.visitDependencies(_) >> { TaskDependencyResolveContext context -> context.add(otherTask) }

        when:
        dependency.add(dep)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "delegates to nested TaskDependencyContainer to determine build dependencies"() {
        def dep = Mock(TaskDependencyContainer)
        def nested = Mock(TaskDependencyContainer)

        given:
        1 * dep.visitDependencies(_) >> { TaskDependencyResolveContext context -> context.add(nested) }
        1 * nested.visitDependencies(_) >> { TaskDependencyResolveContext context -> context.add(otherTask) }

        when:
        dependency.add(dep)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "fails for other types"() {
        when:
        dependency.add(12)
        dependency.getDependencies(task)

        then:
        def e = thrown(TaskDependencyResolveException)
        e.cause instanceof UnsupportedNotationException
        e.cause.message == TextUtil.toPlatformLineSeparators('''Cannot convert 12 to a task.
The following types/formats are supported:
  - A String or CharSequence task name or path
  - A Task instance
  - A TaskReference instance
  - A Buildable instance
  - A TaskDependency instance
  - A Provider that represents a task output
  - A Provider instance that returns any of these types
  - A Closure instance that returns any of these types
  - A Callable instance that returns any of these types
  - An Iterable, Collection, Map or array instance that contains any of these types''')
    }

    def "fails for char sequence when no resolver provided"() {
        DefaultTaskDependency dependency = new DefaultTaskDependency()
        StringBuffer dep = new StringBuffer("task")

        when:
        dependency.add(dep)
        dependency.getDependencies(task)

        then:
        def e = thrown(TaskDependencyResolveException)
        e.cause instanceof UnsupportedNotationException
        e.cause.message.startsWith "Cannot convert $dep to a task."
    }

    def "flattens collections"() {
        when:
        dependency.add(toSet(otherTask))

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "flattens maps"() {
        when:
        dependency.add([key: otherTask])

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "flattens arrays"() {
        when:
        dependency.add([[otherTask] as Task[]])

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "can not mutate dependency values through remove methods"() {
        given:
        dependency.add(otherTask)

        when:
        dependency.mutableValues.removeAll([otherTask])

        then:
        UnsupportedOperationException e1 = thrown()
        e1.message == 'Removing a task dependency from a task instance is not supported.'

        when:
        dependency.mutableValues.retainAll([])

        then:
        UnsupportedOperationException e2 = thrown()
        e2.message == 'Removing a task dependency from a task instance is not supported.'

        when:
        dependency.mutableValues.remove(otherTask)

        then:
        UnsupportedOperationException e3 = thrown()
        e3.message == 'Removing a task dependency from a task instance is not supported.'
    }

    def "can nest iterables and maps and closures and callables"() {
        Map nestedMap = [task: otherTask]
        Iterable nestedCollection = [nestedMap]
        Callable nestedCallable = { nestedCollection } as Callable
        Closure nestedClosure = { nestedCallable }
        List collection = [nestedClosure]
        Closure closure = { collection }
        Object[] array = [closure] as Object[]
        Map map = [key: array]
        Callable callable = { map } as Callable

        when:
        dependency.add(callable)

        then:
        dependency.getDependencies(task) == toSet(otherTask)
    }

    def "reports usages of getDependencies if provided a tracker"() {
        given:
        def tracker = Mock(TaskDependencyUsageTracker)
        def dependency = new DefaultTaskDependencyFactory(resolver, tracker).configurableDependency()

        when:
        dependency.getDependenciesForInternalUse(null)
        then:
        0 * tracker.onTaskDependencyUsage(_)

        when:
        dependency.getDependencies(null)
        then:
        1 * tracker.onTaskDependencyUsage(_)
    }

    static emptySet() {
        return [] as Set
    }
}
