package ch.threema.domain.protocol.connection

import ch.threema.base.crypto.NaCl
import ch.threema.domain.protocol.ServerAddressProvider
import ch.threema.domain.protocol.connection.csp.DeviceCookieManager
import ch.threema.domain.protocol.connection.csp.socket.ChatServerAddressProvider
import ch.threema.domain.protocol.connection.d2m.MultiDevicePropertyProvider
import ch.threema.domain.protocol.connection.data.CspMessage
import ch.threema.domain.protocol.connection.data.OutboundMessage
import ch.threema.domain.protocol.csp.coders.MessageBox
import ch.threema.domain.protocol.urls.AppRatingUrl
import ch.threema.domain.protocol.urls.BlobUrl
import ch.threema.domain.protocol.urls.MapPoiAroundUrl
import ch.threema.domain.protocol.urls.MapPoiNamesUrl
import ch.threema.domain.protocol.urls.MediatorUrl
import ch.threema.domain.stores.IdentityStore
import ch.threema.domain.types.IdentityString
import ch.threema.testhelpers.MUST_NOT_BE_CALLED
import java.io.InputStream
import java.io.OutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.net.InetSocketAddress
import java.net.Socket
import java.net.SocketAddress

internal class TestIdentityStore : IdentityStore {
    companion object {
        const val CLIENT_IDENTITY = "ABCD1234"
        val ckPublic = ByteArray(NaCl.PUBLIC_KEY_BYTES)
        val ckSecret = ByteArray(NaCl.SECRET_KEY_BYTES)

        private val cache: MutableMap<ByteArray, NaCl> = mutableMapOf()

        fun getFromCache(publicKey: ByteArray): NaCl = cache
            .computeIfAbsent(publicKey) { NaCl(ckSecret, it) }
    }

    override fun encryptData(
        plaintext: ByteArray,
        nonce: ByteArray,
        receiverPublicKey: ByteArray,
    ): ByteArray =
        getFromCache(receiverPublicKey)
            .encrypt(
                data = plaintext,
                nonce = nonce,
            )

    override fun decryptData(
        ciphertext: ByteArray,
        nonce: ByteArray,
        senderPublicKey: ByteArray,
    ): ByteArray =
        getFromCache(senderPublicKey)
            .decrypt(
                data = ciphertext,
                nonce = nonce,
            )

    override fun calcSharedSecret(publicKey: ByteArray): ByteArray {
        return getFromCache(publicKey).sharedSecret
    }

    override fun getIdentityString() = CLIENT_IDENTITY

