package com.tt.honeyDue.data

import com.tt.honeyDue.models.TaskResponse
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.BeforeTest

/**
 * Regression tests for the gitea#2 task-cache bug:
 * `DataManager.updateTask` was a no-op when both `_allTasks` was null AND
 * `_tasksByResidence[residenceId]` was empty — exactly the cache state
 * after a fresh register-then-bulkCreateTasks flow. The just-created
 * tasks would only appear after an app restart.
 *
 * After the fix, `updateTask` must seed `_allTasks` from empty rather
 * than skipping the update.
 */
class DataManagerTaskCacheTest {

    @BeforeTest
    fun resetState() {
        DataManager.clear()
    }

    /// Onboarding-flow scenario: brand-new user, fresh launch, no kanban
    /// has ever been fetched, then a task arrives via bulkCreateTasks →
    /// DataManager.updateTask. The new task MUST land in `_allTasks` and
    /// be visible to any observer.
    @Test
    fun updateTask_seedsAllTasks_whenCacheIsEmpty() {
        // Given: fresh DataManager, kanban never loaded
        assertEquals(null, DataManager.allTasks.value, "_allTasks must start null after clear()")

        // When: a new task arrives via the same path bulkCreateTasks uses
        DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "upcoming_tasks"))

        // Then: _allTasks must contain that task in the right column
        val allTasks = DataManager.allTasks.value
        assertNotNull(allTasks, "updateTask must seed _allTasks even when it was null")

        val upcoming = allTasks.columns.firstOrNull { it.name == "upcoming_tasks" }
        assertNotNull(upcoming, "the seeded kanban must include an upcoming_tasks column")
        assertTrue(
            upcoming.tasks.any { it.id == 1 },
            "the new task must land in upcoming_tasks; got columns=${allTasks.columns.map { it.name to it.tasks.map { t -> t.id } }}"
        )
        assertEquals(upcoming.tasks.size, upcoming.count, "column count must match tasks.size")
    }

    /// Reasonable-defaults sanity check for the bulk-create scenario:
    /// multiple tasks land across different kanban columns and end up
    /// distributed correctly. This exercises the upsert when _allTasks
    /// was seeded by a previous call.
    @Test
    fun updateTask_distributesAcrossColumns_whenSeedingThenAdding() {
        DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "overdue_tasks"))
        DataManager.updateTask(sampleTask(id = 2, residenceId = 100, column = "upcoming_tasks"))
        DataManager.updateTask(sampleTask(id = 3, residenceId = 100, column = "upcoming_tasks"))

        val allTasks = DataManager.allTasks.value
        assertNotNull(allTasks)

        val overdue = allTasks.columns.first { it.name == "overdue_tasks" }
        val upcoming = allTasks.columns.first { it.name == "upcoming_tasks" }

        assertEquals(setOf(1), overdue.tasks.map { it.id }.toSet())
        assertEquals(setOf(2, 3), upcoming.tasks.map { it.id }.toSet())
    }

    /// Replacement contract: calling updateTask with the same id twice
    /// must not duplicate; the second call replaces the first wherever it
    /// lives. Catches the "always-append" implementation mistake.
    @Test
    fun updateTask_replacesExistingTaskById_acrossColumns() {
        DataManager.updateTask(sampleTask(id = 5, residenceId = 100, column = "upcoming_tasks", title = "v1"))
        DataManager.updateTask(sampleTask(id = 5, residenceId = 100, column = "in_progress_tasks", title = "v2"))

        val allTasks = DataManager.allTasks.value
        assertNotNull(allTasks)

        val upcoming = allTasks.columns.first { it.name == "upcoming_tasks" }
        val inProgress = allTasks.columns.first { it.name == "in_progress_tasks" }

        assertTrue(upcoming.tasks.none { it.id == 5 }, "task 5 must move out of upcoming_tasks")
        assertEquals(1, inProgress.tasks.count { it.id == 5 }, "task 5 must appear once in in_progress_tasks")
        assertEquals("v2", inProgress.tasks.first { it.id == 5 }.title)
    }

    /// Characterization: getTasksForResidence filters _allTasks by
    /// residence id. This is the helper that becomes the primary path
    /// for residence-detail in Phase 3 (collapse the dual cache).
    @Test
    fun getTasksForResidence_filtersAllTasksByResidenceId() {
        // Seed _allTasks with tasks across two residences via the upsert path.
        DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "upcoming_tasks"))
        DataManager.updateTask(sampleTask(id = 2, residenceId = 100, column = "overdue_tasks"))
        DataManager.updateTask(sampleTask(id = 3, residenceId = 200, column = "upcoming_tasks"))

        val r100 = DataManager.getTasksForResidence(100)
        assertNotNull(r100)
        val r100Ids = r100.columns.flatMap { it.tasks }.map { it.id }.toSet()
        assertEquals(setOf(1, 2), r100Ids)

        val r200 = DataManager.getTasksForResidence(200)
        assertNotNull(r200)
        val r200Ids = r200.columns.flatMap { it.tasks }.map { it.id }.toSet()
        assertEquals(setOf(3), r200Ids)

        // Counts on each column must match the filtered task lists.
        for (column in r100.columns) {
            assertEquals(column.tasks.size, column.count, "column ${column.name} count mismatch")
        }
    }

    /// Characterization: residenceId with no tasks returns a non-null
    /// shell so the residence-detail screen can distinguish "loading"
    /// (null) from "loaded, no tasks" (non-null with empty columns).
    @Test
    fun getTasksForResidence_returnsEmptyShellForResidenceWithNoTasks() {
        DataManager.updateTask(sampleTask(id = 1, residenceId = 100, column = "upcoming_tasks"))

        val r999 = DataManager.getTasksForResidence(999)
        assertNotNull(r999, "residence with no tasks must return an empty shell, not null")
        assertEquals(0, r999.columns.sumOf { it.tasks.size })
    }

    /// Characterization: when _allTasks is null entirely (cache never
    /// populated), getTasksForResidence returns null — caller must call
    /// the API path. Phase 3's getTasksByResidence relies on this.
    @Test
    fun getTasksForResidence_returnsNullWhenAllTasksIsNull() {
        DataManager.clear()
        assertEquals(null, DataManager.getTasksForResidence(100))
    }

    private fun sampleTask(
        id: Int,
        residenceId: Int,
        column: String,
        title: String = "Task $id"
    ) = TaskResponse(
        id = id,
        residenceId = residenceId,
        createdById = 1,
        title = title,
        kanbanColumn = column,
        createdAt = "2026-04-25T00:00:00Z",
        updatedAt = "2026-04-25T00:00:00Z"
    )
}
