package viaduct.codegen.km

// The intent of the tests in the file is to ensure that the JVM Descriptors and
// Signatures we're generating agree with those generated by the Kotlin compiler.  These
// tests not only test our code-generation itself, but also the many, many utility
// functions we have (e.g., in KmToCtTypeUtils.kt) to support that generation.

import kotlinx.metadata.ClassKind
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmFunction
import kotlinx.metadata.KmType
import kotlinx.metadata.KmValueParameter
import kotlinx.metadata.Modality
import kotlinx.metadata.Visibility
import kotlinx.metadata.isSuspend
import kotlinx.metadata.modality
import kotlinx.metadata.visibility
import org.junit.jupiter.api.Test
import viaduct.codegen.ct.KM_UNIT_TYPE
import viaduct.codegen.utils.JavaIdName
import viaduct.codegen.utils.Km
import viaduct.codegen.utils.KmName

const val actualspkg = "actualspkg"

fun CustomClassBuilder.addFun(
    name: String,
    retType: KmType,
    params: List<KmValueParameter> = listOf(),
    body: String? = "{}",
    isSuspend: Boolean = false
): KmFunction {
    val result =
        KmFunction(name).apply {
            visibility = Visibility.PUBLIC
            this.isSuspend = isSuspend
            returnType = retType
            valueParameters.addAll(params)
        }
    if (isSuspend) {
        this.addSuspendFunction(result, retType, body)
    } else {
        this.addFunction(result, body)
    }
    return result
}

fun CustomClassBuilder.addCtor(
    params: List<KmValueParameter> = listOf(),
    body: String = "{}"
) {
    this.addConstructor(
        KmConstructor().apply {
            visibility = Visibility.PUBLIC
            valueParameters.addAll(params)
        },
        body = body
    )
}

class EmptyClass

class EmptyClassTest {
    @Test
    fun shouldMatch() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.customClassBuilder(ClassKind.CLASS, KmName("$actualspkg/EmptyClass"))
            .addEmptyCtor()
        kmCtx.assertNoDiff(EmptyClass::class, "$actualspkg.EmptyClass")
    }
}

data class DataClassWithDefaultValue(var s: String = "s")

class DataClassWithDefaultValueTest {
    @Test
    fun shouldMatch() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.dataClassBuilder(KmName("$actualspkg/DataClassWithDefaultValue")).apply {
            addProperty(
                KmPropertyBuilder(JavaIdName("s"), Km.STRING.asType(), Km.STRING.asType(), isVariable = true, true)
                    .defaultValue("s")
            )
        }
        kmCtx.assertNoDiff(DataClassWithDefaultValue::class, "$actualspkg.DataClassWithDefaultValue")
    }
}

@Suppress("UNUSED_PARAMETER")
class NotNullTypes {
    var p1: String = ""
    var p2: Boolean = false
    var p3: List<Char> = listOf()

    fun f1(
        a1: Any,
        a2: Boolean,
        a3: Byte,
        a4: Char
    ): Double = 1.0

    fun f2(
        a1: Array<Float>,
        a2: Array<Int>,
        a3: Array<Short>
    ): Array<String> = arrayOf()
}

class NotNullTypesTest {
    @Test
    fun shouldMatch() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.customClassBuilder(ClassKind.CLASS, KmName("$actualspkg/NotNullTypes")).apply {
            addProperty(KmPropertyBuilder(JavaIdName("p1"), Km.STRING.asType(), Km.STRING.asType(), isVariable = true, constructorProperty = false))
            addProperty(KmPropertyBuilder(JavaIdName("p2"), Km.BOOLEAN.asType(), Km.BOOLEAN.asType(), isVariable = true, constructorProperty = false))
            addProperty(
                KmPropertyBuilder(JavaIdName("p3"), kmListOfType(Km.CHAR.asType()), kmListOfType(Km.CHAR.asType()), isVariable = true, constructorProperty = false)
            )

            addCtor(listOf())

            addFun(
                "f1",
                Km.DOUBLE.asType(),
                listOf(
                    KmValueParameter("a1").apply { type = Km.ANY.asType() },
                    KmValueParameter("a2").apply { type = Km.BOOLEAN.asType() },
                    KmValueParameter("a3").apply { type = Km.BYTE.asType() },
                    KmValueParameter("a4").apply { type = Km.CHAR.asType() }
                ),
                "{ return 1.0; }"
            )

            addFun(
                "f2",
                arrayKmTypeOf(Km.STRING.asType()),
                listOf(
                    KmValueParameter("a1").apply {
                        type = arrayKmTypeOf(Km.FLOAT.asType())
                    },
                    KmValueParameter("a2").apply {
                        type = arrayKmTypeOf(Km.INT.asType())
                    },
                    KmValueParameter("a3").apply {
                        type = arrayKmTypeOf(Km.SHORT.asType())
                    }
                ),
                "{ return new String[0]; }"
            )
        }

        kmCtx.assertNoDiff(NotNullTypes::class, "$actualspkg.NotNullTypes")
    }
}

@Suppress("UNUSED_PARAMETER")
class NullTypes {
    var p1: String? = null
    var p2: Boolean? = null
    var p3: List<Byte?>? = null

    fun f1(
        a1: Any?,
        a2: Float?,
        a3: Int?,
        a4: Long?
    ): Short? = null

