package com.doctate.watch.sync

import com.doctate.watch.audio.PendingStore
import com.doctate.watch.audio.PendingUpload
import com.doctate.watch.net.UploadResult
import com.doctate.watch.testfixtures.InMemoryCaseStore
import com.google.common.truth.Truth.assertThat
import java.io.File
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder

@OptIn(ExperimentalCoroutinesApi::class)
class SyncWorkerLoopTest {
    @get:Rule val tempDir = TemporaryFolder()

    private fun newPendingStore(): PendingStore = PendingStore(File(tempDir.root, "unsynced"))

    private fun PendingStore.seed(caseId: String, recordedAt: String): File {
        val audio = audioPathFor(caseId, recordedAt)
        audio.writeBytes(byteArrayOf(0x66, 0x74, 0x79, 0x70))
        writeSidecar(PendingUpload(caseId, recordedAt, audio))
        return audio
    }

    @Test fun success_deletes_pair_marks_synced_and_resets_backoff() = runTest {
        val store = InMemoryCaseStore()
        val caseId = store.createLocal()
        val pending = newPendingStore()
        val audio = pending.seed(caseId, "2026-04-26T10:00:00Z")
        val state = SyncStateProvider()
        val uploader = SyncWorkerLoop.Uploader { c, t, _ -> UploadResult.Success(c, t, "received") }
        val loop = SyncWorkerLoop(pending, uploader, store, state, Channel(Channel.UNLIMITED))

        val step = loop.runOnce()
        assertThat(step).isInstanceOf(SyncWorkerLoop.Step.Uploaded::class.java)
        assertThat(audio.exists()).isFalse()
        assertThat(pending.sidecarPathFor(audio).exists()).isFalse()
        assertThat(store.casesFlow.value.first { it.caseId == caseId }.syncedToServer).isTrue()
        assertThat(state.state.value.lastSuccessAt).isNotNull()
    }

    @Test fun fifo_order_processes_oldest_recordedAt_first() = runTest {
        val store = InMemoryCaseStore()
        val pending = newPendingStore()
        pending.seed("case-newer", "2026-04-26T12:00:00Z")
        pending.seed("case-older", "2026-04-26T10:00:00Z")

        val processed = mutableListOf<String>()
        val state = SyncStateProvider()
        val uploader = SyncWorkerLoop.Uploader { c, t, _ ->
            processed += c
            UploadResult.Success(c, t, "received")
        }
        val loop = SyncWorkerLoop(pending, uploader, store, state, Channel(Channel.UNLIMITED))

        loop.runOnce() // first
        loop.runOnce() // second
        assertThat(processed).containsExactly("case-older", "case-newer").inOrder()
    }

    @Test fun transient_keeps_pair_and_increments_backoff_capped() = runTest {
        val store = InMemoryCaseStore()
        val pending = newPendingStore()
        val audio = pending.seed("case-x", "2026-04-26T10:00:00Z")

        val state = SyncStateProvider()
        val uploader = SyncWorkerLoop.Uploader { _, _, _ -> UploadResult.Transient("HTTP 503") }
        val loop = SyncWorkerLoop(
            pending, uploader, store, state, Channel(Channel.UNLIMITED),
            initialBackoffMs = 100L,
            maxBackoffMs = 800L,
        )

        // Each runOnce in a transient state delays internally — runTest uses
        // virtual time, so this completes instantly under the test scheduler.
        loop.runOnce(); assertThat(loop.currentBackoffMs).isEqualTo(200L)
        loop.runOnce(); assertThat(loop.currentBackoffMs).isEqualTo(400L)
        loop.runOnce(); assertThat(loop.currentBackoffMs).isEqualTo(800L)
        loop.runOnce(); assertThat(loop.currentBackoffMs).isEqualTo(800L) // capped

        assertThat(audio.exists()).isTrue()
        assertThat(pending.sidecarPathFor(audio).exists()).isTrue()
        assertThat(state.state.value.lastFailureReason).contains("HTTP 503")
    }

