package com.correx.apps.server.memory

import com.correx.core.config.ProjectConfig
import com.correx.core.events.events.RepoMapEntry
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.core.events.types.SessionId
import com.correx.infrastructure.persistence.InMemoryEventStore
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.nio.file.Path

private class CountingIndexer(private val entries: List<RepoMapEntry>) : RepoMapIndexerPort {
    var callCount = 0
    override fun index(repoRoot: Path): List<RepoMapEntry> {
        callCount++
        return entries
    }
}

private class ConstantEmbedderReuse(override val dimension: Int = 4) : Embedder {
    override suspend fun embed(text: String): FloatArray = FloatArray(dimension) { 0.5f }
}

class ProjectMemoryServiceReuseTest {

    private val entries = listOf(
        RepoMapEntry(path = "src/Foo.kt", score = 1.0, symbols = listOf("FooClass")),
    )

    private fun service(
        es: InMemoryEventStore,
        l3: InMemoryL3MemoryStore,
        indexer: CountingIndexer,
        probe: WorkspaceStateProbe,
    ) = ProjectMemoryService(
        config = ProjectConfig(enabled = true),
        embedder = ConstantEmbedderReuse(),
        l3MemoryStore = l3,
        journalRepository = DefaultDecisionJournalRepository(
            DefaultEventReplayer(es, DecisionJournalProjector(DefaultDecisionJournalReducer())),
        ),
        eventStore = es,
        indexer = indexer,
        workspaceStateProbe = probe,
    )

    @Test
    fun `same stateKey skips re-index on second session`(): Unit = runBlocking {
        val es = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val indexer = CountingIndexer(entries)
        val probe = FakeWorkspaceStateProbe(WorkspaceState("git:hash1", "git", "main", false))

        val svc = service(es, l3, indexer, probe)
        val sessionA = SessionId("session-reuse-a")
        val sessionB = SessionId("session-reuse-b")

        svc.observeAndRecord(sessionA, "/repo")
        svc.indexAndRecord(sessionA, "/repo")
        assertEquals(1, indexer.callCount, "first session should index")

        svc.observeAndRecord(sessionB, "/repo")
        svc.indexAndRecord(sessionB, "/repo")
        assertEquals(1, indexer.callCount, "second session with same stateKey should skip index")
    }

    @Test
    fun `changed stateKey re-indexes`(): Unit = runBlocking {
        val es = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val indexer = CountingIndexer(entries)

        val svc1 = service(es, l3, indexer, FakeWorkspaceStateProbe(WorkspaceState("git:hash1", "git", "main", false)))
        val sessionA = SessionId("session-change-a")
        svc1.observeAndRecord(sessionA, "/repo")
        svc1.indexAndRecord(sessionA, "/repo")
        assertEquals(1, indexer.callCount)

        val svc2 = service(es, l3, indexer, FakeWorkspaceStateProbe(WorkspaceState("git:hash2", "git", "main", false)))
        val sessionB = SessionId("session-change-b")
        svc2.observeAndRecord(sessionB, "/repo")
        svc2.indexAndRecord(sessionB, "/repo")
        assertEquals(2, indexer.callCount, "changed stateKey should trigger re-index")
    }

    @Test
    fun `no observation event always re-indexes`(): Unit = runBlocking {
        val es = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val indexer = CountingIndexer(entries)
        val probe = FakeWorkspaceStateProbe(null)

        val svc = service(es, l3, indexer, probe)
        svc.indexAndRecord(SessionId("session-noobs-1"), "/repo")
        svc.indexAndRecord(SessionId("session-noobs-2"), "/repo")
        assertEquals(2, indexer.callCount, "no observation event -> always re-index")
    }

    @Test
    fun `entries are embedded with stateKey turnId tag after fresh index`(): Unit = runBlocking {
        val es = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val indexer = CountingIndexer(entries)
        val probe = FakeWorkspaceStateProbe(WorkspaceState("git:hash1", "git", "main", false))

        val svc = service(es, l3, indexer, probe)
        val sessionId = SessionId("session-embed-1")
        svc.observeAndRecord(sessionId, "/repo")
        svc.indexAndRecord(sessionId, "/repo")

        assertTrue(
            l3.existsByTurnIdPrefix("repomap:v2:/repo:git:hash1"),
            "entries should be embedded with stateKey tag",
        )
    }

    @Test
    fun `repoRoot prefix does not collide across roots — repo vs repo2`(): Unit = runBlocking {
        val es = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val probe = FakeWorkspaceStateProbe(WorkspaceState("git:hash1", "git", "main", false))
        val embedder = ConstantEmbedderReuse()

        // Index /repo and /repo2 into the same shared L3 store with the same stateKey.
        // The versioned trailing-':' delimiter on the repomap tag must keep their entries from bleeding
        // across roots: "/repo" must NOT match tags written for "/repo2".
        val repoEntries = listOf(RepoMapEntry(path = "src/Repo.kt", score = 1.0, symbols = listOf("RepoClass")))
        val repo2Entries = listOf(RepoMapEntry(path = "src/Repo2.kt", score = 1.0, symbols = listOf("Repo2Class")))

        service(es, l3, CountingIndexer(repoEntries), probe)
            .indexAndRecord(SessionId("session-collide-repo"), "/repo")
        service(es, l3, CountingIndexer(repo2Entries), probe)
            .indexAndRecord(SessionId("session-collide-repo2"), "/repo2")

        // existsByTurnIdPrefix with the delimiter must not match the other root.
        assertTrue(l3.existsByTurnIdPrefix("repomap:v2:/repo:"), "tag for /repo should exist")
        assertTrue(l3.existsByTurnIdPrefix("repomap:v2:/repo2:"), "tag for /repo2 should exist")

        // The retriever scoped to /repo must return only /repo's entry, never /repo2's.
        val hits = L3RepoKnowledgeRetriever(embedder = embedder, l3MemoryStore = l3, repoRoot = "/repo")
            .retrieve(SessionId("session-collide-query"), "anything", k = 10)

        assertTrue(hits.isNotEmpty(), "retriever for /repo should find /repo entries")
        val text = hits.joinToString(" ") { it.text }
        assertTrue(text.contains("src/Repo.kt"), "should contain /repo entry; got: $text")
        assertFalse(text.contains("src/Repo2.kt"), "must NOT contain /repo2 entry (prefix collision); got: $text")
    }
}
