package world.respect.datalayer.db.school.xapi

import io.ktor.http.Url
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import world.respect.datalayer.db.school.testSchoolDb
import world.respect.datalayer.db.school.toDataSource
import world.respect.datalayer.db.school.xapi.adapters.toEntities
import world.respect.datalayer.db.school.xapi.adapters.toModel
import world.respect.lib.dataloadstate.ext.dataOrNull
import world.respect.lib.xapi.resources.XapiStatementsResource
import world.respect.datalayer.school.xapi.ext.addStatementIdIfNotPresent
import world.respect.datalayer.school.xapi.ext.allActors
import world.respect.datalayer.school.xapi.ext.allDefinedActivities
import world.respect.datalayer.school.xapi.ext.allDefinedVerbs
import world.respect.datalayer.school.xapi.ext.distinctMerged
import world.respect.lib.xapi.model.XapiStatement
import world.respect.lib.xapi.model.XapiStatementTransformingSerializer
import world.respect.datalayer.shared.XXHashUidNumberMapper
import world.respect.lib.test.res.forXapiSampleStatements
import world.respect.lib.test.res.xapiSampleStatements
import world.respect.lib.xapi.model.XapiStatementRef
import world.respect.lib.xapi.model.XapiVerb
import world.respect.lib.xapi.resources.XapiStatementsResource.GetStatementFormatEnum
import world.respect.libxxhash.jvmimpl.XXStringHasherCommonJvm
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.time.Clock
import kotlin.uuid.Uuid

class XapiStatementsResourceDbTest {


    @Rule
    @JvmField
    val temporaryFolder: TemporaryFolder = TemporaryFolder()

    val json = Json

    @Test
    fun givenStatement_whenConvertedToEntitiesAndBack_thenShouldMatch() {
        forXapiSampleStatements { sample ->
            val statement = Json.decodeFromJsonElement(
                XapiStatementTransformingSerializer,
                sample.jsonObject.addStatementIdIfNotPresent(),
            ).let { it.copy(id = it.id ?: Uuid.random()) }

            val uidNumberMapper = XXHashUidNumberMapper(XXStringHasherCommonJvm())

            val statementEntities = statement.toEntities(
                uidNumberMapper = uidNumberMapper,
                json = json,
                isSubStatement = false,
            )

            val timeNow = Clock.System.now()
            val actors = statement.allActors().distinctMerged().map {
                it.toEntities(uidNumberMapper, timeNow)
            }.map {
                it.toModel(idOnlyFormat = false)
            }
            val activities = statement.allDefinedActivities().distinctMerged().mapNotNull {
                it.toEntities(uidNumberMapper, json, timeNow)
            }.map {
                it.toModel(json)
            }
            val verbs = statement.allDefinedVerbs()

            val primaryStatementEntity = statementEntities.statements.first { !it.isSubStatement }
            val statementFromEntities = statementEntities.toModel(
                json = json,
                uidNumberMapper = uidNumberMapper,
                actors = actors,
                activities = activities,
                verbs = verbs,
                statementIdHi = primaryStatementEntity.statementIdHi,
                statementIdLo = primaryStatementEntity.statementIdLo,
            )

            try {
                assertXapiStatementCanonicallyEqual(
                    expected = statement,
                    actual = statementFromEntities
                )
            }catch(e: Throwable) {
                e.printStackTrace()
                throw e
            }
        }
    }

