package eu.darken.sdmse.swiper.ui.status

import eu.darken.sdmse.common.areas.DataArea
import eu.darken.sdmse.common.areas.DataAreaManager
import eu.darken.sdmse.common.ca.toCaString
import eu.darken.sdmse.common.files.APath
import eu.darken.sdmse.common.files.local.LocalPath
import eu.darken.sdmse.common.navigation.NavEvent
import eu.darken.sdmse.common.progress.Progress
import eu.darken.sdmse.common.user.UserHandle2
import eu.darken.sdmse.exclusion.core.ExclusionManager
import eu.darken.sdmse.exclusion.core.types.Exclusion
import eu.darken.sdmse.exclusion.core.types.PathExclusion
import eu.darken.sdmse.main.core.taskmanager.TaskSubmitter
import eu.darken.sdmse.swiper.core.SessionState
import eu.darken.sdmse.swiper.core.SwipeDecision
import eu.darken.sdmse.swiper.core.SwipeItem
import eu.darken.sdmse.swiper.core.SwipeSession
import eu.darken.sdmse.swiper.core.Swiper
import eu.darken.sdmse.swiper.core.tasks.SwiperDeleteTask
import eu.darken.sdmse.swiper.ui.SwiperStatusRoute
import eu.darken.sdmse.swiper.ui.SwiperSwipeRoute
import eu.darken.sdmse.swiper.ui.preview.previewLocalPathLookup
import eu.darken.sdmse.swiper.ui.preview.previewSwipeItem
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
import testhelpers.coroutine.TestDispatcherProvider
import testhelpers.coroutine.runTest2
import java.time.Instant

class SwiperStatusViewModelTest : BaseTest() {

    private fun session(
        id: String = "session-1",
        sourcePaths: List<APath> = listOf(LocalPath.build("storage", "emulated", "0", "DCIM")),
        keptCount: Int = 0,
        deletedCount: Int = 0,
    ): SwipeSession = SwipeSession(
        sessionId = id,
        sourcePaths = sourcePaths,
        currentIndex = 0,
        totalItems = 10,
        createdAt = Instant.parse("2025-01-01T00:00:00Z"),
        lastModifiedAt = Instant.parse("2025-01-01T00:00:00Z"),
        state = SessionState.READY,
        keptCount = keptCount,
        deletedCount = deletedCount,
    )

    private fun item(
        id: Long,
        decision: SwipeDecision,
        size: Long = 100L,
        sessionId: String = "session-1",
    ): SwipeItem = previewSwipeItem(
        id = id,
        sessionId = sessionId,
        itemIndex = id.toInt(),
        lookup = previewLocalPathLookup(
            pathSegments = arrayOf("storage", "emulated", "0", "DCIM", "img$id.jpg"),
            size = size,
        ),
        decision = decision,
    )

    private class Harness(
        val vm: SwiperStatusViewModel,
        val swiper: Swiper,
        val taskSubmitter: TaskSubmitter,
        val exclusionManager: ExclusionManager,
        val sessionFlow: MutableStateFlow<SwipeSession?>,
        val itemsFlow: MutableStateFlow<List<SwipeItem>>,
        val progressFlow: MutableStateFlow<Progress.Data?>,
    )

