package com.doctate.watch.audio

import com.google.common.truth.Truth.assertThat
import java.io.File
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder

class PendingStoreTest {
    @get:Rule val tempDir = TemporaryFolder()

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

    private fun writeAudio(store: PendingStore, caseId: String, recordedAt: String): File {
        val audio = store.audioPathFor(caseId, recordedAt)
        audio.writeBytes(byteArrayOf(0x66, 0x74, 0x79, 0x70)) // dummy 'ftyp' bytes
        return audio
    }

    @Test fun audioPathFor_replaces_colons_in_recorded_at() {
        val store = newStore()
        val path = store.audioPathFor("case-aaa", "2026-04-26T10:30:45Z")
        assertThat(path.name).isEqualTo("case-aaa_2026-04-26T10-30-45Z.m4a")
    }

    @Test fun writeSidecar_atomic_no_tmp_left() {
        val store = newStore()
        val audio = writeAudio(store, "case-a", "2026-04-26T10:30:45Z")
        store.writeSidecar(PendingUpload("case-a", "2026-04-26T10:30:45Z", audio))

        val sidecar = store.sidecarPathFor(audio)
        assertThat(sidecar.exists()).isTrue()
        val tmpArtefacts = audio.parentFile?.listFiles { f -> f.name.endsWith(".tmp") } ?: emptyArray()
        assertThat(tmpArtefacts).isEmpty()
    }

    @Test fun scan_returns_pairs_only_when_sidecar_present() {
        val store = newStore()
        // pair 1: complete
        val a = writeAudio(store, "case-a", "2026-04-26T10:00:00Z")
        store.writeSidecar(PendingUpload("case-a", "2026-04-26T10:00:00Z", a))
        // pair 2: orphan audio (no sidecar) — must be skipped, not crash the scan
        writeAudio(store, "case-b", "2026-04-26T11:00:00Z")

        val found = store.scan()
        assertThat(found.map { it.caseId }).containsExactly("case-a")
    }

    @Test fun scan_skips_unreadable_sidecar_without_killing_recovery() {
        val store = newStore()
        // pair 1: complete
        val a = writeAudio(store, "case-a", "2026-04-26T10:00:00Z")
        store.writeSidecar(PendingUpload("case-a", "2026-04-26T10:00:00Z", a))
        // pair 2: corrupted sidecar
        val b = writeAudio(store, "case-b", "2026-04-26T11:00:00Z")
        store.sidecarPathFor(b).writeText("{ this is not valid json", Charsets.UTF_8)

        val found = store.scan()
        assertThat(found.map { it.caseId }).containsExactly("case-a")
    }

    @Test fun scan_returns_pairs_in_recordedAt_ascending_order_for_FIFO_upload() {
        val store = newStore()
        val ts3 = "2026-04-26T12:00:00Z"
        val ts1 = "2026-04-26T10:00:00Z"
        val ts2 = "2026-04-26T11:00:00Z"
        // Insert out of chronological order — scan must still sort.
        for ((id, ts) in listOf("c-c" to ts3, "c-a" to ts1, "c-b" to ts2)) {
            val audio = writeAudio(store, id, ts)
            store.writeSidecar(PendingUpload(id, ts, audio))
        }
        val recorded = store.scan().map { it.recordedAt }
        assertThat(recorded).isInOrder() // ascending
    }

    @Test fun scan_returns_empty_for_missing_dir() {
        val missing = PendingStore(File(tempDir.root, "does-not-exist"))
        assertThat(missing.scan()).isEmpty()
    }

    @Test fun deletePair_idempotent_even_when_files_missing() {
        val store = newStore()
        val audio = writeAudio(store, "case-x", "2026-04-26T10:00:00Z")
        store.writeSidecar(PendingUpload("case-x", "2026-04-26T10:00:00Z", audio))
        // First delete: both files go.
        store.deletePair(audio)
        assertThat(audio.exists()).isFalse()
        assertThat(store.sidecarPathFor(audio).exists()).isFalse()
        // Second delete: no exception.
        store.deletePair(audio)
    }

    @Test fun roundtrip_via_scan_recovers_caseId_and_recordedAt() {
        val store = newStore()
        val audio = writeAudio(store, "case-a", "2026-04-26T10:00:00Z")
        store.writeSidecar(PendingUpload("case-a", "2026-04-26T10:00:00Z", audio))

        val found = store.scan().single()
        assertThat(found.caseId).isEqualTo("case-a")
        assertThat(found.recordedAt).isEqualTo("2026-04-26T10:00:00Z")
        assertThat(found.file.absolutePath).isEqualTo(audio.absolutePath)
    }

    /** Regression guard: subsecond precision must be stripped. A single
     *  microsecond leak would feed the server filename parser
     *  `YYYY-MM-DDTHH-MM-SS.NNNNNNZ.m4a`, which it treats as malformed —
     *  the resulting `<time datetime>` is empty and the day-grouping
     *  falls back to `today`, producing duplicate "Heute" headers.
     *  Mirrors `now_rfc3339_has_no_subsecond_component` in the Rust
     *  `doctate-common::timestamp` crate. */
    @Test fun nowRfc3339_has_no_subsecond_component() {
        val now = PendingStore.nowRfc3339()
        assertThat(now).doesNotContain(".")
        assertThat(now).endsWith("Z")
    }
}