    override fun getServerGroup(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getPublicKey(): ByteArray = ckPublic

    override fun getPrivateKey(): ByteArray = ckSecret

    override fun getPublicNickname(): String = "Test"

    override fun storeIdentity(
        identity: IdentityString,
        serverGroup: String,
        privateKey: ByteArray,
    ) {
        MUST_NOT_BE_CALLED()
    }

    override fun setPublicNickname(publicNickname: String) {
        MUST_NOT_BE_CALLED()
    }

    override fun clear() {
        MUST_NOT_BE_CALLED()
    }
}

internal class TestServerAddressProvider(
    private val skPublic: ByteArray,
    private val skPublicAlt: ByteArray,
    private val mediatorUrl: String? = null,
) : ServerAddressProvider {
    var keyFetchCount = 0
    var altKeyFetchCount = 0

    override fun getChatServerNamePrefix(): String = "prefix"

    override fun getChatServerNameSuffix(): String = "suffix"

    override fun getChatServerPorts(): IntArray = intArrayOf(1234)

    override fun getChatServerUseServerGroups(): Boolean = false

    override fun getChatServerPublicKey(): ByteArray {
        keyFetchCount++
        return skPublic
    }

    override fun getChatServerPublicKeyAlt(): ByteArray {
        altKeyFetchCount++
        return skPublicAlt
    }

    override fun getMediatorUrl(): MediatorUrl {
        return MediatorUrl(mediatorUrl ?: MUST_NOT_BE_CALLED())
    }

    override fun getAppRatingUrl(): AppRatingUrl {
        MUST_NOT_BE_CALLED()
    }

    // The following methods should not be used by the connection
    override fun getDirectoryServerUrl(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getWorkServerUrlLegacy(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getWorkServerUrl(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getBlobServerDownloadUrl(): BlobUrl {
        MUST_NOT_BE_CALLED()
    }

    override fun getBlobServerUploadUrl(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getBlobServerDoneUrl(): BlobUrl {
        MUST_NOT_BE_CALLED()
    }

    override fun getBlobMirrorServerDownloadUrl(multiDevicePropertyProvider: MultiDevicePropertyProvider): BlobUrl {
        MUST_NOT_BE_CALLED()
    }

    override fun getBlobMirrorServerUploadUrl(multiDevicePropertyProvider: MultiDevicePropertyProvider): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getBlobMirrorServerDoneUrl(multiDevicePropertyProvider: MultiDevicePropertyProvider): BlobUrl {
        MUST_NOT_BE_CALLED()
    }

    override fun getAvatarServerUrl(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getSafeServerUrl(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getWebServerUrl(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getWebOverrideSaltyRtcHost(): String {
        MUST_NOT_BE_CALLED()
    }

    override fun getWebOverrideSaltyRtcPort(): Int {
        MUST_NOT_BE_CALLED()
    }

    override fun getThreemaPushPublicKey(): ByteArray? {
        MUST_NOT_BE_CALLED()
    }

    override fun getMapStyleUrl(): String? {
        MUST_NOT_BE_CALLED()
    }

    override fun getMapPoiNamesUrl(): MapPoiNamesUrl {
        MUST_NOT_BE_CALLED()
    }

    override fun getMapPoiAroundUrl(): MapPoiAroundUrl {
        MUST_NOT_BE_CALLED()
    }
}

internal class TestSocket : Socket() {
    private val outputPipe = PipedInputStream(UShort.MAX_VALUE.toInt() * 2)
    private val outputStream = PipedOutputStream(outputPipe)

    private val inputPipe = PipedOutputStream()
    private val inputStream = PipedInputStream(inputPipe, UShort.MAX_VALUE.toInt() * 2)

    var closed = false

    fun read(length: Int): ByteArray {
        return outputPipe.readNBytes(length)
    }

    fun write(bytes: ByteArray) {
        inputPipe.write(bytes)
        inputPipe.flush()
    }

    override fun connect(endpoint: SocketAddress?, timeout: Int) {}
    override fun connect(endpoint: SocketAddress?) {}

    override fun close() {
        outputPipe.close()
        outputStream.close()
        inputPipe.close()
        inputStream.close()
        closed = true
    }

    override fun getInputStream(): InputStream = inputStream

    override fun getOutputStream(): OutputStream = outputStream

    override fun setSoTimeout(timeout: Int) {}
}

internal class TestChatServerAddressProvider : ChatServerAddressProvider {
    private var updated = false
    override fun advance() {
        // No-op
    }

    override fun get(): InetSocketAddress? {
        return if (updated) {
            InetSocketAddress.createUnresolved("dummy", 4711)
        } else {
            null
        }
    }

    override fun update() {
        updated = true
    }
}

internal class TestNoopDeviceCookieManager : DeviceCookieManager {
    override fun getOrCreateDeviceCookie() = ByteArray(16)
    override fun onChangeIndicationReceived() {
        MUST_NOT_BE_CALLED()
    }
}

fun getFromOutboundMessage(message: OutboundMessage): MessageBox? {
    return try {
        val cspMessage = message as CspMessage
        MessageBox.parseBinary(cspMessage.toCspContainer().data)
    } catch (e: Exception) {
        throw AssertionError("Could not parse csp message: " + e.message)
    }
}
