/*
 * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
 * that can be found in the LICENSE file.
 */
package codegen.kclass.kclass0
import kotlin.test.*
import kotlin.coroutines.*
import kotlin.reflect.KClass

fun box(): String {
    main(emptyArray<String>())
    return "OK"
}

fun main(args: Array<String>) {
    checkBasics()
    checkLocalClasses(args)
    checkAnonymousObjects(args)
    checkAnonymousObjectsAssignedToProperty(args)
    checkFunctionReferences()
    checkInterfaces()
    checkEtc()
}

private fun checkBasics() {
    checkClass(
            clazz = Any::class,
            expectedQualifiedName = "kotlin.Any",
            expectedSimpleName = "Any",
            expectedToStringName = "class kotlin.Any",
            expectedInstance = Any(),
            expectedNotInstance = null
    )
    checkClass(
            clazz = Int::class,
            expectedQualifiedName = "kotlin.Int",
            expectedSimpleName = "Int",
            expectedToStringName = "class kotlin.Int",
            expectedInstance = 42,
            expectedNotInstance = "17"
    )
    checkClass(
            clazz = String::class,
            expectedQualifiedName = "kotlin.String",
            expectedSimpleName = "String",
            expectedToStringName = "class kotlin.String",
            expectedInstance = "17",
            expectedNotInstance = 42
    )
    checkClass(
            clazz = TopLevel::class,
            expectedQualifiedName = "codegen.kclass.kclass0.TopLevel",
            expectedSimpleName = "TopLevel",
            expectedToStringName = "class codegen.kclass.kclass0.TopLevel",
            expectedInstance = TopLevel(),
            expectedNotInstance = Any()
    )
    checkClass(
            clazz = TopLevel.Nested::class,
            expectedQualifiedName = "codegen.kclass.kclass0.TopLevel.Nested",
            expectedSimpleName = "Nested",
            expectedToStringName = "class codegen.kclass.kclass0.TopLevel.Nested",
            expectedInstance = TopLevel.Nested(),
            expectedNotInstance = Any()
    )
}

private fun checkLocalClasses(args: Array<String>) {
    // Local class.
    class Local0 {
        val captured = args
        inner class Inner
    }
    checkClass(
            clazz = Local0::class,
            expectedQualifiedName = null,
            expectedSimpleName = "Local0",
            expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$Local0",
            expectedInstance = Local0(),
            expectedNotInstance = Any()
    )
    checkClass(
            clazz = Local0.Inner::class,
            expectedQualifiedName = null,
            expectedSimpleName = "Inner",
            expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$Local0\$Inner",
            expectedInstance = Local0().Inner(),
            expectedNotInstance = Any()
    )

    // Local class inside of lambda.
    ;{
        class Local {
            val captured = args
            inner class Inner
        }
        checkClass(
                clazz = Local::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Local",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$1\$invoke\$Local",
                expectedInstance = Local(),
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = Local.Inner::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$1\$invoke\$Local\$Inner",
                expectedInstance = Local().Inner(),
                expectedNotInstance = Any()
        )
    }.invoke()

    // Local class inside of non-inlined lambda.
    runNonInlining {
        class Local {
            val captured = args
            inner class Inner
        }
        checkClass(
                clazz = Local::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Local",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$2\$invoke\$Local",
                expectedInstance = Local(),
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = Local.Inner::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$2\$invoke\$Local\$Inner",
                expectedInstance = Local().Inner(),
                expectedNotInstance = Any()
        )
    }

    // Local class inside of inlined lambda.
    run {
        class Local {
            val captured = args
            inner class Inner
        }
        checkClass(
                clazz = Local::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Local",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$Local",
                expectedInstance = Local(),
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = Local.Inner::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$Local\$Inner",
                expectedInstance = Local().Inner(),
                expectedNotInstance = Any()
        )
    }

    // Local class inside of suspend lambda.
    suspend {
        class Local {
            val captured = args
            inner class Inner
        }
        checkClass(
                clazz = Local::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Local",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$3\$invoke\$Local",
                expectedInstance = Local(),
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = Local.Inner::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$3\$invoke\$Local\$Inner",
                expectedInstance = Local().Inner(),
                expectedNotInstance = Any()
        )
    }.runCoroutine()

    // Local class inside of suspend function.
    suspend fun suspendFunWithLocalClass() {
        class Local {
            val captured = args
            inner class Inner
        }
        checkClass(
                clazz = Local::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Local",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$suspendFunWithLocalClass\$Local",
                expectedInstance = Local(),
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = Local.Inner::class,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkLocalClasses\$suspendFunWithLocalClass\$Local\$Inner",
                expectedInstance = Local().Inner(),
                expectedNotInstance = Any()
        )
    }
    ::suspendFunWithLocalClass.runCoroutine()
}

