package com.correx.core.tasks

import com.correx.core.events.types.ProjectId
import com.correx.core.events.types.TaskId
import com.correx.core.events.types.TaskLinkType
import com.correx.core.events.types.TaskNoteAuthor
import com.correx.core.events.types.TaskTargetKind
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class TaskServiceTest {

    private val store = InMemoryEventStore()
    private val service = TaskService(store)
    private val project = ProjectId("auth")

    @Test
    fun `createTask allocates sequential human keys per project`() = runBlocking {
        val first = service.createTask(project, "Add JWT refresh", "users stay authenticated")
        val second = service.createTask(project, "Rotate keys", "tokens rotate")

        assertEquals("auth-1", first.taskId.value)
        assertEquals("auth-2", second.taskId.value)
        assertEquals(TaskStatus.TODO, first.state.status)
        assertEquals("Add JWT refresh", first.state.title)
    }

    @Test
    fun `lifecycle round-trips through append and replay`() = runBlocking {
        val task = service.createTask(project, "Add JWT refresh", "users stay authenticated")
        val id = task.taskId

        assertEquals(TaskStatus.IN_PROGRESS, service.claim(id, "claude-opus")?.state?.status)
        assertEquals(TaskStatus.IN_REVIEW, service.submitForReview(id)?.state?.status)
        val done = service.complete(id)

        assertEquals(TaskStatus.DONE, done?.state?.status)
        assertEquals("claude-opus", done?.state?.claimant)
    }

    @Test
    fun `links and notes are persisted on the task`() = runBlocking {
        val id = service.createTask(project, "Add JWT refresh", "auth").taskId

        service.link(id, targetId = "adr-7", type = TaskLinkType.IMPLEMENTS, targetKind = TaskTargetKind.DOC)
        val withNote = service.addNote(id, TaskNoteAuthor.AGENT, "migration failed, retrying")

        assertEquals(
            TaskLink("adr-7", TaskLinkType.IMPLEMENTS, TaskTargetKind.DOC),
            withNote?.state?.links?.single(),
        )
        assertEquals("migration failed, retrying", withNote?.state?.notes?.single()?.body)
    }

    @Test
    fun `delete is a soft tombstone hidden from reads`() = runBlocking {
        val id = service.createTask(project, "throwaway", "oops").taskId

        assertTrue(service.delete(id))
        assertNull(service.getTask(id))
        assertTrue(service.list(project).none { it.taskId == id })
        // A second delete is a no-op: the task is already gone from reads.
        assertFalse(service.delete(id))
    }

    @Test
    fun `mutating an unknown task returns null`() = runBlocking {
        assertNull(service.claim(TaskId("auth-999"), "claude-opus"))
    }

    @Test
    fun `session resolution drives the deterministic claim loop`() = runBlocking {
        // Two tasks filed under this session (via the CONTEXT/SESSION link the tools record), one
        // unrelated. The loop must resolve the project from those links, regardless of its key.
        val session = "sess-1"
        val a = service.createTask(project, "first", "g", affectedPaths = listOf("src/a/**")).taskId
        val b = service.createTask(project, "second", "g").taskId
        service.link(a, session, TaskLinkType.CONTEXT, TaskTargetKind.SESSION)
        service.link(b, session, TaskLinkType.CONTEXT, TaskTargetKind.SESSION)
        service.createTask(ProjectId("other"), "noise", "g")

        assertEquals(project, service.projectForSession(session))
        // Deterministic order: lowest key sequence first.
        assertEquals(a, service.nextReadyForSession(session)?.taskId)

        // Claiming moves it off ready() so the predicate eventually terminates, and becomes the
        // session's active claim carrying its write scope.
        service.claim(a, session)
        assertEquals(b, service.nextReadyForSession(session)?.taskId)
        assertEquals(a, service.activeClaim(session)?.taskId)
        assertEquals(listOf("src/a/**"), service.activeClaim(session)?.state?.affectedPaths)

        assertNull(service.projectForSession("unknown-session"))
    }

    @Test
    fun `listAll spans every project's stream`() = runBlocking {
        service.createTask(project, "a", "g")
        service.createTask(ProjectId("billing"), "b", "g")

        assertEquals(setOf("auth-1", "billing-1"), service.listAll().map { it.taskId.value }.toSet())
    }

    @Test
    fun `keys keep incrementing even after deletion`() = runBlocking {
        val first = service.createTask(project, "one", "g").taskId
        service.delete(first)
        val third = service.createTask(project, "two", "g")

        // count() is creation-based, so the deleted id is never reused.
        assertEquals("auth-2", third.taskId.value)
    }

    @Test
    fun `history returns the task's own events in order`() = runBlocking {
        val id = service.createTask(project, "Add JWT refresh", "auth").taskId
        service.claim(id, "claude-opus")
        service.submitForReview(id)
        service.complete(id)
        // A second task in the same stream must not bleed into this one's history.
        service.createTask(project, "unrelated", "g")

        val lines = TaskHistory.render(service.history(id)).lines()
        assertEquals(4, lines.size)
        assertTrue(lines.first().contains("created"))
        assertTrue(lines.last().contains("completed"))
    }

    @Test
    fun `findDuplicates matches active same-title tasks but ignores terminal ones`() = runBlocking {
        service.createTask(project, "Add JWT refresh", "g") // auth-1, active

        // Case- and whitespace-insensitive match.
        assertEquals(
            listOf("auth-1"),
            service.findDuplicates(project, "  add   jwt REFRESH ").map { it.taskId.value },
        )

        // A DONE look-alike is not a duplicate — that work may legitimately recur.
        val done = service.createTask(project, "Rotate keys", "g").taskId
        service.claim(done, "x"); service.submitForReview(done); service.complete(done)
        assertTrue(service.findDuplicates(project, "Rotate keys").isEmpty())

        assertTrue(service.findDuplicates(project, "totally new").isEmpty())
    }

    @Test
    fun `ready surfaces unblocked work and blockers explain the wait`() = runBlocking {
        val a = service.createTask(project, "A", "g").taskId // auth-1
        val b = service.createTask(project, "B", "g").taskId // auth-2 depends on auth-1
        service.link(b, a.value, TaskLinkType.DEPENDS_ON, TaskTargetKind.TASK)

        assertEquals(setOf("auth-1"), service.ready(project).map { it.taskId.value }.toSet())
        assertEquals(listOf("auth-1"), service.blockers(b).map { it.taskId.value })
        assertEquals(listOf("auth-2"), service.blocking(a).map { it.taskId.value })

        // Finishing auth-1 unblocks auth-2.
        service.claim(a, "x"); service.submitForReview(a); service.complete(a)
        assertEquals(setOf("auth-2"), service.ready(project).map { it.taskId.value }.toSet())
        assertTrue(service.blockers(b).isEmpty())
    }

    @Test
    fun `history survives soft-delete but is empty for an unknown task`() = runBlocking {
        val id = service.createTask(project, "throwaway", "oops").taskId
        service.delete(id)

        // getTask hides a deleted task, but its audit trail (incl. the deletion) remains.
        assertNull(service.getTask(id))
        assertTrue(service.history(id).isNotEmpty())
        assertTrue(service.history(TaskId("auth-999")).isEmpty())
    }
}
