package ch.threema.localcrypto

import ch.threema.base.crypto.NaCl
import ch.threema.common.Base64
import ch.threema.common.toCryptographicByteArray
import ch.threema.domain.libthreema.LibthreemaHttpClient
import ch.threema.domain.models.UserCredentials
import ch.threema.domain.models.WorkClientInfo
import ch.threema.domain.types.Identity
import ch.threema.libthreema.HttpsMethod
import ch.threema.libthreema.HttpsRequest
import ch.threema.libthreema.HttpsResponse
import ch.threema.libthreema.HttpsResult
import ch.threema.localcrypto.MasterKeyTestData.AUTH_TOKEN
import ch.threema.localcrypto.MasterKeyTestData.REMOTE_SECRET
import ch.threema.localcrypto.MasterKeyTestData.REMOTE_SECRET_HASH
import ch.threema.localcrypto.MasterKeyTestData.WORK_URL
import ch.threema.localcrypto.exceptions.InvalidCredentialsException
import ch.threema.localcrypto.models.RemoteSecretClientParameters
import ch.threema.localcrypto.models.RemoteSecretParameters
import io.mockk.coEvery
import io.mockk.mockk
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import kotlin.test.fail
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest

class RemoteSecretClientTest {
    @Test
    fun `monitor remote secret`() = runTest {
        val httpClientMock = mockk<LibthreemaHttpClient> {
            coEvery { sendHttpsRequest(any()) } answers {
                val request = firstArg<HttpsRequest>()
                val body = request.body.toString(Charsets.UTF_8)
                if (request.url == "${WORK_URL}api-client/v1/remote-secret" && request.method == HttpsMethod.POST) {
                    assertTrue(Base64.encode(AUTH_TOKEN.value) in body)
                    HttpsResult.Response(
                        v1 = HttpsResponse(
                            status = 200.toUShort(),
                            body = """
                                {
                                    "secret": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=",
                                    "checkIntervalS": 11,
                                    "nMissedChecksMax": 1
                                }
                        """.toByteArray(),
                        ),
                    )
                } else {
                    fail("Unexpected request: ${request.method} ${request.url}")
                }
            }
        }
        val client = RemoteSecretClient(
            clientInfo = CLIENT_INFO,
            httpClient = httpClientMock,
        )
        val monitor = client.createRemoteSecretLoop(
            baseUrl = WORK_URL,
            parameters = RemoteSecretParameters(
                authenticationToken = AUTH_TOKEN,
                remoteSecretHash = REMOTE_SECRET_HASH,
            ),
        )

        val job = launch {
            monitor.run()
        }

        assertEquals(REMOTE_SECRET, monitor.remoteSecret.await())
        job.cancel()
    }

    @Test
    fun `create remote secret`() = runTest {
        val httpClientMock = mockk<LibthreemaHttpClient> {
            coEvery { sendHttpsRequest(any()) } answers {
                val request = firstArg<HttpsRequest>()
                val body = request.body.toString(Charsets.UTF_8)
                if (request.url == "${WORK_URL}api-client/v1/remote-secret" && request.method == HttpsMethod.PUT) {
                    assertTrue(USERNAME in body)
                    assertTrue(PASSWORD in body)
                    assertTrue(IDENTITY.value in body)
                    HttpsResult.Response(
                        v1 = HttpsResponse(
                            status = 200.toUShort(),
                            body = if ("challenge" in body) {
                                """{
                                       "secretAuthenticationToken": "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI="
                                   }"""
                            } else {
                                """{
                                       "challengePublicKey": "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
                                       "challenge": "bWVvdw=="
                                   }"""
                            }
                                .toByteArray(),
                        ),
                    )
                } else {
                    fail("Unexpected request: ${request.method} ${request.url}")
                }
            }
        }
        val client = RemoteSecretClient(
            clientInfo = CLIENT_INFO,
            httpClient = httpClientMock,
        )

        val result = client.createRemoteSecret(
            parameters = CLIENT_PARAMETERS,
        )

        assertEquals(AUTH_TOKEN, result.parameters.authenticationToken)
    }

    @Test
    fun `creating remote secret fails with invalid credentials`() = runTest {
        val httpClientMock = mockk<LibthreemaHttpClient> {
            coEvery { sendHttpsRequest(any()) } answers {
                HttpsResult.Response(
                    v1 = HttpsResponse(
                        status = 401.toUShort(),
                        body = """{ "code": "invalid-credentials" }""".toByteArray(),
                    ),
                )
            }
        }
        val client = RemoteSecretClient(
            clientInfo = CLIENT_INFO,
            httpClient = httpClientMock,
        )

        assertFailsWith<InvalidCredentialsException> {
            client.createRemoteSecret(
                parameters = CLIENT_PARAMETERS,
            )
        }
    }

    @Test
    fun `delete remote secret`() = runTest {
        val httpClientMock = mockk<LibthreemaHttpClient> {
            coEvery { sendHttpsRequest(any()) } answers {
                val request = firstArg<HttpsRequest>()
                val body = request.body.toString(Charsets.UTF_8)
                if (request.url == "${WORK_URL}api-client/v1/remote-secret" && request.method == HttpsMethod.DELETE) {
                    assertTrue(USERNAME in body)
                    assertTrue(PASSWORD in body)
                    assertTrue(IDENTITY.value in body)
                    HttpsResult.Response(
                        v1 = HttpsResponse(
                            status = 200.toUShort(),
                            body = """
                                {
                                    "challengePublicKey": "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
                                    "challenge": "bWVvdw=="
                                }
                        """.toByteArray(),
                        ),
                    )
                } else {
                    fail("Unexpected request: ${request.method} ${request.url}")
                }
            }
        }
        val client = RemoteSecretClient(
            clientInfo = CLIENT_INFO,
            httpClient = httpClientMock,
        )

        client.deleteRemoteSecret(
            parameters = CLIENT_PARAMETERS,
            authenticationToken = AUTH_TOKEN,
        )
    }

    companion object {
        private val CLIENT_INFO = WorkClientInfo(
            appVersion = "1.2.3",
            appLocale = "de_CH",
            deviceModel = "Test",
            osVersion = "999",
            workFlavor = WorkClientInfo.WorkFlavor.ON_PREM,
        )
        private val IDENTITY = Identity("01234567")
        private const val USERNAME = "Testy McTestface"
        private const val PASSWORD = "Password1"
        private val CLIENT_PARAMETERS = RemoteSecretClientParameters(
            workServerBaseUrl = WORK_URL,
            userIdentity = IDENTITY,
            clientKey = ByteArray(NaCl.SECRET_KEY_BYTES) { it.toByte() }.toCryptographicByteArray(),
            credentials = UserCredentials(
                username = USERNAME,
                password = PASSWORD,
            ),
        )
    }
}