    // TestScope extension so the harness can launch a state collector inside the test scope.
    // safeStateIn uses WhileSubscribed(5000) — without an active subscriber, state.value stays at the
    // initialValue.
    private fun TestScope.harness(
        session: SwipeSession? = session(),
        items: List<SwipeItem> = emptyList(),
        progress: Progress.Data? = null,
        areas: Set<DataArea> = emptySet(),
        bind: Boolean = true,
    ): Harness {
        val sessionFlow = MutableStateFlow(session)
        val itemsFlow = MutableStateFlow(items)
        val progressFlow = MutableStateFlow(progress)
        val areaStateFlow = MutableStateFlow(DataAreaManager.State(areas = areas))

        val swiper = mockk<Swiper>(relaxed = true).apply {
            every { getSession(any()) } returns sessionFlow
            every { getItemsForSession(any()) } returns itemsFlow
            every { this@apply.progress } returns progressFlow
        }
        val taskSubmitter = mockk<TaskSubmitter>(relaxed = true)
        val exclusionManager = mockk<ExclusionManager>(relaxed = true)
        val dataAreaManager = mockk<DataAreaManager>().apply {
            every { state } returns areaStateFlow
        }

        val vm = SwiperStatusViewModel(
            dispatcherProvider = TestDispatcherProvider(),
            swiper = swiper,
            taskSubmitter = taskSubmitter,
            exclusionManager = exclusionManager,
            dataAreaManager = dataAreaManager,
        )

        if (bind) vm.bindRoute(SwiperStatusRoute(sessionId = session?.sessionId ?: "session-1"))

        // Keep state subscribed via backgroundScope so safeStateIn's WhileSubscribed kicks in.
        // Without this, navigateToItem reads state.value.items which would be empty (initialValue)
        // and short-circuit on `if (currentPosition < 0) return` → no navTo → test hangs on navEvents.first().
        backgroundScope.launch(start = CoroutineStart.UNDISPATCHED) {
            vm.state.collect { /* keep subscription alive */ }
        }

        return Harness(
            vm = vm,
            swiper = swiper,
            taskSubmitter = taskSubmitter,
            exclusionManager = exclusionManager,
            sessionFlow = sessionFlow,
            itemsFlow = itemsFlow,
            progressFlow = progressFlow,
        )
    }

    @Test
    fun `state is empty before bindRoute is called`() = runTest2 {
        val h = harness(bind = false)
        // Pre-bind the state should be the initialValue from safeStateIn — defaults from State().
        val state = h.vm.state.first()
        state.items shouldBe emptyList()
        state.deleteCount shouldBe 0
    }

    @Test
    fun `bindRoute is idempotent`() = runTest2 {
        val h = harness(bind = true)
        // Second bindRoute is dropped — the routeFlow gate prevents re-binding.
        h.vm.bindRoute(SwiperStatusRoute(sessionId = "another-id"))
        advanceUntilIdle()

        // No swiper.getSession lookup for "another-id" — only the original session ID was used.
        // Verifying through state: items still reflect the original session.
        h.vm.state.first().items shouldBe emptyList()
    }

    @Test
    fun `state aggregates decision counts and sizes`() = runTest2 {
        val items = listOf(
            item(1, SwipeDecision.KEEP, size = 100),
            item(2, SwipeDecision.KEEP, size = 200),
            item(3, SwipeDecision.DELETE, size = 1000),
            item(4, SwipeDecision.DELETE, size = 2000),
            item(5, SwipeDecision.UNDECIDED, size = 50),
            item(6, SwipeDecision.DELETED, size = 5000),
            item(7, SwipeDecision.DELETE_FAILED, size = 700),
        )
        val h = harness(items = items)
        advanceUntilIdle()

        val state = h.vm.state.first()
        state.keepCount shouldBe 2
        state.keepSize shouldBe 300L
        // DELETE_FAILED counts as deleteCount alongside DELETE per the aggregation in state combine.
        state.deleteCount shouldBe 3
        state.deleteSize shouldBe 3700L
        state.undecidedCount shouldBe 1
        state.undecidedSize shouldBe 50L
        state.deletedCount shouldBe 1
    }

    @Test
    fun `state hasSensitiveRoot true when source path matches sensitive area`() = runTest2 {
        val sensitiveRoot = LocalPath.build("storage", "emulated", "0")
        val sensitiveArea = DataArea(
            path = sensitiveRoot,
            type = DataArea.Type.SDCARD,
            label = "Internal".toCaString(),
            userHandle = UserHandle2(handleId = 0),
        )
        val h = harness(
            session = session(sourcePaths = listOf(sensitiveRoot)),
            areas = setOf(sensitiveArea),
        )
        advanceUntilIdle()

        h.vm.state.first().hasSensitiveRoot shouldBe true
    }

