package org.gradle.kotlin.dsl

import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.PolymorphicDomainObjectContainer
import org.gradle.api.Task
import org.gradle.api.tasks.Delete
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.TaskContainer
import org.gradle.kotlin.dsl.support.uncheckedCast
import org.gradle.test.fixtures.ExpectDeprecationExtension
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.sameInstance
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify


class NamedDomainObjectContainerExtensionsTest {

    data class DomainObject(var foo: String? = null, var bar: Boolean? = null)

    @Test
    fun `can use monomorphic container api`() {

        val alice = DomainObject()
        val bob = DomainObject()
        val john = DomainObject()
        val marty = mockDomainObjectProviderFor(DomainObject())
        val doc = mockDomainObjectProviderFor(DomainObject())
        val container = mock<NamedDomainObjectContainer<DomainObject>> {
            on { getByName("alice") } doReturn alice
            on { create("bob") } doReturn bob
            on { maybeCreate("john") } doReturn john
            on { named("marty") } doReturn marty
            on { register("doc") } doReturn doc
            on { getByName(eq("alice"), any<Action<DomainObject>>()) } doReturn alice
            on { create(eq("bob"), any<Action<DomainObject>>()) } doReturn bob
        }

        // regular syntax
        container.getByName("alice") {
            foo = "alice-foo"
        }
        container.create("bob") {
            foo = "bob-foo"
        }
        container.maybeCreate("john")

        container.named("marty")
        container.register("doc")

        // invoke syntax
        container {
            getByName("alice") {
                foo = "alice-foo"
            }
            create("bob") {
                foo = "bob-foo"
            }
            maybeCreate("john")

            named("marty")
            register("doc")
        }
    }

    @Test
    fun `can use polymorphic container api`() {

        val alice = DomainObjectBase.Foo()
        val bob = DomainObjectBase.Bar()
        val default = DomainObjectBase.Default()
        val marty = DomainObjectBase.Foo()
        val martyProvider = mockDomainObjectProviderFor(marty)
        val doc = DomainObjectBase.Bar()
        val docProvider = mockDomainObjectProviderFor<DomainObjectBase>(doc)
        val docProviderAsBarProvider = uncheckedCast<NamedDomainObjectProvider<DomainObjectBase.Bar>>(docProvider)
        val container = mock<PolymorphicDomainObjectContainer<DomainObjectBase>> {
            on { getByName("alice") } doReturn alice
            on { maybeCreate("alice", DomainObjectBase.Foo::class.java) } doReturn alice
            on { create(eq("bob"), eq(DomainObjectBase.Bar::class.java), any<Action<DomainObjectBase.Bar>>()) } doReturn bob
            on { create("john") } doReturn default
            on { create("john", DomainObjectBase.Default::class.java) } doReturn default
            onNamedWithAction("marty", DomainObjectBase.Foo::class, martyProvider)
            on { register(eq("doc"), eq(DomainObjectBase.Bar::class.java)) } doReturn docProviderAsBarProvider
            onRegisterWithAction("doc", DomainObjectBase.Bar::class, docProviderAsBarProvider)
        }

        // regular syntax
        container.getByName<DomainObjectBase.Foo>("alice") {
            foo = "alice-foo-2"
        }
        container.maybeCreate<DomainObjectBase.Foo>("alice")
        container.create<DomainObjectBase.Bar>("bob") {
            bar = true
        }
        container.create("john")

        container.named("marty", DomainObjectBase.Foo::class) {
            foo = "marty-foo"
        }
        container.named<DomainObjectBase.Foo>("marty") {
            foo = "marty-foo-2"
        }
        container.register<DomainObjectBase.Bar>("doc") {
            bar = true
        }

        // invoke syntax
        container {
            getByName<DomainObjectBase.Foo>("alice") {
                foo = "alice-foo-2"
            }
            maybeCreate<DomainObjectBase.Foo>("alice")
            create<DomainObjectBase.Bar>("bob") {
                bar = true
            }
            create("john")

            named("marty", DomainObjectBase.Foo::class) {
                foo = "marty-foo"
            }
            named<DomainObjectBase.Foo>("marty") {
                foo = "marty-foo-2"
            }
            register<DomainObjectBase.Bar>("doc") {
                bar = true
            }
        }
    }