    fun f2(
        a1: Array<String?>?,
        a2: Array<Boolean?>?,
        a3: Array<Byte?>?
    ): Array<Short?>? = null
}

class NullTypesTest {
    @Test
    fun shouldMatch() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.customClassBuilder(ClassKind.CLASS, KmName("$actualspkg/NullTypes")).apply {
            addProperty(KmPropertyBuilder(JavaIdName("p1"), Km.STRING.asNullableType(), Km.STRING.asNullableType(), isVariable = true, constructorProperty = false))
            addProperty(KmPropertyBuilder(JavaIdName("p2"), Km.BOOLEAN.asNullableType(), Km.BOOLEAN.asNullableType(), isVariable = true, constructorProperty = false))
            addProperty(
                KmPropertyBuilder(
                    JavaIdName("p3"),
                    kmListOfType(Km.BYTE.asNullableType(), true),
                    kmListOfType(Km.BYTE.asNullableType(), true),
                    isVariable = true,
                    constructorProperty = false
                )
            )

            addCtor(listOf())

            addFun(
                "f1",
                Km.SHORT.asNullableType(),
                listOf(
                    KmValueParameter("a1").apply { type = Km.ANY.asNullableType() },
                    KmValueParameter("a2").apply { type = Km.FLOAT.asNullableType() },
                    KmValueParameter("a3").apply { type = Km.INT.asNullableType() },
                    KmValueParameter("a4").apply { type = Km.LONG.asNullableType() }
                ),
                "{ return null; }"
            )

            addFun(
                "f2",
                arrayKmTypeOf(Km.SHORT.asNullableType(), true),
                listOf(
                    KmValueParameter("a1").apply {
                        type = arrayKmTypeOf(Km.STRING.asNullableType(), true)
                    },
                    KmValueParameter("a2").apply {
                        type = arrayKmTypeOf(Km.BOOLEAN.asNullableType(), true)
                    },
                    KmValueParameter("a3").apply {
                        type = arrayKmTypeOf(Km.BYTE.asType(), true)
                    }
                ),
                "{ return null; }"
            )
        }
        kmCtx.assertNoDiff(NullTypes::class, "$actualspkg.NullTypes")
    }
}

@Suppress("UNUSED_PARAMETER")
class UnitTypes(p: Int) {
    fun f1(a1: String?) = Unit

    fun f2(a1: Unit) = Unit
}

class UnitTypesTest {
    @Test
    fun unitTypeTests() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.customClassBuilder(ClassKind.CLASS, KmName("$actualspkg/UnitTypes")).apply {
            addCtor(
                listOf(KmValueParameter("p").apply { type = Km.INT.asType() })
            )

            addFun(
                "f1",
                KM_UNIT_TYPE,
                listOf(
                    KmValueParameter("a1").apply { type = Km.STRING.asNullableType() }
                )
            )

            addFun(
                "f2",
                KM_UNIT_TYPE,
                listOf(
                    KmValueParameter("a1").apply { type = KM_UNIT_TYPE }
                )
            )
        }
        kmCtx.assertNoDiff(UnitTypes::class, "$actualspkg.UnitTypes")
    }
}

interface BasicInterface {
    fun fn(a: Int): String
}

class BasicInterfaceTest {
    @Test
    fun shouldMatch() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.customClassBuilder(ClassKind.INTERFACE, KmName("$actualspkg/BasicInterface")).apply {
            addFun(
                "fn",
                Km.STRING.asType(),
                listOf(KmValueParameter("a").apply { type = Km.INT.asType() }),
                body = null
            ).also { it.modality = Modality.ABSTRACT }
        }
        kmCtx.assertNoDiff(BasicInterface::class, "$actualspkg.BasicInterface")
    }
}

data class DataClass(var a: String, var b: Int?)

class DataClassTest {
    @Test
    fun shouldMatch() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.dataClassBuilder(KmName("$actualspkg/DataClass")).apply {
            addProperty(
                KmPropertyBuilder(JavaIdName("a"), Km.STRING.asType(), Km.STRING.asType(), isVariable = true, true)
            )
            addProperty(
                KmPropertyBuilder(JavaIdName("b"), Km.INT.asNullableType(), Km.INT.asNullableType(), isVariable = true, true)
            )
        }
        kmCtx.assertNoDiff(DataClass::class, "$actualspkg.DataClass")
    }
}

interface SuspendFun {
    suspend fun foo(p: List<Int>): Int

    suspend fun bar(): Any
}

class SuspendFunTest {
    @Test
    fun shouldMatch() {
        val kmCtx = KmClassFilesBuilder()
        kmCtx.customClassBuilder(ClassKind.INTERFACE, KmName("$actualspkg/SuspendFun")).apply {
            addFun(
                "foo",
                Km.INT.asType(),
                listOf(KmValueParameter("p").apply { type = kmListOfType(Km.INT.asType()) }),
                isSuspend = true,
                body = null
            ).also { it.modality = Modality.ABSTRACT }
            addFun(
                "bar",
                Km.ANY.asType(),
                emptyList(),
                isSuspend = true,
                body = null
            ).also { it.modality = Modality.ABSTRACT }
        }
        kmCtx.assertNoDiff(SuspendFun::class, "$actualspkg.SuspendFun")
    }
}