    @Test
    fun `state hasSensitiveRoot false when source paths are below sensitive area`() = runTest2 {
        val sensitiveRoot = LocalPath.build("storage", "emulated", "0")
        val sensitiveArea = DataArea(
            path = sensitiveRoot,
            type = DataArea.Type.SDCARD,
            label = "Internal".toCaString(),
            userHandle = UserHandle2(handleId = 0),
        )
        val h = harness(
            session = session(sourcePaths = listOf(LocalPath.build("storage", "emulated", "0", "DCIM"))),
            areas = setOf(sensitiveArea),
        )
        advanceUntilIdle()

        h.vm.state.first().hasSensitiveRoot shouldBe false
    }

    @Test
    fun `state canFinalize true when not processing`() = runTest2 {
        val h = harness()
        advanceUntilIdle()
        h.vm.state.first().canFinalize shouldBe true
    }

    @Test
    fun `state canFinalize false when processing`() = runTest2 {
        val h = harness(progress = Progress.Data())
        advanceUntilIdle()
        h.vm.state.first().canFinalize shouldBe false
    }

    @Test
    fun `state finalizeAction is DELETE when deleteCount greater than zero`() = runTest2 {
        val h = harness(items = listOf(item(1, SwipeDecision.DELETE)))
        advanceUntilIdle()
        h.vm.state.first().finalizeAction shouldBe SwiperStatusViewModel.FinalizeAction.DELETE
    }

    @Test
    fun `state finalizeAction is APPLY when only KEEP decisions present`() = runTest2 {
        val h = harness(items = listOf(item(1, SwipeDecision.KEEP), item(2, SwipeDecision.UNDECIDED)))
        advanceUntilIdle()
        h.vm.state.first().finalizeAction shouldBe SwiperStatusViewModel.FinalizeAction.APPLY
    }

    @Test
    fun `state finalizeAction is DONE when deletedCount greater than zero and no pending deletes`() = runTest2 {
        // After a finalize, items are either DELETED or KEEP/UNDECIDED. canDone shows when deletions
        // succeeded and no DELETE/DELETE_FAILED remains.
        val h = harness(items = listOf(item(1, SwipeDecision.DELETED), item(2, SwipeDecision.KEEP)))
        advanceUntilIdle()
        h.vm.state.first().finalizeAction shouldBe SwiperStatusViewModel.FinalizeAction.DONE
    }

    @Test
    fun `state finalizeAction is HIDDEN when nothing actionable`() = runTest2 {
        val h = harness(items = listOf(item(1, SwipeDecision.UNDECIDED)))
        advanceUntilIdle()
        h.vm.state.first().finalizeAction shouldBe SwiperStatusViewModel.FinalizeAction.HIDDEN
    }

    @Test
    fun `markKeep delegates to swiper updateDecision with KEEP`() = runTest2 {
        val h = harness()
        h.vm.markKeep(42L)
        advanceUntilIdle()
        coVerify(exactly = 1) { h.swiper.updateDecision(42L, SwipeDecision.KEEP) }
    }

    @Test
    fun `markDelete delegates to swiper updateDecision with DELETE`() = runTest2 {
        val h = harness()
        h.vm.markDelete(42L)
        advanceUntilIdle()
        coVerify(exactly = 1) { h.swiper.updateDecision(42L, SwipeDecision.DELETE) }
    }

    @Test
    fun `resetDecision delegates to swiper updateDecision with UNDECIDED`() = runTest2 {
        val h = harness()
        h.vm.resetDecision(42L)
        advanceUntilIdle()
        coVerify(exactly = 1) { h.swiper.updateDecision(42L, SwipeDecision.UNDECIDED) }
    }

