import com.correx.apps.server.memory.FakeWorkspaceStateProbe
import com.correx.apps.server.memory.L3RepoKnowledgeRetriever
import com.correx.apps.server.memory.ProjectMemoryService
import com.correx.apps.server.memory.RepoMapIndexerPort
import com.correx.apps.server.memory.WorkspaceState
import com.correx.core.config.ProjectConfig
import com.correx.core.events.events.RepoMapEntry
import com.correx.core.events.types.SessionId
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 org.junit.jupiter.api.Assertions.assertEquals
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
    }
}

/**
 * Deterministic embedder: encodes each text as a stable float vector using char codepoint sums.
 * Guarantees non-trivial cosine similarity separation between distinct texts while keeping
 * retrieval deterministic (no RNG, no network).
 */
private class DeterministicEmbedder(override val dimension: Int = 8) : Embedder {
    override suspend fun embed(text: String): FloatArray {
        val v = FloatArray(dimension)
        text.forEachIndexed { i, c -> v[i % dimension] += c.code.toFloat() }
        val norm = kotlin.math.sqrt(v.map { it * it }.sum()).coerceAtLeast(1e-6f)
        return FloatArray(dimension) { v[it] / norm }
    }
}

/**
 * Integration test: two sessions sharing the same workspace stateKey and L3 store.
 *
 * Proves:
 * 1. Exactly ONE index walk across both sessions (state-keyed reuse via L3 presence check).
 * 2. The second session's retriever returns non-empty hits from the shared L3 embeddings —
 *    demonstrating that repo context is available without a per-session RepoMapComputedEvent.
 *
 * Note: this test depends on apps:server (ProjectMemoryService, L3RepoKnowledgeRetriever,
 * FakeWorkspaceStateProbe) and core:router (InMemoryL3MemoryStore). These were added to
 * testing/integration's classpath to allow end-to-end coverage without duplicating the
 * production classes inline.
 */
class RepoMapReuseIntegrationTest {

    private val repoRoot = "/repo-integration"
    private val stateKey = "git:abc123"
    private val entries = listOf(
        RepoMapEntry(path = "src/Parser.kt", score = 1.0, symbols = listOf("Parser", "Token")),
        RepoMapEntry(path = "src/Lexer.kt", score = 0.8, symbols = listOf("Lexer")),
    )

    @Test
    fun `same stateKey — exactly one index walk across two sessions`(): Unit = runBlocking {
        val es = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val indexer = CountingIndexer(entries)
        val embedder = DeterministicEmbedder()
        val probe = FakeWorkspaceStateProbe(WorkspaceState(stateKey, "git", "main", false))

        val svc = buildService(es, l3, indexer, embedder, probe)
        val sessionA = SessionId("integration-reuse-a")
        val sessionB = SessionId("integration-reuse-b")

        svc.observeAndRecord(sessionA, repoRoot)
        svc.indexAndRecord(sessionA, repoRoot)
        assertEquals(1, indexer.callCount, "first session must trigger index walk")

        svc.observeAndRecord(sessionB, repoRoot)
        svc.indexAndRecord(sessionB, repoRoot)
        assertEquals(1, indexer.callCount, "second session with same stateKey must skip index walk")
    }

    @Test
    fun `second session retrieves repo content from shared L3 embeddings`(): Unit = runBlocking {
        val es = InMemoryEventStore()
        val l3 = InMemoryL3MemoryStore()
        val indexer = CountingIndexer(entries)
        val embedder = DeterministicEmbedder()
        val probe = FakeWorkspaceStateProbe(WorkspaceState(stateKey, "git", "main", false))

        val svc = buildService(es, l3, indexer, embedder, probe)
        val sessionA = SessionId("integration-retrieve-a")
        val sessionB = SessionId("integration-retrieve-b")

        // Session A: observe + index → entries embedded into shared L3.
        svc.observeAndRecord(sessionA, repoRoot)
        svc.indexAndRecord(sessionA, repoRoot)

        // Session B: observe + skip scan (stateKey reuse).
        svc.observeAndRecord(sessionB, repoRoot)
        svc.indexAndRecord(sessionB, repoRoot)

        // Session B has no per-session RepoMapComputedEvent but retriever reads shared L3.
        val retriever = L3RepoKnowledgeRetriever(
            embedder = embedder,
            l3MemoryStore = l3,
            repoRoot = repoRoot,
        )
        val hits = retriever.retrieve(sessionB, "parse the input", k = 5)

        assertTrue(hits.isNotEmpty(), "second session must receive repo content from shared L3 embeddings")
        val allText = hits.joinToString(" ") { it.text }
        assertTrue(
            allText.contains("src/Parser.kt") || allText.contains("src/Lexer.kt"),
            "retrieved hits must reference indexed files; got: $allText",
        )
    }

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