package eu.darken.bluemusic.common.debug.recorder.core

import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.File
import java.time.Instant
import testhelpers.BaseTest

class DebugSessionManagerSessionLogicTest : BaseTest() {

    @TempDir
    lateinit var tempDir: File

    private lateinit var externalLogsDir: File
    private lateinit var cacheLogsDir: File

    @BeforeEach
    fun setup() {
        externalLogsDir = File(tempDir, "external/debug/logs").also { it.mkdirs() }
        cacheLogsDir = File(tempDir, "cache/debug/logs").also { it.mkdirs() }
    }

    private fun logDirs() = listOf(externalLogsDir, cacheLogsDir)

    @Nested
    inner class ParseCreatedAt {
        @Test
        fun `returns creation time parsed from filename`() {
            val file = File(externalLogsDir, "bluemusic_1.2.3_20240307T112000Z_abcd1234").also { it.mkdirs() }
            val result = DebugSessionManager.parseCreatedAt(file)
            result shouldBe Instant.parse("2024-03-07T11:20:00Z")
        }

        @Test
        fun `non-existent file returns epoch fallback`() {
            val file = File(externalLogsDir, "nonexistent")
            val result = DebugSessionManager.parseCreatedAt(file)
            result shouldBe Instant.EPOCH
        }
    }

    @Nested
    inner class DeriveSessionId {
        @Test
        fun `external dir gets ext prefix`() {
            val file = File("/storage/emulated/0/Android/data/pkg/files/debug/logs/session1")
            DebugSessionManager.deriveSessionId(file) shouldBe "ext:session1"
        }

        @Test
        fun `cache dir gets cache prefix`() {
            val file = File("/data/data/pkg/cache/debug/logs/session1")
            DebugSessionManager.deriveSessionId(file) shouldBe "cache:session1"
        }

        @Test
        fun `zip suffix is stripped`() {
            val file = File("/data/data/pkg/cache/debug/logs/session1.zip")
            DebugSessionManager.deriveSessionId(file) shouldBe "cache:session1"
        }

        @Test
        fun `ambiguous path without cache marker defaults to ext`() {
            val file = File("/tmp/some/random/path/session1")
            DebugSessionManager.deriveSessionId(file) shouldBe "ext:session1"
        }

        @Test
        fun `path with cache substring elsewhere gets cache prefix`() {
            val file = File("/storage/emulated/0/backup/cache/debug/logs/session1")
            DebugSessionManager.deriveSessionId(file) shouldBe "cache:session1"
        }

        @Test
        fun `dotcache path does not trigger cache prefix`() {
            val file = File("/storage/emulated/0/.cache/debug/logs/session1")
            DebugSessionManager.deriveSessionId(file) shouldBe "ext:session1"
        }
    }

    @Nested
    inner class ScanSessions {
        @Test
        fun `empty directories returns empty list`() {
            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())
            result.shouldBeEmpty()
        }

        @Test
        fun `dir with valid core log returns Ready`() {
            val sessionDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").writeText("some log content")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first()
            session.shouldBeInstanceOf<DebugSession.Ready>()
            session.id shouldBe "ext:bluemusic_1.0_20231114T221320Z_abcd1234"
            session.logDir shouldBe sessionDir
            session.zipFile shouldBe null
            session.compressedSize shouldBe 0L
        }

        @Test
        fun `dir with empty core log returns Failed EMPTY_LOG`() {
            val sessionDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").createNewFile()

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first()
            session.shouldBeInstanceOf<DebugSession.Failed>().reason shouldBe DebugSession.Failed.Reason.EMPTY_LOG
        }

        @Test
        fun `dir with no core log returns Failed MISSING_LOG`() {
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").mkdirs()

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first()
            session.shouldBeInstanceOf<DebugSession.Failed>().reason shouldBe DebugSession.Failed.Reason.MISSING_LOG
        }

        @Test
        fun `standalone non-empty zip returns Ready with null logDir`() {
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").writeText("zipdata")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first()
            val ready = session.shouldBeInstanceOf<DebugSession.Ready>()
            ready.logDir shouldBe null
            ready.zipFile shouldBe File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip")
            ready.compressedSize shouldBe File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").length()
        }

        @Test
        fun `standalone empty zip returns Failed CORRUPT_ZIP`() {
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").createNewFile()

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first()
            session.shouldBeInstanceOf<DebugSession.Failed>().reason shouldBe DebugSession.Failed.Reason.CORRUPT_ZIP
        }

        @Test
        fun `dir plus sibling zip reports combined diskSize`() {
            val sessionDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").writeText("log content here")
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").writeText("zipdata12345")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first() as DebugSession.Ready
            val expectedDirSize = sessionDir.walkTopDown().filter { it.isFile }.sumOf { it.length() }
            val zipFile = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip")
            val expectedZipSize = zipFile.length()
            session.diskSize shouldBe expectedDirSize + expectedZipSize
            session.zipFile shouldBe zipFile
            session.compressedSize shouldBe expectedZipSize
        }

        @Test
        fun `dir missing core log but valid sibling zip returns Ready with null logDir`() {
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").mkdirs()
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").writeText("zipdata")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first()
            val ready = session.shouldBeInstanceOf<DebugSession.Ready>()
            ready.logDir shouldBe null
            ready.zipFile shouldBe File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip")
        }

