package com.correx.apps.server.memory

import com.correx.core.config.ProjectConfig
import com.correx.core.events.events.EventMetadata
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.SteeringNoteAddedEvent
import com.correx.core.events.events.TransitionExecutedEvent
import com.correx.core.events.types.EventId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.events.types.TransitionId
import com.correx.core.inference.Embedder
import com.correx.core.journal.DecisionJournalProjector
import com.correx.core.journal.DefaultDecisionJournalReducer
import com.correx.core.journal.DefaultDecisionJournalRepository
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.Clock
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.util.UUID

private class OnesEmbedder(override val dimension: Int = 8) : Embedder {
    override suspend fun embed(text: String): FloatArray = FloatArray(dimension) { 1f }
}

class ProjectMemoryServiceTest {

    private fun store(
        sessionId: SessionId,
        payload: com.correx.core.events.events.EventPayload,
        es: InMemoryEventStore,
    ) =
        runBlocking {
            es.append(
                NewEvent(
                    EventMetadata(EventId(UUID.randomUUID().toString()), sessionId, Clock.System.now(), 1, null, null),
                    payload,
                ),
            )
        }

    private fun service(config: ProjectConfig, es: InMemoryEventStore, l3: InMemoryL3MemoryStore) =
        ProjectMemoryService(
            config = config,
            embedder = OnesEmbedder(),
            l3MemoryStore = l3,
            journalRepository = DefaultDecisionJournalRepository(
                DefaultEventReplayer(es, DecisionJournalProjector(DefaultDecisionJournalReducer())),
            ),
            eventStore = es,
        )

    @Test
    fun `decisions persisted in one session are retrieved and seeded in the next`(): Unit = runBlocking {
        val eventStore = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val config = ProjectConfig(enabled = true, memoryK = 5)

        val sessionA = SessionId("A")
        store(sessionA, SteeringNoteAddedEvent(sessionA, "use jwt for auth"), eventStore)
        store(
            sessionA,
            TransitionExecutedEvent(sessionA, StageId("plan"), StageId("impl"), TransitionId("t1")),
            eventStore,
        )

        service(config, eventStore, l3).persist(sessionA, "/repo")

        val sessionB = SessionId("B")
        val seeded = service(config, eventStore, l3).retrieveAndSeed(sessionB, "/repo")

        assertTrue(seeded.any { it.contains("use jwt for auth") }, "expected prior decision retrieved")
        val note = eventStore.read(sessionB).mapNotNull { it.payload as? SteeringNoteAddedEvent }.firstOrNull()
        assertTrue(
            note != null && note.content.contains("Project memory"),
            "expected seeded steering note in session B",
        )
    }

    @Test
    fun `disabled project memory is a no-op`(): Unit = runBlocking {
        val eventStore = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val config = ProjectConfig(enabled = false)

        val sessionA = SessionId("A")
        store(sessionA, SteeringNoteAddedEvent(sessionA, "secret"), eventStore)
        service(config, eventStore, l3).persist(sessionA, "/repo")

        val sessionB = SessionId("B")
        val seeded = service(config, eventStore, l3).retrieveAndSeed(sessionB, "/repo")

        assertTrue(seeded.isEmpty())
        assertFalse(eventStore.read(sessionB).any { it.payload is SteeringNoteAddedEvent })
    }
}
