NoteVocaleSuppressionRelanceTest.kt 269 lignes · 9305 octets
package fr.ebii.card2vcf.sync

import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import fr.ebii.card2vcf.data.CrmDatabase
import fr.ebii.card2vcf.data.InteractionEntity
import fr.ebii.card2vcf.data.NoteProjetEntity
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [31])
class NoteVocaleSuppressionRelanceTest {

    private lateinit var db: CrmDatabase
    private lateinit var api: FakeAilianceApi
    private lateinit var engine: SyncEngine

    @Before
    fun setUp() {
        val ctx = ApplicationProvider.getApplicationContext<Context>()
        db = Room.inMemoryDatabaseBuilder(ctx, CrmDatabase::class.java)
            .allowMainThreadQueries()
            .build()
        api = FakeAilianceApi()
        engine = SyncEngine(api, db)
    }

    @After
    fun tearDown() = db.close()

    // ---- Push interaction delete ----

    @Test
    fun push_interactionDelete_appelApiEtOpConsommee() = runBlocking {
        db.interactionDao().upsert(
            InteractionEntity(
                serverId = "iid-srv-1",
                contactServerId = "contact-srv-1",
                type = "note_vocale",
                sujet = "Note à supprimer",
                createdAt = 1L,
            ),
        )
        db.syncOpDao().insert(
            SyncOpEntity(
                entityType = "interaction",
                op = "delete",
                payloadJson = "{}",
                serverId = "iid-srv-1",
                createdAt = 1L,
            ),
        )

        engine.syncNow()

        assertEquals(1, api.deleteInteractionCalls.size)
        assertEquals("iid-srv-1", api.deleteInteractionCalls[0])
        assertEquals(0, db.syncOpDao().listAll().size)
    }

    @Test
    fun push_interactionDelete_sansSrvId_opConservee() = runBlocking {
        db.syncOpDao().insert(
            SyncOpEntity(
                entityType = "interaction",
                op = "delete",
                payloadJson = "{}",
                serverId = null,
                createdAt = 1L,
            ),
        )

        engine.syncNow()

        assertEquals(0, api.deleteInteractionCalls.size)
        // op sans serverId : abandon sans consommation
        assertEquals(1, db.syncOpDao().listAll().size)
    }

    @Test
    fun push_interactionDelete_echecApi_opConservee() = runBlocking {
        api.deleteInteractionResult = AilianceApiClient.ApiResult.Err(503, "Service indisponible")
        db.syncOpDao().insert(
            SyncOpEntity(
                entityType = "interaction",
                op = "delete",
                payloadJson = "{}",
                serverId = "iid-srv-2",
                createdAt = 1L,
            ),
        )

        engine.syncNow()

        assertEquals(1, api.deleteInteractionCalls.size)
        assertEquals(1, db.syncOpDao().listAll().size)
    }

    // ---- Pull interaction avec transcriptionErreur ----

    @Test
    fun pull_interaction_avecTranscriptionErreur_stockeChamp() = runBlocking {
        api.pullResult = AilianceApiClient.ApiResult.Ok(
            SyncPullResponse(
                serverTime = "2026-09-15T10:00:00Z",
                interactions = listOf(
                    InteractionDto(
                        id = "int-err-1",
                        contactId = "contact-srv-1",
                        typeInteraction = "note_vocale",
                        sujet = "Note en échec",
                        creeLe = "2026-09-14T08:00:00Z",
                        transcription = "echec",
                        transcriptionErreur = "délai de 30 s dépassé",
                    ),
                ),
            ),
        )

        engine.syncNow()

        val stored = db.interactionDao().getByServerId("int-err-1")
        assertNotNull(stored)
        assertEquals("echec", stored?.transcriptionStatut)
        assertEquals("délai de 30 s dépassé", stored?.transcriptionErreur)
    }

