package com.correx.infrastructure.artifactscas.recovery

import com.correx.infrastructure.artifactscas.CasArtifactStore
import com.correx.infrastructure.artifactscas.config.CasConfig
import com.correx.infrastructure.artifactscas.index.SqliteArtifactIndex
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Files
import java.nio.file.Path
import java.sql.DriverManager
import kotlin.io.path.listDirectoryEntries

class TailScannerTest {

    private fun newIndex(dir: Path) = SqliteArtifactIndex(dir.resolve("index.sqlite"))

    @Test
    fun `tail beyond known index is re-added on open`(@TempDir dir: Path): Unit = runBlocking {
        val payload = "recover me".toByteArray()
        val id = CasArtifactStore.open(CasConfig(dir), newIndex(dir)).use { store ->
            val id = store.put(payload)
            store.flushBefore {}
            id
        }
        // Simulate lost index: wipe the rows.
        DriverManager.getConnection("jdbc:sqlite:${dir.resolve("index.sqlite").toAbsolutePath()}").use { conn ->
            conn.createStatement().use { it.execute("DELETE FROM artifacts;") }
        }
        CasArtifactStore.open(CasConfig(dir), newIndex(dir)).use { reopened ->
            assertArrayEquals(payload, reopened.get(id))
        }
    }

    @Test
    fun `corrupt tail is truncated on open`(@TempDir dir: Path): Unit = runBlocking {
        val p1 = "first".toByteArray()
        val p2 = "second".toByteArray()
        val (id1, id2) = CasArtifactStore.open(CasConfig(dir), newIndex(dir)).use { store ->
            val a = store.put(p1)
            val b = store.put(p2)
            store.flushBefore {}
            a to b
        }
        val segFile = dir.resolve("segments").listDirectoryEntries("*.seg").single()
        val cleanSize = Files.size(segFile)
        // Append garbage that will fail header/length validation.
        Files.write(
            segFile,
            ByteArray(GARBAGE_BYTES) { 0xAB.toByte() },
            java.nio.file.StandardOpenOption.APPEND,
        )
        CasArtifactStore.open(CasConfig(dir), newIndex(dir)).use { reopened ->
            assertArrayEquals(p1, reopened.get(id1))
            assertArrayEquals(p2, reopened.get(id2))
        }
        assertEquals(cleanSize, Files.size(segFile))
    }

    @Test
    fun `recover returns null when no segments dir exists`(@TempDir dir: Path): Unit = runBlocking {
        val root = dir.resolve("fresh")
        Files.createDirectories(root)
        newIndex(root).use { idx ->
            val scanner = TailScanner(root.resolve("segments"), idx)
            assertNull(scanner.recover())
        }
        // And open() still succeeds despite no prior segments.
        CasArtifactStore.open(CasConfig(root), newIndex(root)).use { store ->
            assertNotNull(store)
        }
    }

    private companion object {
        const val GARBAGE_BYTES = 20
    }
}