    @Test
    fun `can configure monomorphic container`() {

        val alice = DomainObject()
        val aliceProvider = mockDomainObjectProviderFor(alice)

        val bob = DomainObject()
        val bobProvider = mockDomainObjectProviderFor(bob)

        val container = mock<NamedDomainObjectContainer<DomainObject>> {
            on { named("alice") } doReturn aliceProvider
            on { named("bob") } doReturn bobProvider
        }

        container {
            "alice" {
                foo = "alice-foo"
            }
            "alice" {
                // will configure the same object as the previous block
                bar = true
            }
            "bob" {
                foo = "bob-foo"
                bar = false
            }
        }

        assertThat(
            alice,
            equalTo(DomainObject("alice-foo", true))
        )

        assertThat(
            bob,
            equalTo(DomainObject("bob-foo", false))
        )
    }

    sealed class DomainObjectBase {
        data class Foo(var foo: String? = null) : DomainObjectBase()
        data class Bar(var bar: Boolean? = null) : DomainObjectBase()
        data class Default(val isDefault: Boolean = true) : DomainObjectBase()
    }

    @Test
    fun `can configure polymorphic container`() {

        val alice = DomainObjectBase.Foo()
        val aliceProvider = mockDomainObjectProviderFor(alice)

        val bob = DomainObjectBase.Bar()
        val bobProvider = mockDomainObjectProviderFor(bob)

        val default: DomainObjectBase = DomainObjectBase.Default()
        val defaultProvider = mockDomainObjectProviderFor(default)

        val container = mock<PolymorphicDomainObjectContainer<DomainObjectBase>> {
            onNamedWithAction("alice", DomainObjectBase.Foo::class, aliceProvider)
            on { named(eq("bob"), eq(DomainObjectBase.Bar::class.java)) } doReturn bobProvider
            on { named(eq("jim")) } doReturn defaultProvider
            on { named(eq("steve")) } doReturn defaultProvider
        }

        container {
            val a = "alice"(DomainObjectBase.Foo::class) {
                foo = "foo"
            }
            val b = "bob"(type = DomainObjectBase.Bar::class)
            val j = "jim" {}
            val s = named("steve")

            assertThat(a.get(), sameInstance(alice))
            assertThat(b.get(), sameInstance(bob))
            assertThat(j.get(), sameInstance(default))
            assertThat(s.get(), sameInstance(default))
        }

        assertThat(
            alice,
            equalTo(DomainObjectBase.Foo("foo"))
        )

        assertThat(
            bob,
            equalTo(DomainObjectBase.Bar())
        )
    }

    @Suppress("DEPRECATION")
    @Test
    fun `can create and configure tasks`() {

        val clean = mock<Delete>()
        val cleanProvider = mockTaskProviderFor(clean)

        val tasks = mock<TaskContainer> {
            on { create(eq("clean"), eq(Delete::class.java), any<Action<Delete>>()) } doReturn clean
            on { getByName("clean") } doReturn clean
            onNamedWithAction("clean", Delete::class, cleanProvider)
        }

        tasks {
            create<Delete>("clean") {
                delete("some")
            }
            getByName<Delete>("clean") {
                delete("stuff")
            }
            "clean"(type = Delete::class) {
                delete("things")
            }
        }

        tasks.getByName<Delete>("clean") {
            delete("build")
        }

        inOrder(clean) {
            verify(clean).delete("stuff")
            verify(clean).delete("things")
            verify(clean).delete("build")
        }
    }

    @Suppress("DEPRECATION")
    @Test
    fun `can create element within configuration block via delegated property`() {
        ExpectDeprecationExtension.intercept(
            "The 'val name by creating' property delegate syntax has been deprecated."
        ) {
            val tasks = mock<TaskContainer> {
                on { create("hello") } doReturn mock<Task>()
            }

            tasks {
                @Suppress("DEPRECATION", "UnusedVariable", "unused")
                val hello by creating
            }
            verify(tasks).create("hello")
        }
    }

    @Test
    fun `can get element of specific type within configuration block via delegated property`() {
        ExpectDeprecationExtension.intercept(
            "The 'val name by getting(Type::class)' property delegate syntax has been deprecated."
        ) {
            val task = mock<Exec>()
            val tasks = mock<TaskContainer> {
                on { getByName("hello") } doReturn task
            }

            tasks {
                @Suppress("DEPRECATION", "UnusedVariable", "unused")
                val hello by getting(Exec::class)
            }
            verify(tasks).getByName("hello")
        }
    }
}