private fun checkAnonymousObjects(args: Array<String>) {
    // Anonymous object.
    with(object : Any() {
        val captured = args
        inner class Inner
        val innerKClass = Inner::class
    }) {
        checkClass(
                clazz = this::class,
                expectedQualifiedName = null,
                expectedSimpleName = null,
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$1",
                expectedInstance = this,
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = this.innerKClass,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$1\$Inner",
                expectedInstance = this.Inner(),
                expectedNotInstance = Any()
        )
    }

    // Anonymous object inside of lambda.
    ;{
        // $2 goes for this lambda itself.

        with(object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }) {
            checkClass(
                    clazz = this::class,
                    expectedQualifiedName = null,
                    expectedSimpleName = null,
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$2\$invoke\$1",
                    expectedInstance = this,
                    expectedNotInstance = Any()
            )
            checkClass(
                    clazz = this.innerKClass,
                    expectedQualifiedName = null,
                    expectedSimpleName = "Inner",
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$2\$invoke\$1\$Inner",
                    expectedInstance = this.Inner(),
                    expectedNotInstance = Any()
            )
        }
    }.invoke()

    // Anonymous object inside of non-inlined lambda.
    runNonInlining {
        with(object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }) {
            checkClass(
                    clazz = this::class,
                    expectedQualifiedName = null,
                    expectedSimpleName = null,
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$3\$invoke\$1",
                    expectedInstance = this,
                    expectedNotInstance = Any()
            )
            checkClass(
                    clazz = this.innerKClass,
                    expectedQualifiedName = null,
                    expectedSimpleName = "Inner",
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$3\$invoke\$1\$Inner",
                    expectedInstance = this.Inner(),
                    expectedNotInstance = Any()
            )
        }
    }

    // Anonymous object inside of inlined lambda.
    run {
        with(object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }) {
            checkClass(
                    clazz = this::class,
                    expectedQualifiedName = null,
                    expectedSimpleName = null,
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$4",
                    expectedInstance = this,
                    expectedNotInstance = Any()
            )
            checkClass(
                    clazz = this.innerKClass,
                    expectedQualifiedName = null,
                    expectedSimpleName = "Inner",
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$4\$Inner",
                    expectedInstance = this.Inner(),
                    expectedNotInstance = Any()
            )
        }
    }

    // Anonymous object inside of suspend lambda.
    suspend {
        with(object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }) {
            checkClass(
                    clazz = this::class,
                    expectedQualifiedName = null,
                    expectedSimpleName = null,
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$5\$invoke\$2",
                    expectedInstance = this,
                    expectedNotInstance = Any()
            )
            checkClass(
                    clazz = this.innerKClass,
                    expectedQualifiedName = null,
                    expectedSimpleName = "Inner",
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$5\$invoke\$2\$Inner",
                    expectedInstance = this.Inner(),
                    expectedNotInstance = Any()
            )
        }
    }.runCoroutine()

    // Anonymous object inside of suspend function.
    suspend fun suspendFunWithAnonymousObject() {
        // $1 goes for lambda created in `::suspendFunWithAnonymousObject.runCoroutine()` call.

        with(object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }) {
            checkClass(
                    clazz = this::class,
                    expectedQualifiedName = null,
                    expectedSimpleName = null,
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$suspendFunWithAnonymousObject\$2",
                    expectedInstance = this,
                    expectedNotInstance = Any()
            )
            checkClass(
                    clazz = this.innerKClass,
                    expectedQualifiedName = null,
                    expectedSimpleName = "Inner",
                    expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjects\$suspendFunWithAnonymousObject\$2\$Inner",
                    expectedInstance = this.Inner(),
                    expectedNotInstance = Any()
            )
        }
    }
    ::suspendFunWithAnonymousObject.runCoroutine()
}

