package com.correx.apps.server.routes

import com.correx.apps.server.ServerModule
import com.correx.apps.server.configureServer
import com.correx.apps.server.registry.ProviderRegistry
import com.correx.apps.server.registry.WorkflowRegistry
import com.correx.apps.server.registry.WorkflowSummary
import com.correx.apps.server.undo.SessionUndoService
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.context.builder.DefaultContextPackBuilder
import com.correx.core.context.compression.DefaultContextCompressor
import com.correx.core.events.EventDispatcher
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.ArtifactId
import com.correx.core.inference.InferenceProjector
import com.correx.core.inference.InferenceProvider
import com.correx.core.inference.InferenceRepository
import com.correx.core.inference.ProviderHealth
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
import com.correx.core.kernel.orchestration.OrchestrationConfig
import com.correx.core.kernel.orchestration.OrchestrationProjector
import com.correx.core.kernel.orchestration.OrchestrationRepository
import com.correx.core.kernel.orchestration.OrchestratorEngines
import com.correx.core.kernel.orchestration.OrchestratorRepositories
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.events.types.ProviderId
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.transitions.graph.WorkflowGraph
import com.correx.core.transitions.resolution.DefaultTransitionResolver
import com.correx.core.validation.pipeline.ValidationPipeline
import com.correx.infrastructure.InfrastructureModule
import com.correx.infrastructure.inference.commons.UnavailableProbe
import com.correx.infrastructure.persistence.InMemoryEventStore
import com.correx.infrastructure.tools.FileEditConfig
import com.correx.infrastructure.tools.FileReadConfig
import com.correx.infrastructure.tools.FileWriteConfig
import com.correx.infrastructure.tools.ShellConfig
import com.correx.infrastructure.tools.ToolConfig
import io.ktor.client.HttpClient
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.server.testing.testApplication
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.nio.file.Path

class TaskRoutesTest {

    private val testJson = Json { ignoreUnknownKeys = true }

    private suspend fun HttpResponse.json(): JsonObject =
        testJson.parseToJsonElement(bodyAsText()) as JsonObject

    private suspend fun HttpClient.createTask(project: String = "demo"): JsonObject =
        post("/tasks") {
            contentType(ContentType.Application.Json)
            setBody("""{"project":"$project","title":"JWT refresh","goal":"users stay authed"}""")
        }.json()

