package com.correx.infrastructure.artifactscas

import com.correx.core.utils.TypeId
import com.correx.infrastructure.artifactscas.config.CasConfig
import com.correx.infrastructure.artifactscas.index.SqliteArtifactIndex
import com.correx.infrastructure.artifactscas.segment.CorruptRecordException
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.listDirectoryEntries
import kotlin.random.Random

class CasArtifactStoreTest {

    private fun newStore(dir: Path, maxSegmentBytes: Long = 64L * 1024 * 1024): CasArtifactStore {
        val index = SqliteArtifactIndex(dir.resolve("index.sqlite"))
        return CasArtifactStore(CasConfig(dir, maxSegmentBytes), index)
    }

    @Test
    fun `roundtrip put then get returns same bytes`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir).use { store ->
            val small = "hello cas".toByteArray()
            val big = Random(1).nextBytes(1024 * 1024)
            val idSmall = store.put(small)
            val idBig = store.put(big)
            assertArrayEquals(small, store.get(idSmall))
            assertArrayEquals(big, store.get(idBig))
        }
    }

    private fun segmentsTotalSize(dir: Path): Long =
        Files.walk(dir.resolve("segments"))
            .filter { Files.isRegularFile(it) }
            .mapToLong { Files.size(it) }
            .sum()

    @Test
    fun `idempotent put returns same id and does not grow segments`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir).use { store ->
            val payload = Random(2).nextBytes(500)
            val id1 = store.put(payload)
            val sizeAfterFirst = segmentsTotalSize(dir)
            val id2 = store.put(payload)
            val sizeAfterSecond = segmentsTotalSize(dir)
            assertEquals(id1, id2)
            assertEquals(sizeAfterFirst, sizeAfterSecond)
        }
    }

    @Test
    fun `get for unknown id returns null`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir).use { store ->
            val bogus = TypeId("00".repeat(32))
            assertNull(store.get(bogus))
        }
    }

    @Test
    fun `restart after fsync reopened store reads previous bytes`(@TempDir dir: Path): Unit = runBlocking {
        val payload = "persistent".toByteArray()
        val id = newStore(dir).use { store ->
            val id = store.put(payload)
            store.flushBefore { /* no-op event commit */ }
            id
        }
        newStore(dir).use { reopened ->
            assertArrayEquals(payload, reopened.get(id))
        }
    }

    @Test
    fun `rotation exceeding threshold opens a new segment`(@TempDir dir: Path): Unit = runBlocking {
        newStore(dir, maxSegmentBytes = 1024).use { store ->
            store.put(Random(3).nextBytes(600))
            store.put(Random(4).nextBytes(600))
            val files = dir.resolve("segments").listDirectoryEntries("*.seg")
            assertEquals(2, files.size, "expected rotation, found segments: ${files.map { it.fileName }}")
        }
    }

    @Test
    fun `get throws CorruptRecordException when segment payload is tampered`(@TempDir dir: Path): Unit = runBlocking {
        val id = newStore(dir).use { store ->
            store.put("tamper me".toByteArray()).also { store.flushBefore {} }
        }
        val segFile = dir.resolve("segments").listDirectoryEntries("*.seg").single()
        val bytes = Files.readAllBytes(segFile)
        bytes[bytes.size - 1] = (bytes[bytes.size - 1].toInt() xor 0xFF).toByte()
        Files.write(segFile, bytes)
        newStore(dir).use { reopened ->
            assertThrows(CorruptRecordException::class.java) { runBlocking { reopened.get(id) } }
        }
    }
}