private fun checkAnonymousObjectsAssignedToProperty(args: Array<String>) {
    // Anonymous object.
    val obj = object : Any() {
        val captured = args
        inner class Inner
        val innerKClass = Inner::class
    }
    checkClass(
            clazz = obj::class,
            expectedQualifiedName = null,
            expectedSimpleName = null,
            expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$obj\$1",
            expectedInstance = obj,
            expectedNotInstance = Any()
    )
    checkClass(
            clazz = obj.innerKClass,
            expectedQualifiedName = null,
            expectedSimpleName = "Inner",
            expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$obj\$1\$Inner",
            expectedInstance = obj.Inner(),
            expectedNotInstance = Any()
    )

    // Anonymous object inside of lambda.
    ;{
        val obj = object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }
        checkClass(
                clazz = obj::class,
                expectedQualifiedName = null,
                expectedSimpleName = null,
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$1\$invoke\$obj\$1",
                expectedInstance = obj,
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = obj.innerKClass,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$1\$invoke\$obj\$1\$Inner",
                expectedInstance = obj.Inner(),
                expectedNotInstance = Any()
        )
    }.invoke()

    // Anonymous object inside of non-inlined lambda.
    runNonInlining {
        val obj = object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }
        checkClass(
                clazz = obj::class,
                expectedQualifiedName = null,
                expectedSimpleName = null,
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$2\$invoke\$obj\$1",
                expectedInstance = obj,
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = obj.innerKClass,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$2\$invoke\$obj\$1\$Inner",
                expectedInstance = obj.Inner(),
                expectedNotInstance = Any()
        )
    }

    // Anonymous object inside of inlined lambda.
    run {
        val obj = object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }
        checkClass(
                clazz = obj::class,
                expectedQualifiedName = null,
                expectedSimpleName = null,
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$3",
                expectedInstance = obj,
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = obj.innerKClass,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$3\$Inner",
                expectedInstance = obj.Inner(),
                expectedNotInstance = Any()
        )
    }

    // Anonymous object inside of suspend lambda.
    suspend {
        val obj = object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }
        checkClass(
                clazz = obj::class,
                expectedQualifiedName = null,
                expectedSimpleName = null,
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$4\$invoke\$obj\$1",
                expectedInstance = obj,
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = obj.innerKClass,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$4\$invoke\$obj\$1\$Inner",
                expectedInstance = obj.Inner(),
                expectedNotInstance = Any()
        )
    }.runCoroutine()

    // Anonymous object inside of suspend function.
    suspend fun suspendFunWithAnonymousObject() {
        // $1 goes for lambda created in `::suspendFunWithAnonymousObject.runCoroutine()` call.

        val obj = object : Any() {
            val captured = args
            inner class Inner
            val innerKClass = Inner::class
        }
        checkClass(
                clazz = obj::class,
                expectedQualifiedName = null,
                expectedSimpleName = null,
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$suspendFunWithAnonymousObject\$obj\$1",
                expectedInstance = obj,
                expectedNotInstance = Any()
        )
        checkClass(
                clazz = obj.innerKClass,
                expectedQualifiedName = null,
                expectedSimpleName = "Inner",
                expectedToStringName = "class codegen.kclass.kclass0.checkAnonymousObjectsAssignedToProperty\$suspendFunWithAnonymousObject\$obj\$1\$Inner",
                expectedInstance = obj.Inner(),
                expectedNotInstance = Any()
        )
    }
    ::suspendFunWithAnonymousObject.runCoroutine()
}

private fun checkFunctionReferences() {
    // TODO add tests for lambdas and anonymous functions, see KT-47194
    fun foo() = Unit
    val ref = ::foo
    checkClass(
            clazz = ref::class,
            expectedQualifiedName = null,
            expectedSimpleName = null,
            expectedToStringName = "class codegen.kclass.kclass0.checkFunctionReferences\$ref\$\$FUNCTION_REFERENCE_FOR\$foo\$3",
            expectedInstance = ref,
            expectedNotInstance = Any()
    )
}

private fun checkInterfaces() {
    checkClass(
            clazz = Comparable::class,
            expectedQualifiedName = "kotlin.Comparable",
            expectedSimpleName = "Comparable",
            expectedToStringName = "class kotlin.Comparable",
            expectedInstance = 42,
            expectedNotInstance = Any()
    )
    checkClass(
            clazz = Interface::class,
            expectedQualifiedName = "codegen.kclass.kclass0.Interface",
            expectedSimpleName = "Interface",
            expectedToStringName = "class codegen.kclass.kclass0.Interface",
            expectedInstance = object : Interface {},
            expectedNotInstance = Any()
    )
}

private fun checkEtc() {
    checkInstanceClass(Any(), Any::class)
    checkInstanceClass(42, Int::class)
    assertEquals(42::class, Int::class)

    class Local

    checkReifiedClass<Int>(Int::class)
    checkReifiedClass<Int?>(Int::class)
    checkReifiedClass2<Int>(Int::class)
    checkReifiedClass2<Int?>(Int::class)
    checkReifiedClass<Any>(Any::class)
    checkReifiedClass2<Any>(Any::class)
    checkReifiedClass2<Any?>(Any::class)
    checkReifiedClass<Local>(Local::class)
    checkReifiedClass2<Local>(Local::class)
    checkReifiedClass<TopLevel>(TopLevel::class)
    checkReifiedClass2<TopLevel>(TopLevel::class)
}


class TopLevel {
    class Nested
}
interface Interface

private fun checkClass(
        clazz: KClass<*>,
        expectedQualifiedName: String?, expectedSimpleName: String?, expectedToStringName: String,
        expectedInstance: Any, expectedNotInstance: Any?
) {
    assertEquals(expectedQualifiedName, clazz.qualifiedName)
    assertEquals(expectedSimpleName, clazz.simpleName)
    assertEquals(expectedToStringName, clazz.toString())

    assertTrue(clazz.isInstance(expectedInstance))
    if (expectedNotInstance != null) assertTrue(!clazz.isInstance(expectedNotInstance))
}

private fun checkInstanceClass(instance: Any, clazz: KClass<*>) {
    assertEquals(clazz, instance::class)
}

private inline fun <reified T> checkReifiedClass(expectedClass: KClass<*>) {
    assertEquals(expectedClass, T::class)
}

private inline fun <reified T> checkReifiedClass2(expectedClass: KClass<*>) {
    checkReifiedClass<T>(expectedClass)
    checkReifiedClass<T?>(expectedClass)
}

// Like `run` but without inlining.
private fun runNonInlining(block: () -> Unit) = block()

private fun <T> (suspend () -> T).runCoroutine() = startCoroutine(Continuation(EmptyCoroutineContext) { it.getOrThrow() })
