package pl.firmatpp.kierowca.sync

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.runCurrent
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import pl.firmatpp.kierowca.data.model.BroadcastAuthResponse
import pl.firmatpp.kierowca.data.model.RealtimeConfigDto

@OptIn(ExperimentalCoroutinesApi::class)
class DriverLiveSyncClientTest {
    private val config = RealtimeConfigDto(
        reverbEnabled = true,
        reverbAppKey = "app-key",
        reverbWsBaseUrl = "wss://example.test/app",
        reverbOrigin = "https://bootstrap-origin.test",
    )

    @Test
    fun derivesBrowserCompatibleOriginFromWebSocketUrl() {
        assertEquals("https://api-intranet.firmatpp.pl", websocketOrigin("wss://api-intranet.firmatpp.pl/ws/app/app-key"))
        assertEquals("https://example.test:8443", websocketOrigin("wss://example.test:8443/app/app-key"))
        assertEquals("http://localhost:8080", websocketOrigin("ws://localhost:8080/app/app-key"))
    }

    @Test
    fun passesBootstrapOriginToWebSocketFactory() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)

        assertEquals("https://bootstrap-origin.test", factory.origins.single())
    }

    @Test
    fun reconnectsWithBackoffAfterSocketFailure() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        factory.sockets.single().fail()

        advanceTimeBy(999)
        assertEquals(1, factory.sockets.size)

        advanceTimeBy(1)
        runCurrent()
        assertEquals(2, factory.sockets.size)

        factory.sockets.last().fail()
        advanceTimeBy(1_999)
        assertEquals(2, factory.sockets.size)

        advanceTimeBy(1)
        runCurrent()
        assertEquals(3, factory.sockets.size)
    }

    @Test
    fun pausesReconnectWhileOfflineAndReconnectsImmediatelyWhenNetworkReturns() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        client.onNetworkAvailable(false)
        factory.sockets.single().fail()

        advanceTimeBy(10_000)
        assertEquals(1, factory.sockets.size)
        assertTrue(factory.sockets.single().closed)

        client.onNetworkAvailable(true)
        advanceUntilIdle()

        assertEquals(2, factory.sockets.size)
    }

    @Test
    fun foregroundEnsureConnectedRestartsWhenSocketIsDisconnected() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        factory.sockets.single().fail()
        client.ensureConnected()
        advanceUntilIdle()

        assertEquals(2, factory.sockets.size)
    }

    @Test
    fun backgroundClosesSocketAndForegroundReconnects() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        client.setForeground(false)

        assertTrue(factory.sockets.single().closed)
        assertEquals(LiveSyncConnectionState.Stopped, client.connectionState.value)

        client.setForeground(true)
        advanceUntilIdle()

        assertEquals(2, factory.sockets.size)
    }

    @Test
    fun retriesWhenPrivateChannelSubscriptionAuthFails() = runTest {
        val gateway = FakeLiveSyncGateway(authFailure = RuntimeException("offline"))
        val factory = FakeWebSocketFactory()
        val client = liveClient(gateway = gateway, factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        factory.sockets.single().message(connectionEstablished("socket-1"))
        advanceUntilIdle()
        advanceTimeBy(1_000)
        runCurrent()

        assertEquals(2, factory.sockets.size)
    }

    @Test
    fun respondsToPusherPingWithPong() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        factory.sockets.single().message("""{"event":"pusher:ping"}""")

        assertTrue(factory.sockets.single().sent.any { it.contains("pusher:pong") })
    }

    @Test
    fun subscribesToOwnUpdatesAndSharedLeaveCalendarUpdates() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = this)

        client.start("driver-1", config)
        factory.sockets.single().message(connectionEstablished("socket-1"))
        runCurrent()

        val sent = factory.sockets.single().sent
        assertTrue(sent.any { it.contains("private-driver-mobile.driver-1") })
        assertTrue(sent.any { it.contains("private-driver-mobile.0") })
        client.stop()
    }

    @Test
    fun closesAndReconnectsWhenConnectedSocketStopsReceivingMessages() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        factory.sockets.single().message(connectionEstablished("socket-1"))
        advanceUntilIdle()
        factory.sockets.single().message("""{"event":"pusher_internal:subscription_succeeded","channel":"private-driver-mobile.driver-1"}""")

        advanceTimeBy(30_000)
        runCurrent()

        assertTrue(factory.sockets.first().closed)
        assertEquals(2, factory.sockets.size)
    }

    @Test
    fun ignoresCallbacksFromOldSocketAfterReconnect() = runTest {
        val factory = FakeWebSocketFactory()
        val client = liveClient(factory = factory, scope = backgroundScope)

        client.start("driver-1", config)
        val oldSocket = factory.sockets.single()
        oldSocket.fail()
        client.ensureConnected()
        assertEquals(2, factory.sockets.size)

        oldSocket.fail()
        client.ensureConnected()

        assertEquals(2, factory.sockets.size)
    }

    private fun liveClient(
        gateway: FakeLiveSyncGateway = FakeLiveSyncGateway(),
        factory: FakeWebSocketFactory,
        scope: CoroutineScope,
    ): DriverLiveSyncClient =
        DriverLiveSyncClient(
            gateway = gateway,
            onConnected = {},
            onHint = {},
            webSocketFactory = factory,
            scope = scope,
            reconnectDelaysMs = listOf(1_000L, 2_000L),
            staleTimeoutMs = 30_000L,
        )

    private fun connectionEstablished(socketId: String): String =
        """{"event":"pusher:connection_established","data":"{\"socket_id\":\"$socketId\"}"}"""

    private class FakeLiveSyncGateway(
        private val authFailure: Throwable? = null,
    ) : DriverLiveSyncGateway {
        override suspend fun broadcastAuth(socketId: String, channelName: String): BroadcastAuthResponse {
            authFailure?.let { throw it }
            return BroadcastAuthResponse(auth = "auth-token")
        }

        override suspend fun storeRealtimeStatus(status: String, socketId: String?, error: String?) = Unit
    }

    private class FakeWebSocketFactory : LiveWebSocketFactory {
        val sockets = mutableListOf<FakeWebSocket>()
        val origins = mutableListOf<String?>()

        override fun newWebSocket(url: String, origin: String?, listener: WebSocketListener): WebSocket {
            val socket = FakeWebSocket(listener)
            sockets += socket
            origins += origin
            return socket
        }
    }

    private class FakeWebSocket(
        private val listener: WebSocketListener,
    ) : WebSocket {
        val sent = mutableListOf<String>()
        var closed = false

        fun fail() {
            listener.onFailure(this, RuntimeException("boom"), null)
        }

        fun message(text: String) {
            listener.onMessage(this, text)
        }

        override fun request(): Request = Request.Builder().url("wss://example.test/app").build()

        override fun queueSize(): Long = 0L

        override fun send(text: String): Boolean {
            sent += text
            return true
        }

        override fun send(bytes: ByteString): Boolean = true

        override fun close(code: Int, reason: String?): Boolean {
            closed = true
            return true
        }

        override fun cancel() {
            closed = true
        }
    }
}