    @Test
    fun `POST creates a task and GET lists and fetches it`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}

            val created = client.post("/tasks") {
                contentType(ContentType.Application.Json)
                setBody(
                    """{"project":"demo","title":"JWT refresh","goal":"stay authed",
                       "acceptanceCriteria":["rotates"],"affectedPaths":["auth/**"]}""",
                )
            }
            assertEquals(HttpStatusCode.Created, created.status)
            val body = created.json()
            assertEquals("demo-1", body["id"]!!.jsonPrimitive.content)
            assertEquals("TODO", body["status"]!!.jsonPrimitive.content)
            assertEquals("rotates", body["acceptanceCriteria"]!!.jsonArray.single().jsonPrimitive.content)

            val list = client.get("/tasks?project=demo")
            assertEquals(HttpStatusCode.OK, list.status)
            val rows = testJson.parseToJsonElement(list.bodyAsText()) as JsonArray
            assertEquals(1, rows.size)
            assertEquals("demo-1", (rows[0] as JsonObject)["id"]!!.jsonPrimitive.content)

            // No project → cross-project board lists it too.
            val all = client.get("/tasks")
            assertEquals(HttpStatusCode.OK, all.status)
            val allRows = testJson.parseToJsonElement(all.bodyAsText()) as JsonArray
            assertEquals(1, allRows.size)
            assertEquals("demo-1", (allRows[0] as JsonObject)["id"]!!.jsonPrimitive.content)

            val fetched = client.get("/tasks/demo-1")
            assertEquals(HttpStatusCode.OK, fetched.status)
            assertEquals("JWT refresh", fetched.json()["title"]!!.jsonPrimitive.content)
        }
    }

    @Test
    fun `GET tasks reports dependency-graph ready and blockedBy`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask() // demo-1 (TODO, no deps)
            client.post("/tasks") {
                contentType(ContentType.Application.Json)
                setBody("""{"project":"demo","title":"Rotate keys","goal":"rotate signing keys"}""")
            } // demo-2
            // demo-2 DEPENDS_ON demo-1 (TASK kind inferred from the id).
            client.post("/tasks/demo-2/links") {
                contentType(ContentType.Application.Json)
                setBody("""{"targetId":"demo-1","type":"DEPENDS_ON"}""")
            }

            val rows = testJson.parseToJsonElement(client.get("/tasks?project=demo").bodyAsText()) as JsonArray
            val byId = rows.associate { (it as JsonObject)["id"]!!.jsonPrimitive.content to it }
            val one = byId.getValue("demo-1") as JsonObject
            val two = byId.getValue("demo-2") as JsonObject
            // default-valued fields are omitted from JSON (encodeDefaults=false), which the TUI reads
            // back as zero values — so tolerate absence here too.
            fun readyOf(o: JsonObject) = o["ready"]?.jsonPrimitive?.content == "true"
            fun blockedByOf(o: JsonObject): List<String> =
                (o["blockedBy"] as? JsonArray)?.map { it.jsonPrimitive.content } ?: emptyList()
            // demo-1: TODO with nothing in its way → ready, no blockers.
            assertTrue(readyOf(one))
            assertTrue(blockedByOf(one).isEmpty())
            // demo-2: waits on the unfinished demo-1 → not ready, blockedBy = [demo-1].
            assertTrue(!readyOf(two))
            assertEquals(listOf("demo-1"), blockedByOf(two))
        }
    }

    @Test
    fun `lifecycle transitions move a task to DONE`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask()

            val claimed = client.post("/tasks/demo-1/claim") {
                contentType(ContentType.Application.Json)
                setBody("""{"claimant":"claude-opus"}""")
            }
            assertEquals("IN_PROGRESS", claimed.json()["status"]!!.jsonPrimitive.content)
            assertEquals("claude-opus", claimed.json()["claimant"]!!.jsonPrimitive.content)

            val reviewed = client.post("/tasks/demo-1/submit-for-review").json()
            assertEquals("IN_REVIEW", reviewed["status"]!!.jsonPrimitive.content)
            val completed = client.post("/tasks/demo-1/complete").json()
            assertEquals("DONE", completed["status"]!!.jsonPrimitive.content)
        }
    }

    @Test
    fun `PATCH edits fields and POST links and notes mutate the graph`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask()

            val patched = client.patch("/tasks/demo-1") {
                contentType(ContentType.Application.Json)
                setBody("""{"title":"JWT refresh v2"}""")
            }
            assertEquals("JWT refresh v2", patched.json()["title"]!!.jsonPrimitive.content)

            // No explicit kind: an ADR id infers to DOC.
            val linked = client.post("/tasks/demo-1/links") {
                contentType(ContentType.Application.Json)
                setBody("""{"targetId":"adr-7","type":"IMPLEMENTS"}""")
            }
            val link = linked.json()["links"]!!.jsonArray.single() as JsonObject
            assertEquals("adr-7", link["targetId"]!!.jsonPrimitive.content)
            assertEquals("IMPLEMENTS", link["type"]!!.jsonPrimitive.content)
            assertEquals("DOC", link["targetKind"]!!.jsonPrimitive.content)

            val noted = client.post("/tasks/demo-1/notes") {
                contentType(ContentType.Application.Json)
                setBody("""{"body":"kickoff"}""")
            }
            val note = noted.json()["notes"]!!.jsonArray.single() as JsonObject
            assertEquals("kickoff", note["body"]!!.jsonPrimitive.content)
            assertEquals("HUMAN", note["author"]!!.jsonPrimitive.content)

            // Unlink via query params clears the edge.
            val unlinked = client.delete("/tasks/demo-1/links?targetId=adr-7&type=IMPLEMENTS")
            assertTrue(unlinked.json()["links"]!!.jsonArray.isEmpty())
        }
    }

    @Test
    fun `DELETE soft-deletes and the task disappears from reads`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask()

            assertEquals(HttpStatusCode.NoContent, client.delete("/tasks/demo-1").status)
            assertEquals(HttpStatusCode.NotFound, client.get("/tasks/demo-1").status)
            assertEquals(HttpStatusCode.NotFound, client.delete("/tasks/demo-1").status)
        }
    }

    @Test
    fun `GET tasks with q returns only ranked search hits`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask() // demo-1: "JWT refresh" / "users stay authed"
            client.post("/tasks") {
                contentType(ContentType.Application.Json)
                setBody("""{"project":"demo","title":"Rotate keys","goal":"rotate signing keys"}""")
            }

            val res = client.get("/tasks?q=jwt")
            assertEquals(HttpStatusCode.OK, res.status)
            val rows = testJson.parseToJsonElement(res.bodyAsText()) as JsonArray
            assertEquals(1, rows.size)
            assertEquals("demo-1", (rows[0] as JsonObject)["id"]!!.jsonPrimitive.content)
        }
    }

    @Test
    fun `POST sync-git advances tasks from commit messages`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask() // demo-1, TODO

            val res = client.post("/tasks/sync-git") {
                contentType(ContentType.Application.Json)
                setBody("""{"commits":[{"sha":"abc1234567","message":"fixes demo-1"}]}""")
            }
            assertEquals(HttpStatusCode.OK, res.status)
            val change = (res.json()["changes"] as JsonArray).single() as JsonObject
            assertEquals("demo-1", change["key"]!!.jsonPrimitive.content)
            assertEquals("TODO", change["from"]!!.jsonPrimitive.content)
            assertEquals("DONE", change["to"]!!.jsonPrimitive.content)

            assertEquals("DONE", client.get("/tasks/demo-1").json()["status"]!!.jsonPrimitive.content)
        }
    }

    @Test
    fun `GET tasks export renders the board as markdown`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask() // demo-1

            val res = client.get("/tasks/export")
            assertEquals(HttpStatusCode.OK, res.status)
            val md = res.bodyAsText()
            assertTrue(md.startsWith("# Tasks"))
            assertTrue(md.contains("**demo-1**"))
        }
    }

    @Test
    fun `context bundle is served for a known task`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask()

            val bundle = client.get("/tasks/demo-1/context")
            assertEquals(HttpStatusCode.OK, bundle.status)
            assertEquals("demo-1", bundle.json()["id"]!!.jsonPrimitive.content)
            assertEquals(HttpStatusCode.NotFound, client.get("/tasks/demo-999/context").status)
        }
    }

    @Test
    fun `ready returns unblocked TODO tasks and blockers explains a wait`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask() // demo-1, TODO
            client.post("/tasks") {
                contentType(ContentType.Application.Json)
                setBody("""{"project":"demo","title":"second","goal":"g"}""")
            }
            // demo-2 depends on demo-1 (id infers to a TASK target).
            client.post("/tasks/demo-2/links") {
                contentType(ContentType.Application.Json)
                setBody("""{"targetId":"demo-1","type":"DEPENDS_ON"}""")
            }

            val ready = client.get("/tasks?ready=true")
            assertEquals(HttpStatusCode.OK, ready.status)
            val rows = testJson.parseToJsonElement(ready.bodyAsText()) as JsonArray
            assertEquals(1, rows.size)
            assertEquals("demo-1", (rows[0] as JsonObject)["id"]!!.jsonPrimitive.content)

            val blockers = client.get("/tasks/demo-2/blockers")
            assertEquals(HttpStatusCode.OK, blockers.status)
            val blk = testJson.parseToJsonElement(blockers.bodyAsText()) as JsonArray
            assertEquals("demo-1", (blk.single() as JsonObject)["id"]!!.jsonPrimitive.content)
        }
    }

    @Test
    fun `GET history renders the lifecycle timeline`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask() // demo-1
            client.post("/tasks/demo-1/claim") {
                contentType(ContentType.Application.Json)
                setBody("""{"claimant":"claude-opus"}""")
            }

            val res = client.get("/tasks/demo-1/history")
            assertEquals(HttpStatusCode.OK, res.status)
            val timeline = res.bodyAsText()
            assertTrue(timeline.contains("created demo-1"))
            assertTrue(timeline.contains("claimed by claude-opus"))

            // An id that never existed has no history → 404.
            assertEquals(HttpStatusCode.NotFound, client.get("/tasks/demo-999/history").status)
        }
    }

    @Test
    fun `POST rejects a duplicate title with 409 and force overrides`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask() // demo-1 "JWT refresh"

            val dup = client.post("/tasks") {
                contentType(ContentType.Application.Json)
                setBody("""{"project":"demo","title":"jwt refresh","goal":"g"}""")
            }
            assertEquals(HttpStatusCode.Conflict, dup.status)
            val rows = testJson.parseToJsonElement(dup.bodyAsText()) as JsonArray
            assertEquals("demo-1", (rows.single() as JsonObject)["id"]!!.jsonPrimitive.content)

            val forced = client.post("/tasks") {
                contentType(ContentType.Application.Json)
                setBody("""{"project":"demo","title":"jwt refresh","goal":"g","force":true}""")
            }
            assertEquals(HttpStatusCode.Created, forced.status)
        }
    }

    @Test
    fun `bad requests are rejected`(@TempDir tempDir: Path) {
        testApplication {
            application { configureServer(buildModule(tempDir)) }
            val client = createClient {}
            client.createTask()

            assertEquals(HttpStatusCode.BadRequest, client.get("/tasks?status=NONSENSE").status)
            assertEquals(HttpStatusCode.NotFound, client.get("/tasks/nope-1").status)
            val badLink = client.post("/tasks/demo-1/links") {
                contentType(ContentType.Application.Json)
                setBody("""{"targetId":"x","type":"NONSENSE"}""")
            }
            assertEquals(HttpStatusCode.BadRequest, badLink.status)
        }
    }

    // --- harness ----------------------------------------------------------------------------

    private fun buildModule(tempDir: Path): ServerModule {
        val eventStore: EventStore = InMemoryEventStore()
        val artifactStore = object : ArtifactStore {
            override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("noop")
            override suspend fun get(id: ArtifactId): ByteArray? = null
            override suspend fun flushBefore(commit: suspend () -> Unit) = commit()
        }
        val provider = InfrastructureModule.createLlamaCppProvider(
            modelId = "test-model",
            modelPath = "/dev/null",
            baseUrl = "http://127.0.0.1:1",
        )
        val providerRegistry = InfrastructureModule.createProviderRegistry(listOf(provider))
        val inferenceRouter = com.correx.core.inference.DefaultInferenceRouter(
            providerRegistry,
            com.correx.infrastructure.inference.FirstAvailableRoutingStrategy(),
        )
        val toolConfig = ToolConfig(
            shell = ShellConfig(enabled = false, allowedExecutables = emptySet(), workingDir = tempDir),
            fileRead = FileReadConfig(enabled = false, allowedPaths = setOf(tempDir)),
            fileWrite = FileWriteConfig(enabled = false, allowedPaths = setOf(tempDir), workingDir = tempDir),
            fileEdit = FileEditConfig(enabled = false, allowedPaths = setOf(tempDir), workingDir = tempDir),
        )
        val toolRegistry = InfrastructureModule.createToolRegistry(toolConfig)
        val eventDispatcher = EventDispatcher(eventStore)
        val toolExecutor = InfrastructureModule.createToolExecutor(
            registry = toolRegistry,
            eventDispatcher = eventDispatcher,
            workDir = tempDir,
            artifactStore = null,
        )
        val engines = OrchestratorEngines(
            transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
            contextPackBuilder = DefaultContextPackBuilder(DefaultContextCompressor()),
            inferenceRouter = inferenceRouter,
            validationPipeline = ValidationPipeline(validators = emptyList()),
            approvalEngine = com.correx.core.approvals.domain.DefaultApprovalEngine(),
            riskAssessor = DefaultRiskAssessor(),
            toolRegistry = toolRegistry,
            toolExecutor = toolExecutor,
        )
        val repositories = OrchestratorRepositories(
            eventStore = eventStore,
            inferenceRepository = InferenceRepository(DefaultEventReplayer(eventStore, InferenceProjector())),
            orchestrationRepository = OrchestrationRepository(
                DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())),
            ),
            sessionRepository = DefaultSessionRepository(
                DefaultEventReplayer(eventStore, SessionProjector(DefaultSessionReducer())),
            ),
            artifactRepository = InfrastructureModule.createArtifactRepository(eventStore),
            approvalRepository = InfrastructureModule.createApprovalRepository(eventStore),
        )
        val orchestrator = DefaultSessionOrchestrator(
            repositories = repositories,
            engines = engines,
            retryCoordinator = DefaultRetryCoordinator(eventStore),
            artifactStore = artifactStore,
            tokenizer = provider.tokenizer,
            decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore),
        )
        val routerFacade = InfrastructureModule.createTalkieFacade(
            eventStore = eventStore,
            inferenceRouter = inferenceRouter,
            config = TalkieConfig(),
            tokenizer = provider.tokenizer,
        )
        val noopProviderRegistry = object : ProviderRegistry {
            override fun listAll() = emptyList<InferenceProvider>()
            override suspend fun healthCheckAll() = emptyMap<ProviderId, ProviderHealth>()
        }
        val noopWorkflowRegistry = object : WorkflowRegistry {
            override fun listAll(): List<WorkflowSummary> = emptyList()
            override fun find(workflowId: String): WorkflowGraph? = null
        }
        val sessionUndoService = SessionUndoService(
            eventStore = eventStore,
            artifactStore = artifactStore,
            bootRoots = setOf(tempDir),
        )
        return ServerModule(
            orchestrator = orchestrator,
            eventStore = eventStore,
            artifactStore = artifactStore,
            sessionRepository = repositories.sessionRepository,
            workflowRegistry = noopWorkflowRegistry,
            providerRegistry = noopProviderRegistry,
            defaultOrchestrationConfig = OrchestrationConfig(sandboxRoot = tempDir),
            routerFacade = routerFacade,
            orchestrationRepository = repositories.orchestrationRepository,
            approvalRepository = repositories.approvalRepository,
            toolRegistry = toolRegistry,
            sessionUndoService = sessionUndoService,
            resourceProbe = UnavailableProbe,
        )
    }
}