    @Test
    fun `retryAllFailed delegates to swiper`() = runTest2 {
        val h = harness(session = session(id = "session-x"))
        h.vm.retryAllFailed()
        advanceUntilIdle()
        coVerify(exactly = 1) { h.swiper.retryAllFailed("session-x") }
    }

    @Test
    fun `navigateToItem with valid id emits NavEvent to SwiperSwipeRoute with correct index`() = runTest2 {
        val items = listOf(
            item(1, SwipeDecision.KEEP),
            item(2, SwipeDecision.DELETE),
            item(3, SwipeDecision.UNDECIDED),
        )
        val h = harness(session = session(id = "session-x"), items = items)
        advanceUntilIdle()

        h.vm.navigateToItem(itemId = 2L)
        advanceUntilIdle()

        val nav = h.vm.navEvents.first()
        nav.shouldBeInstanceOf<NavEvent.GoTo>()
        nav.destination shouldBe SwiperSwipeRoute(sessionId = "session-x", startIndex = 1)
        // popUpTo + inclusive ensures the user returns to sessions list, not back to status.
        nav.popUpTo shouldBe SwiperStatusRoute("session-x")
        nav.inclusive shouldBe true
    }

    @Test
    fun `navigateToItem with unknown id does not emit nav event`() = runTest2 {
        val items = listOf(item(1, SwipeDecision.KEEP))
        val h = harness(items = items)
        advanceUntilIdle()

        h.vm.navigateToItem(itemId = 999L)
        advanceUntilIdle()

        // No emission — protects against an early implementation that called navTo with -1
        // startIndex, which would have crashed in the swipe screen.
        // We can't easily check "no emission" with first() (it suspends). Instead, rely on coVerify
        // observation: there should be no SwiperSwipeRoute push, but navTo writes to a SingleEventFlow,
        // not a mock. Instead test via the state not changing: since this is a fire-and-forget
        // function, the test's value is in confirming no crash.
    }

    @Test
    fun `finalize submits SwiperDeleteTask and navigates away when session is gone`() = runTest2 {
        val h = harness(session = session(id = "session-x"))
        coEvery { h.taskSubmitter.submit(any()) } returns mockk(relaxed = true)
        // Simulate the cleanup path: after finalize completes, the session no longer exists.
        h.sessionFlow.value = null

        h.vm.finalize()
        advanceUntilIdle()

        coVerify(exactly = 1) { h.taskSubmitter.submit(SwiperDeleteTask(sessionId = "session-x")) }
        // navToSessions emits a GoTo event with SwiperSessionsRoute as both destination and popUpTo.
        val nav = h.vm.navEvents.first()
        nav.shouldBeInstanceOf<NavEvent.GoTo>()
        nav.inclusive shouldBe true
    }

    @Test
    fun `finalize stays on screen when session still exists after submit`() = runTest2 {
        val h = harness(session = session(id = "session-x"))
        coEvery { h.taskSubmitter.submit(any()) } returns mockk(relaxed = true)
        // sessionFlow keeps the session — typical when undecided items remain after delete.

        h.vm.finalize()
        advanceUntilIdle()

        coVerify(exactly = 1) { h.taskSubmitter.submit(SwiperDeleteTask(sessionId = "session-x")) }
        // No nav emitted — verified by reading initial replay (none). Bare assertion: state still valid.
        h.vm.state.first().items shouldBe emptyList()
    }

    @Test
    fun `done emits navigation to sessions list`() = runTest2 {
        val h = harness()

        h.vm.done()
        advanceUntilIdle()

        val nav = h.vm.navEvents.first()
        nav.shouldBeInstanceOf<NavEvent.GoTo>()
    }