        @Test
        fun `active recording dir returns Recording`() {
            val sessionDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").writeText("recording in progress")

            val result = DebugSessionManager.scanSessions(
                logDirectories = logDirs(),
                activeDir = sessionDir,
                recordingStartedAt = 1700000000000L,
            )

            result shouldHaveSize 1
            val session = result.first()
            session.shouldBeInstanceOf<DebugSession.Recording>().startedAt shouldBe 1700000000000L
        }

        @Test
        fun `ignores non-directory non-zip files`() {
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234/core.log").writeText("log")
            File(externalLogsDir, "random_notes.txt").writeText("not a session")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            result.first().id shouldBe "ext:bluemusic_1.0_20231114T221320Z_abcd1234"
        }

        @Test
        fun `handles missing log directory gracefully`() {
            val missing = File(tempDir, "nonexistent/path")
            val result = DebugSessionManager.scanSessions(logDirectories = listOf(missing))

            result.shouldBeEmpty()
        }

        @Test
        fun `cache directory gets cache prefix`() {
            val sessionDir = File(cacheLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").writeText("cached log")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            result.first().id shouldBe "cache:bluemusic_1.0_20231114T221320Z_abcd1234"
        }

        @Test
        fun `empty core log with valid sibling zip returns Ready`() {
            val sessionDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").createNewFile()
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").writeText("valid zip data")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 1
            val session = result.first()
            val ready = session.shouldBeInstanceOf<DebugSession.Ready>()
            ready.logDir shouldBe null
            ready.zipFile shouldBe File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip")
        }

        @Test
        fun `multiple log directories are combined`() {
            val extDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_aaaa1111").also { it.mkdirs() }
            File(extDir, "core.log").writeText("ext log")

            val cacheDir = File(cacheLogsDir, "bluemusic_1.0_20200913T122640Z_bbbb2222").also { it.mkdirs() }
            File(cacheDir, "core.log").writeText("cache log")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 2
            result.any { it.id == "ext:bluemusic_1.0_20231114T221320Z_aaaa1111" } shouldBe true
            result.any { it.id == "cache:bluemusic_1.0_20200913T122640Z_bbbb2222" } shouldBe true
        }

        @Test
        fun `multiple sessions sorted by createdAt descending then id ascending`() {
            val dirA = File(externalLogsDir, "bluemusic_1.0_20200913T122640Z_aaaa").also { it.mkdirs() }
            File(dirA, "core.log").writeText("log a")

            val dirB = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_bbbb").also { it.mkdirs() }
            File(dirB, "core.log").writeText("log b")

            val result = DebugSessionManager.scanSessions(logDirectories = logDirs())

            result shouldHaveSize 2
            val comparator = compareByDescending<DebugSession> { it.createdAt }.thenBy { it.id }
            result shouldBe result.sortedWith(comparator)
        }
    }

    @Nested
    inner class IdRoundTrip {
        @Test
        fun `deriveSessionId on ext dir matches scanSessions output`() {
            val sessionDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").writeText("log content")

            val derived = DebugSessionManager.deriveSessionId(sessionDir)
            val scanned = DebugSessionManager.scanSessions(logDirectories = logDirs())

            scanned shouldHaveSize 1
            scanned.first().id shouldBe derived
        }

        @Test
        fun `deriveSessionId on cache dir matches scanSessions output`() {
            val sessionDir = File(cacheLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").writeText("log content")

            val derived = DebugSessionManager.deriveSessionId(sessionDir)
            val scanned = DebugSessionManager.scanSessions(logDirectories = logDirs())

            scanned shouldHaveSize 1
            scanned.first().id shouldBe derived
        }

        @Test
        fun `deriveSessionId on zip file matches scanSessions output`() {
            File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").writeText("zipdata")

            val zipFile = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip")
            val derived = DebugSessionManager.deriveSessionId(zipFile)
            val scanned = DebugSessionManager.scanSessions(logDirectories = logDirs())

            scanned shouldHaveSize 1
            scanned.first().id shouldBe derived
        }

        @Test
        fun `deriveSessionId is consistent for dir and sibling zip`() {
            val sessionDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(sessionDir, "core.log").writeText("log content")
            val zipFile = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234.zip").also {
                it.writeText("zipdata")
            }

            val dirId = DebugSessionManager.deriveSessionId(sessionDir)
            val zipId = DebugSessionManager.deriveSessionId(zipFile)
            dirId shouldBe zipId

            val scanned = DebugSessionManager.scanSessions(logDirectories = logDirs())
            scanned shouldHaveSize 1
            scanned.first().id shouldBe dirId
        }

        @Test
        fun `same basename in ext and cache produces distinct IDs`() {
            val extDir = File(externalLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(extDir, "core.log").writeText("ext log")
            val cacheDir = File(cacheLogsDir, "bluemusic_1.0_20231114T221320Z_abcd1234").also { it.mkdirs() }
            File(cacheDir, "core.log").writeText("cache log")

            val extId = DebugSessionManager.deriveSessionId(extDir)
            val cacheId = DebugSessionManager.deriveSessionId(cacheDir)
            extId shouldBe "ext:bluemusic_1.0_20231114T221320Z_abcd1234"
            cacheId shouldBe "cache:bluemusic_1.0_20231114T221320Z_abcd1234"

            val scanned = DebugSessionManager.scanSessions(logDirectories = logDirs())
            scanned shouldHaveSize 2
            scanned.map { it.id }.toSet() shouldBe setOf(extId, cacheId)
        }
    }
}