    @Test
    fun givenStatement_whenStoredAndRetrieved_thenShouldMatch() {
        runBlocking {
            forXapiSampleStatements { statement ->
                testSchoolDb(temporaryFolder.newFolder()) { db ->
                    val dataSource = db.toDataSource(
                        authenticatedUserUid = "1",
                        schoolUrl = Url("http://localhost:8098/"),
                    )

                    val stmtUuid = Uuid.random()
                    val timeNow = Clock.System.now()

                    val statement = Json.decodeFromJsonElement(
                        XapiStatement.serializer(), statement.jsonObject
                    ).copy(
                        id = stmtUuid,
                        timestamp = timeNow,
                        stored = Clock.System.now(),
                    )

                    dataSource.xapiStatementsResource.post(listOf(statement))

                    //check canonical match
                    val canonicalStmtFromDb = dataSource.xapiStatementsResource.get(

                        listParams = XapiStatementsResource.GetStatementParams(
                            format = XapiStatementsResource.GetStatementFormatEnum.CANONICAL,
                            statementId = stmtUuid
                        )
                    ).dataOrNull()?.statements?.first()

                    assertNotNull(canonicalStmtFromDb)
                    assertXapiStatementCanonicallyEqual(
                        expected = statement,
                        actual = canonicalStmtFromDb,
                    )

                    val exactStmtFromDb = dataSource.xapiStatementsResource.get(

                        listParams = XapiStatementsResource.GetStatementParams(
                            format = XapiStatementsResource.GetStatementFormatEnum.EXACT,
                            statementId = stmtUuid
                        )
                    ).dataOrNull()?.statements?.first()
                    assertEquals(statement, exactStmtFromDb)

                    val idOnlyStmtFromDb = dataSource.xapiStatementsResource.get(
                        listParams = XapiStatementsResource.GetStatementParams(
                            format = XapiStatementsResource.GetStatementFormatEnum.IDS,
                            statementId = stmtUuid
                        )
                    ).dataOrNull()?.statements?.first()
                    assertNotNull(idOnlyStmtFromDb)
                    assertXapiStatementCanonicallyEqual(
                        expected = statement,
                        actual = idOnlyStmtFromDb,
                        idOnlyFormat = true,
                    )
                }
            }
        }
    }

    @Test
    fun givenStatementInserted_whenVoided_thenShouldNotShowUp() {
        val sampleStmt = xapiSampleStatements().first()
        runBlocking {
            testSchoolDb(temporaryFolder.newFolder()) { db ->
                val stmtUuid = Uuid.random()
                val statement = Json.decodeFromJsonElement(
                    XapiStatement.serializer(), sampleStmt.jsonObject
                ).copy(id = stmtUuid)

                val dataSource = db.toDataSource(
                    authenticatedUserUid = "1",
                    schoolUrl = Url("http://localhost:8098/"),
                )

                val getStmtParams = XapiStatementsResource.GetStatementParams(
                    statementId = stmtUuid
                )

                dataSource.xapiStatementsResource.post(listOf(statement))

                assertXapiStatementCanonicallyEqual(
                    expected = statement,
                    actual = dataSource.xapiStatementsResource.get(
                        listParams = getStmtParams
                    ).dataOrNull()?.statements?.firstOrNull()!!
                )

                val voidingStatement = XapiStatement(
                    actor = statement.actor,
                    verb = XapiVerb(id = XapiVerb.ID_VOIDED),
                    `object` = XapiStatementRef(id = stmtUuid.toString())
                )
                dataSource.xapiStatementsResource.post(listOf(voidingStatement))

                GetStatementFormatEnum.entries.forEach { format ->
                    assertNull(
                        dataSource.xapiStatementsResource.get(
                            listParams = getStmtParams.copy(
                                format = format
                            )
                        ).dataOrNull()?.statements?.firstOrNull()
                    )
                }

                val getByVoidedParams = XapiStatementsResource.GetStatementParams(
                    voidedStatementId = stmtUuid
                )
                assertEquals(
                    expected = statement,
                    actual = dataSource.xapiStatementsResource.get(
                        getByVoidedParams.copy(format = GetStatementFormatEnum.EXACT)
                    ).dataOrNull()?.statements?.firstOrNull()
                )
                assertXapiStatementCanonicallyEqual(
                    expected = statement,
                    actual = dataSource.xapiStatementsResource.get(
                        getByVoidedParams.copy(format = GetStatementFormatEnum.CANONICAL)
                    ).dataOrNull()?.statements?.firstOrNull()!!
                )
            }
        }
    }


}