/*
 * Copyright (c) 2026 Meshtastic LLC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
@file:Suppress("MagicNumber")

package org.meshtastic.core.database.dao

import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.meshtastic.core.database.MeshtasticDatabase
import org.meshtastic.core.database.entity.DiscoveredNodeEntity
import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity
import org.meshtastic.core.database.entity.DiscoverySessionEntity
import org.meshtastic.core.database.getInMemoryDatabaseBuilder
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

abstract class CommonDiscoveryDaoTest {
    private lateinit var database: MeshtasticDatabase
    private lateinit var dao: DiscoveryDao

    suspend fun createDb() {
        database = getInMemoryDatabaseBuilder().build()
        dao = database.discoveryDao()
    }

    @AfterTest
    fun closeDb() {
        database.close()
    }

    // region Session CRUD

    @Test
    fun insertSession_returnsAutoGeneratedId() = runTest {
        val session = testSession(timestamp = 1_000_000L)
        val id = dao.insertSession(session)
        assertTrue(id > 0, "Auto-generated id should be > 0")
    }

    @Test
    fun getSession_returnsInsertedSession() = runTest {
        val id = dao.insertSession(testSession(timestamp = 2_000_000L, homePreset = "MEDIUM_SLOW"))
        val loaded = dao.getSession(id)
        assertNotNull(loaded)
        assertEquals(id, loaded.id)
        assertEquals("MEDIUM_SLOW", loaded.homePreset)
        assertEquals(2_000_000L, loaded.timestamp)
    }

    @Test fun getSession_returnsNullForMissing() = runTest { assertNull(dao.getSession(999L)) }

    @Test
    fun updateSession_modifiesExistingRow() = runTest {
        val id = dao.insertSession(testSession(timestamp = 3_000_000L))
        val original = dao.getSession(id)!!
        dao.updateSession(original.copy(completionStatus = "stopped", totalUniqueNodes = 5))
        val updated = dao.getSession(id)!!
        assertEquals("stopped", updated.completionStatus)
        assertEquals(5, updated.totalUniqueNodes)
    }

    @Test
    fun deleteSession_removesRow() = runTest {
        val id = dao.insertSession(testSession())
        dao.deleteSession(id)
        assertNull(dao.getSession(id))
    }

    // endregion

    // region Session sort order (getAllSessions returns newest-first)

    @Test
    fun getAllSessions_orderedByTimestampDescending() = runTest {
        dao.insertSession(testSession(timestamp = 100L))
        dao.insertSession(testSession(timestamp = 300L))
        dao.insertSession(testSession(timestamp = 200L))
        val sessions = dao.getAllSessions().first()
        assertEquals(3, sessions.size)
        assertEquals(300L, sessions[0].timestamp)
        assertEquals(200L, sessions[1].timestamp)
        assertEquals(100L, sessions[2].timestamp)
    }

    // endregion

    // region Preset result relation loading

    @Test
    fun getPresetResults_returnsResultsForSession() = runTest {
        val sessionId = dao.insertSession(testSession())
        dao.insertPresetResult(testPresetResult(sessionId, presetName = "LONG_FAST"))
        dao.insertPresetResult(testPresetResult(sessionId, presetName = "SHORT_FAST"))
        val results = dao.getPresetResults(sessionId)
        assertEquals(2, results.size)
        assertTrue(results.any { it.presetName == "LONG_FAST" })
        assertTrue(results.any { it.presetName == "SHORT_FAST" })
    }

    @Test
    fun getPresetResults_doesNotReturnOtherSessionResults() = runTest {
        val session1 = dao.insertSession(testSession(timestamp = 1L))
        val session2 = dao.insertSession(testSession(timestamp = 2L))
        dao.insertPresetResult(testPresetResult(session1, presetName = "A"))
        dao.insertPresetResult(testPresetResult(session2, presetName = "B"))
        val results = dao.getPresetResults(session1)
        assertEquals(1, results.size)
        assertEquals("A", results[0].presetName)
    }

    @Test
    fun getPresetResultsFlow_emitsOnInsert() = runTest {
        val sessionId = dao.insertSession(testSession())
        val initial = dao.getPresetResultsFlow(sessionId).first()
        assertTrue(initial.isEmpty())
        dao.insertPresetResult(testPresetResult(sessionId, presetName = "LONG_FAST"))
        val updated = dao.getPresetResultsFlow(sessionId).first()
        assertEquals(1, updated.size)
    }

    // endregion

    // region Discovered node relation loading

    @Test
    fun getDiscoveredNodes_returnsNodesForPresetResult() = runTest {
        val sessionId = dao.insertSession(testSession())
        val presetId = dao.insertPresetResult(testPresetResult(sessionId))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 100))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 200))
        val nodes = dao.getDiscoveredNodes(presetId)
        assertEquals(2, nodes.size)
    }

    @Test
    fun insertDiscoveredNodes_batchInsert() = runTest {
        val sessionId = dao.insertSession(testSession())
        val presetId = dao.insertPresetResult(testPresetResult(sessionId))
        val batch =
            listOf(testNode(presetId, nodeNum = 1), testNode(presetId, nodeNum = 2), testNode(presetId, nodeNum = 3))
        dao.insertDiscoveredNodes(batch)
        assertEquals(3, dao.getDiscoveredNodes(presetId).size)
    }

    @Test
    fun updateDiscoveredNode_modifiesExistingRow() = runTest {
        val sessionId = dao.insertSession(testSession())
        val presetId = dao.insertPresetResult(testPresetResult(sessionId))
        val nodeId = dao.insertDiscoveredNode(testNode(presetId, nodeNum = 42))
        val original = dao.getDiscoveredNodes(presetId).first { it.id == nodeId }
        dao.updateDiscoveredNode(original.copy(snr = 12.5f, rssi = -55))
        val updated = dao.getDiscoveredNodes(presetId).first { it.id == nodeId }
        assertEquals(12.5f, updated.snr)
        assertEquals(-55, updated.rssi)
    }

    // endregion

    // region Cascade deletion

    @Test
    fun deleteSession_cascadesPresetResults() = runTest {
        val sessionId = dao.insertSession(testSession())
        dao.insertPresetResult(testPresetResult(sessionId, presetName = "LONG_FAST"))
        dao.insertPresetResult(testPresetResult(sessionId, presetName = "SHORT_FAST"))
        dao.deleteSession(sessionId)
        assertTrue(dao.getPresetResults(sessionId).isEmpty(), "Preset results should be cascade-deleted")
    }

    @Test
    fun deleteSession_cascadesDiscoveredNodes() = runTest {
        val sessionId = dao.insertSession(testSession())
        val presetId = dao.insertPresetResult(testPresetResult(sessionId))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 1))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 2))
        dao.deleteSession(sessionId)
        assertTrue(dao.getDiscoveredNodes(presetId).isEmpty(), "Discovered nodes should be cascade-deleted")
    }

    @Test
    fun deleteSession_doesNotAffectOtherSessions() = runTest {
        val session1 = dao.insertSession(testSession(timestamp = 1L))
        val session2 = dao.insertSession(testSession(timestamp = 2L))
        val preset1 = dao.insertPresetResult(testPresetResult(session1))
        val preset2 = dao.insertPresetResult(testPresetResult(session2))
        dao.insertDiscoveredNode(testNode(preset1, nodeNum = 1))
        dao.insertDiscoveredNode(testNode(preset2, nodeNum = 2))
        dao.deleteSession(session1)
        assertNotNull(dao.getSession(session2))
        assertEquals(1, dao.getPresetResults(session2).size)
        assertEquals(1, dao.getDiscoveredNodes(preset2).size)
    }

    // endregion

    // region Aggregate queries

    @Test
    fun getUniqueNodeCount_countsAcrossPresets() = runTest {
        val sessionId = dao.insertSession(testSession())
        val preset1 = dao.insertPresetResult(testPresetResult(sessionId, presetName = "A"))
        val preset2 = dao.insertPresetResult(testPresetResult(sessionId, presetName = "B"))
        // Same node 100 appears in both presets
        dao.insertDiscoveredNode(testNode(preset1, nodeNum = 100))
        dao.insertDiscoveredNode(testNode(preset1, nodeNum = 200))
        dao.insertDiscoveredNode(testNode(preset2, nodeNum = 100))
        dao.insertDiscoveredNode(testNode(preset2, nodeNum = 300))
        assertEquals(3, dao.getUniqueNodeCount(sessionId), "Node 100 appears in both presets but should count once")
    }

    @Test
    fun getUniqueNodeNums_returnsDistinctNodeNums() = runTest {
        val sessionId = dao.insertSession(testSession())
        val presetId = dao.insertPresetResult(testPresetResult(sessionId))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 10))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 20))
        val nums = dao.getUniqueNodeNums(sessionId)
        assertEquals(setOf(10L, 20L), nums.toSet())
    }

    @Test
    fun getMaxDistance_returnsLargestDistance() = runTest {
        val sessionId = dao.insertSession(testSession())
        val presetId = dao.insertPresetResult(testPresetResult(sessionId))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 1, distanceFromUser = 500.0))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 2, distanceFromUser = 15_000.0))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 3, distanceFromUser = 3_000.0))
        assertEquals(15_000.0, dao.getMaxDistance(sessionId))
    }

    @Test
    fun getMaxDistance_returnsNullWhenNoNodes() = runTest {
        val sessionId = dao.insertSession(testSession())
        assertNull(dao.getMaxDistance(sessionId))
    }

    @Test
    fun getMaxDistance_returnsNullWhenAllDistancesNull() = runTest {
        val sessionId = dao.insertSession(testSession())
        val presetId = dao.insertPresetResult(testPresetResult(sessionId))
        dao.insertDiscoveredNode(testNode(presetId, nodeNum = 1, distanceFromUser = null))
        assertNull(dao.getMaxDistance(sessionId))
    }

    // endregion

    // region Flow queries

    @Test
    fun getSessionFlow_emitsUpdatesOnChange() = runTest {
        val id = dao.insertSession(testSession(timestamp = 5_000_000L))
        val initial = dao.getSessionFlow(id).first()
        assertNotNull(initial)
        assertEquals("in_progress", initial.completionStatus)
    }

    // endregion

    // region Helpers

    private fun testSession(timestamp: Long = 1_000_000L, homePreset: String = "LONG_FAST") = DiscoverySessionEntity(
        timestamp = timestamp,
        presetsScanned = "LONG_FAST,SHORT_FAST",
        homePreset = homePreset,
        completionStatus = "in_progress",
    )

    private fun testPresetResult(sessionId: Long, presetName: String = "LONG_FAST") = DiscoveryPresetResultEntity(
        sessionId = sessionId,
        presetName = presetName,
        dwellDurationSeconds = 30,
        uniqueNodes = 5,
    )

    private fun testNode(presetResultId: Long, nodeNum: Long, distanceFromUser: Double? = null) = DiscoveredNodeEntity(
        presetResultId = presetResultId,
        nodeNum = nodeNum,
        snr = 5.0f,
        rssi = -70,
        distanceFromUser = distanceFromUser,
    )

    // endregion
}