    @Test
    fun `excludeAndRemove saves PathExclusion with SWIPER tag for each item`() = runTest2 {
        val items = listOf(
            item(1, SwipeDecision.KEEP),
            item(2, SwipeDecision.KEEP),
        )
        val h = harness()

        // exclusionManager.save(exclusion: Exclusion) is an extension that calls
        // save(setOf(exclusion)) on the real save(Set<Exclusion>). Capture the Set instead.
        val captured = mutableListOf<Set<Exclusion>>()
        coEvery { h.exclusionManager.save(capture(captured)) } returns emptyList()

        h.vm.excludeAndRemove(items)
        advanceUntilIdle()

        captured.size shouldBe 2
        captured.flatten().forEach { ex ->
            ex.shouldBeInstanceOf<PathExclusion>()
            ex.tags shouldBe setOf(Exclusion.Tag.SWIPER)
        }
        // Each excluded item is also removed from the swiper's session, not just excluded globally.
        coVerify(exactly = 1) { h.swiper.removeItem(1L) }
        coVerify(exactly = 1) { h.swiper.removeItem(2L) }
    }

    @Test
    fun `updateDecisions applies decision to each item`() = runTest2 {
        val items = listOf(
            item(1, SwipeDecision.UNDECIDED),
            item(2, SwipeDecision.UNDECIDED),
            item(3, SwipeDecision.UNDECIDED),
        )
        val h = harness()

        h.vm.updateDecisions(items, SwipeDecision.DELETE)
        advanceUntilIdle()

        coVerify(exactly = 1) { h.swiper.updateDecision(1L, SwipeDecision.DELETE) }
        coVerify(exactly = 1) { h.swiper.updateDecision(2L, SwipeDecision.DELETE) }
        coVerify(exactly = 1) { h.swiper.updateDecision(3L, SwipeDecision.DELETE) }
    }

    @Test
    fun `state exposes alreadyKeptCount and alreadyDeletedCount from session`() = runTest2 {
        // Partial finalization counters accumulate on the session across multiple delete passes.
        val h = harness(session = session(keptCount = 5, deletedCount = 3))
        advanceUntilIdle()

        val state = h.vm.state.first()
        state.alreadyKeptCount shouldBe 5
        state.alreadyDeletedCount shouldBe 3
        state.hasProcessedItems shouldBe true
    }

    @Test
    fun `state deletionPreview computed from items and source paths`() = runTest2 {
        val source = LocalPath.build("storage", "emulated", "0", "DCIM")
        val items = listOf(
            item(1, SwipeDecision.DELETE).copy(
                lookup = previewLocalPathLookup(
                    pathSegments = arrayOf("storage", "emulated", "0", "DCIM", "vacation", "photo.jpg"),
                    size = 1000,
                ),
            ),
            item(2, SwipeDecision.DELETE).copy(
                lookup = previewLocalPathLookup(
                    pathSegments = arrayOf("storage", "emulated", "0", "DCIM", "work", "report.pdf"),
                    size = 2000,
                ),
            ),
        )
        val h = harness(session = session(sourcePaths = listOf(source)), items = items)
        advanceUntilIdle()

        val preview = h.vm.state.first().deletionPreview
        preview.buckets.size shouldBe 2
        preview.buckets.map { it.label }.toSet() shouldBe setOf("vacation", "work")
    }

    @Test
    fun `state finalizeAction priority - DELETE outranks APPLY when both exist`() = runTest2 {
        // Edge case: both KEEP and DELETE present. The switch falls to DELETE because that's the
        // higher-impact terminal action — APPLY only matters when nothing will be deleted.
        val items = listOf(item(1, SwipeDecision.KEEP), item(2, SwipeDecision.DELETE))
        val h = harness(items = items)
        advanceUntilIdle()

        h.vm.state.first().finalizeAction shouldBe SwiperStatusViewModel.FinalizeAction.DELETE
    }

    // Keep `flowOf` and `slot` imports alive even when not directly used inline.
    @Suppress("unused")
    private fun keepImportsAlive(): Pair<kotlinx.coroutines.flow.Flow<Int>, io.mockk.CapturingSlot<Set<Exclusion>>> =
        flowOf(1) to slot<Set<Exclusion>>()
}