    @Test fun terminal_drops_pair_no_retry() = runTest {
        val store = InMemoryCaseStore()
        val pending = newPendingStore()
        val audio = pending.seed("case-terminal", "2026-04-26T10:00:00Z")

        var calls = 0
        val state = SyncStateProvider()
        val uploader = SyncWorkerLoop.Uploader { _, _, _ ->
            calls++
            UploadResult.Terminal("HTTP 401")
        }
        val loop = SyncWorkerLoop(pending, uploader, store, state, Channel(Channel.UNLIMITED))

        loop.runOnce()
        // Second runOnce sees an empty queue — terminal must have dropped the pair.
        val nextStep = loop.runOnce()
        assertThat(nextStep).isEqualTo(SyncWorkerLoop.Step.Idle)
        assertThat(audio.exists()).isFalse()
        assertThat(pending.sidecarPathFor(audio).exists()).isFalse()
        assertThat(calls).isEqualTo(1)
    }

    @Test fun idle_branch_publishes_zero_queue() = runTest {
        val store = InMemoryCaseStore()
        val pending = newPendingStore()
        val state = SyncStateProvider()
        val uploader = SyncWorkerLoop.Uploader { _, _, _ -> error("must not be called") }
        val loop = SyncWorkerLoop(pending, uploader, store, state, Channel(Channel.UNLIMITED))

        val step = loop.runOnce()
        assertThat(step).isEqualTo(SyncWorkerLoop.Step.Idle)
        assertThat(state.state.value.phase).isEqualTo(WorkerPhase.Idle)
        assertThat(state.state.value.queueLen).isEqualTo(0)
    }

    @Test fun successful_upload_kicks_burst_poll_and_shrinks_interval() = runTest {
        val store = InMemoryCaseStore()
        val pending = newPendingStore()
        pending.seed("case-burst", "2026-04-26T10:00:00Z")
        val state = SyncStateProvider()
        val kicks = Channel<SyncKick>(Channel.UNLIMITED)
        var nowVal = 1_700_000_000_000L
        val uploader = SyncWorkerLoop.Uploader { c, t, _ -> UploadResult.Success(c, t, "received") }
        val loop = SyncWorkerLoop(
            pending, uploader, store, state, kicks,
            pollIntervalMsProvider = { 30_000L },
            nowMs = { nowVal },
        )

        loop.runOnce() // upload success → self-kick BurstPoll
        // Drain kick the way run() does in production: pull from channel, apply.
        val kick = kicks.tryReceive().getOrNull() as SyncKick.BurstPoll
        loop.applyKickForTest(kick)
        assertThat(loop.isBurstActive).isTrue()
        assertThat(loop.effectivePollIntervalMs).isEqualTo(2_000L)
    }

    @Test fun burst_expires_after_budget_returns_to_default_interval() = runTest {
        val store = InMemoryCaseStore()
        val pending = newPendingStore()
        val state = SyncStateProvider()
        val kicks = Channel<SyncKick>(Channel.UNLIMITED)
        var nowVal = 1_000_000L
        val uploader = SyncWorkerLoop.Uploader { _, _, _ -> error("unused") }
        val loop = SyncWorkerLoop(
            pending, uploader, store, state, kicks,
            pollIntervalMsProvider = { 30_000L },
            nowMs = { nowVal },
        )
        loop.applyKickForTest(SyncKick.BurstPoll("case-x", deadlineMs = nowVal + 60_000L))
        assertThat(loop.effectivePollIntervalMs).isEqualTo(2_000L)

        // Advance clock past the deadline.
        nowVal += 61_000L
        assertThat(loop.isBurstActive).isFalse()
        assertThat(loop.effectivePollIntervalMs).isEqualTo(30_000L)
    }

    @Test fun success_after_transient_resets_backoff() = runTest {
        val store = InMemoryCaseStore()
        val pending = newPendingStore()
        pending.seed("case-y", "2026-04-26T10:00:00Z")

        var attempt = 0
        val state = SyncStateProvider()
        val uploader = SyncWorkerLoop.Uploader { c, t, _ ->
            attempt++
            if (attempt == 1) UploadResult.Transient("server warming up")
            else UploadResult.Success(c, t, "received")
        }
        val loop = SyncWorkerLoop(
            pending, uploader, store, state, Channel(Channel.UNLIMITED),
            initialBackoffMs = 100L, maxBackoffMs = 800L,
        )

        loop.runOnce() // transient → backoff doubles to 200
        assertThat(loop.currentBackoffMs).isEqualTo(200L)
        loop.runOnce() // success → backoff resets
        assertThat(loop.currentBackoffMs).isEqualTo(100L)
    }
}
