package net.corda.finance.flows

import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.finance.contracts.asset.Cash
import net.corda.serialization.internal.AllWhitelist
import net.corda.serialization.internal.amqp.DeserializationInput
import net.corda.serialization.internal.amqp.Schema
import net.corda.serialization.internal.amqp.SerializationOutput
import net.corda.serialization.internal.amqp.SerializerFactoryBuilder
import net.corda.serialization.internal.amqp.custom.PublicKeySerializer
import net.corda.testing.core.SerializationEnvironmentRule
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.fail

// TODO: If this type of testing gets momentum, we can create a mini-framework that rides through list of files
// and performs necessary validation on all of them.
class CompatibilityTest {

    @Rule
    @JvmField
    val testSerialization = SerializationEnvironmentRule()

    val serializerFactory = SerializerFactoryBuilder.build(AllWhitelist, ClassLoader.getSystemClassLoader()).apply {
        register(PublicKeySerializer)
    }

    @Test(timeout=300_000)
	fun issueCashTansactionReadTest() {
        val inputStream = javaClass.classLoader.getResourceAsStream("compatibilityData/v3/node_transaction.dat")
        assertNotNull(inputStream)

        val inByteArray: ByteArray = inputStream.readBytes()
        val input = DeserializationInput(serializerFactory)

        val (transaction, envelope) = input.deserializeAndReturnEnvelope(
                SerializedBytes(inByteArray),
                SignedTransaction::class.java,
                SerializationDefaults.STORAGE_CONTEXT)
        assertNotNull(transaction)

        val commands = transaction.tx.commands
        assertEquals(1, commands.size)
        assertTrue(commands.first().value is Cash.Commands.Issue)

        // Serialize back and check that representation is byte-to-byte identical to what it was originally.
        val output = SerializationOutput(serializerFactory)
        val (serializedBytes, schema) = output.serializeAndReturnSchema(transaction, SerializationDefaults.STORAGE_CONTEXT)

        assertSchemasMatch(envelope.schema, schema)

        assertTrue(inByteArray.contentEquals(serializedBytes.bytes))
    }

    @Test(timeout = 300_000)
    fun performanceTest() {
        val inputStream = javaClass.classLoader.getResourceAsStream("compatibilityData/v3/node_transaction.dat")
        assertNotNull(inputStream)

        val inByteArray: ByteArray = inputStream.readBytes()
        val deserializationInput = DeserializationInput(serializerFactory)

        val bytes = SerializedBytes<SignedTransaction>(inByteArray)
        val transaction = deserializationInput.deserialize(bytes, SignedTransaction::class.java, SerializationDefaults.STORAGE_CONTEXT)
        assertNotNull(transaction)

        val counts = 1000
        val loops = 200
        for (loop in 0 until loops) {
            val start = System.nanoTime()
            for (count in 0 until counts) {
                val stx = deserializationInput.deserialize(bytes, SignedTransaction::class.java, SerializationDefaults.STORAGE_CONTEXT)
                for (input in stx.inputs) {
                    assertNotNull(input)
                }
                for (output in stx.tx.outputs) {
                    assertNotNull(output)
                }
                for (command in stx.tx.commands) {
                    assertNotNull(command)
                }
            }
            val end = System.nanoTime()
            println("Time per transaction deserialize on loop $loop = ${(end - start) / counts} nanoseconds")
        }
    }

    private fun assertSchemasMatch(original: Schema, reserialized: Schema) {
        if (original.toString() == reserialized.toString()) return
        original.types.forEach { originalType ->
            val reserializedType = reserialized.types.firstOrNull { it.name == originalType.name }
                    ?: fail("""Schema mismatch between original and re-serialized data. Could not find reserialized schema matching:

$originalType
""")

            if (originalType.toString() != reserializedType.toString())
                fail("""Schema mismatch between original and re-serialized data. Expected:

$originalType

but was:

$reserializedType
""")
        }

        reserialized.types.forEach { reserializedType ->
            if (original.types.none { it.name == reserializedType.name })
            fail("""Schema mismatch between original and re-serialized data. Could not find original schema matching:

$reserializedType
""")
        }
    }
}