    @Test
    fun pull_interaction_transcriptionErreur_misAJourSurExistant() = runBlocking {
        db.interactionDao().upsert(
            InteractionEntity(
                serverId = "int-err-2",
                contactServerId = "contact-srv-1",
                type = "note_vocale",
                sujet = "Note",
                transcriptionStatut = "en_attente",
                transcriptionErreur = null,
                createdAt = parseIsoToEpochMs("2026-09-14T08:00:00Z"),
                updatedAt = parseIsoToEpochMs("2026-09-14T08:00:00Z"),
            ),
        )

        api.pullResult = AilianceApiClient.ApiResult.Ok(
            SyncPullResponse(
                serverTime = "2026-09-15T10:00:00Z",
                interactions = listOf(
                    InteractionDto(
                        id = "int-err-2",
                        contactId = "contact-srv-1",
                        typeInteraction = "note_vocale",
                        sujet = "Note",
                        creeLe = "2026-09-14T08:00:00Z",
                        misAJourLe = "2026-09-15T09:00:00Z",
                        transcription = "echec",
                        transcriptionErreur = "modèle introuvable",
                    ),
                ),
            ),
        )

        engine.syncNow()

        val updated = db.interactionDao().getByServerId("int-err-2")
        assertEquals("echec", updated?.transcriptionStatut)
        assertEquals("modèle introuvable", updated?.transcriptionErreur)
    }

    // ---- Pull note_projet avec transcriptionErreur ----

    @Test
    fun pull_noteProjet_avecTranscriptionErreur_stockeChamp() = runBlocking {
        api.pullResult = AilianceApiClient.ApiResult.Ok(
            SyncPullResponse(
                serverTime = "2026-09-15T10:00:00Z",
                notes = listOf(
                    NoteProjetDto(
                        id = "note-err-1",
                        projetId = "projet-srv-1",
                        titre = "Note en échec",
                        contenu = "",
                        auteur = "alice",
                        creeLe = "2026-09-14T08:00:00Z",
                        transcription = "echec",
                        transcriptionErreur = "audio corrompu",
                    ),
                ),
            ),
        )

        engine.syncNow()

        val stored = db.noteProjetDao().getByServerId("note-err-1")
        assertNotNull(stored)
        assertEquals("echec", stored?.transcriptionStatut)
        assertEquals("audio corrompu", stored?.transcriptionErreur)
    }

    @Test
    fun pull_noteProjet_transcriptionTerminee_erreurEffacee() = runBlocking {
        db.noteProjetDao().upsert(
            NoteProjetEntity(
                serverId = "note-err-2",
                projetServerId = "projet-srv-1",
                titre = "Note",
                texte = "",
                auteur = "alice",
                transcriptionStatut = "echec",
                transcriptionErreur = "ancienne erreur",
                createdAt = parseIsoToEpochMs("2026-09-14T08:00:00Z"),
                updatedAt = parseIsoToEpochMs("2026-09-14T08:00:00Z"),
            ),
        )

        api.pullResult = AilianceApiClient.ApiResult.Ok(
            SyncPullResponse(
                serverTime = "2026-09-15T10:00:00Z",
                notes = listOf(
                    NoteProjetDto(
                        id = "note-err-2",
                        projetId = "projet-srv-1",
                        titre = "Note",
                        contenu = "Transcription OK.",
                        auteur = "alice",
                        creeLe = "2026-09-14T08:00:00Z",
                        majLe = "2026-09-15T09:00:00Z",
                        transcription = "terminee",
                        transcriptionErreur = null,
                    ),
                ),
            ),
        )

        engine.syncNow()

        val updated = db.noteProjetDao().getByServerId("note-err-2")
        assertEquals("terminee", updated?.transcriptionStatut)
        assertNull(updated?.transcriptionErreur)
    }

    // ---- FakeAilianceApi relance (smoke tests) ----

    @Test
    fun fake_relancerTranscriptionInteraction_enregistreAppel() {
        val result = api.relancerTranscriptionInteraction("contact-srv-1", "iid-srv-1")

        assertEquals(AilianceApiClient.ApiResult.Ok(Unit), result)
        assertEquals(1, api.relancerTranscriptionInteractionCalls.size)
        assertEquals("contact-srv-1" to "iid-srv-1", api.relancerTranscriptionInteractionCalls[0])
    }

    @Test
    fun fake_relancerTranscriptionNote_enregistreAppel() {
        val result = api.relancerTranscriptionNote("projet-srv-1", "note-srv-1")

        assertEquals(AilianceApiClient.ApiResult.Ok(Unit), result)
        assertEquals(1, api.relancerTranscriptionNoteCalls.size)
        assertEquals("projet-srv-1" to "note-srv-1", api.relancerTranscriptionNoteCalls[0])
    }
}