package de.krisenvorrat.app.data.db

import android.database.sqlite.SQLiteDatabase
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import de.krisenvorrat.app.data.db.entity.CategoryEntity
import de.krisenvorrat.app.data.db.entity.LocationEntity
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/**
 * Instrumentierte Tests für Room-Migrationen.
 *
 * Hinweis: Ab V2 exportiert Room das Schema als JSON nach `app/schemas/`.
 * Künftige Migrationen (V2 → V3 usw.) können deshalb MigrationTestHelper mit
 * `createDatabase(name, version)` nutzen, das die gespeicherte Schema-Datei
 * als Ausgangsbasis verwendet.
 *
 * Für V1 → V2 existierte noch kein exportiertes Schema, daher wird die V1-DB
 * hier manuell per SQLite-API aufgebaut.
 */
@RunWith(AndroidJUnit4::class)
internal class KrisenvorratDatabaseMigrationTest {

    private val dbName = "krisenvorrat-migration-test.db"
    private val context get() = InstrumentationRegistry.getInstrumentation().targetContext

    @Before
    fun setUp() {
        context.deleteDatabase(dbName)
    }

    @After
    fun tearDown() {
        context.deleteDatabase(dbName)
    }

    // -------------------------------------------------------------------------
    // Hilfsmethoden
    // -------------------------------------------------------------------------

    /**
     * Legt eine V1-Datenbank auf dem Gerät an.
     * Schema: items mit kcal_per_100g + min_stock (vor dem Umbenennen/Löschen).
     */
    private fun createV1Database(includeTestData: Boolean = false) {
        val dbFile = context.getDatabasePath(dbName).also { it.parentFile?.mkdirs() }
        SQLiteDatabase.openOrCreateDatabase(dbFile, null).use { db ->
            db.execSQL(
                "CREATE TABLE `categories` " +
                    "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
            )
            db.execSQL(
                "CREATE TABLE `locations` " +
                    "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
            )
            db.execSQL(
                """
                CREATE TABLE `items` (
                    `id` TEXT NOT NULL,
                    `name` TEXT NOT NULL,
                    `category_id` INTEGER NOT NULL,
                    `quantity` REAL NOT NULL,
                    `unit` TEXT NOT NULL,
                    `unit_price` REAL NOT NULL,
                    `kcal_per_100g` INTEGER,
                    `expiry_date` TEXT,
                    `location_id` INTEGER NOT NULL,
                    `min_stock` REAL NOT NULL,
                    `notes` TEXT NOT NULL,
                    `last_updated` INTEGER NOT NULL,
                    PRIMARY KEY(`id`)
                )
                """.trimIndent()
            )
            db.execSQL(
                "CREATE TABLE `settings` " +
                    "(`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))"
            )
            if (includeTestData) {
                db.execSQL("INSERT INTO `categories` (id, name) VALUES (1, 'Lebensmittel')")
                db.execSQL("INSERT INTO `locations` (id, name) VALUES (1, 'Keller')")
                db.execSQL(
                    """
                    INSERT INTO `items`
                        (id, name, category_id, quantity, unit, unit_price,
                         kcal_per_100g, expiry_date, location_id, min_stock, notes, last_updated)
                    VALUES
                        ('item-uuid-1', 'Apfel', 1, 5.0, 'kg', 2.50,
                         52, NULL, 1, 1.0, 'Testnotiz', 1700000000000)
                    """.trimIndent()
                )
            }
            db.version = 1
        }
    }

    private fun openMigratedDb() = Room.databaseBuilder(
        context,
        KrisenvorratDatabase::class.java,
        dbName
    ).addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4).build()

    private fun createV2Database() {
        val dbFile = context.getDatabasePath(dbName).also { it.parentFile?.mkdirs() }
        SQLiteDatabase.openOrCreateDatabase(dbFile, null).use { db ->
            db.execSQL(
                "CREATE TABLE `categories` " +
                    "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
            )
            db.execSQL(
                "CREATE TABLE `locations` " +
                    "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
            )
            db.execSQL(
                """
                CREATE TABLE `items` (
                    `id` TEXT NOT NULL,
                    `name` TEXT NOT NULL,
                    `category_id` INTEGER NOT NULL,
                    `quantity` REAL NOT NULL,
                    `unit` TEXT NOT NULL,
                    `unit_price` REAL NOT NULL,
                    `kcal_per_kg` INTEGER,
                    `expiry_date` TEXT,
                    `location_id` INTEGER NOT NULL,
                    `notes` TEXT NOT NULL,
                    `last_updated` INTEGER NOT NULL,
                    PRIMARY KEY(`id`)
                )
                """.trimIndent()
            )
            db.execSQL(
                "CREATE TABLE `settings` " +
                    "(`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))"
            )
            db.version = 2
        }
    }

    private fun openMigratedDbV3() = Room.databaseBuilder(
        context,
        KrisenvorratDatabase::class.java,
        dbName
    ).addMigrations(Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4).build()

    private fun openMigratedDbV4() = Room.databaseBuilder(
        context,
        KrisenvorratDatabase::class.java,
        dbName
    ).addMigrations(Migrations.MIGRATION_3_4).build()

    private fun createV3Database() {
        val dbFile = context.getDatabasePath(dbName).also { it.parentFile?.mkdirs() }
        SQLiteDatabase.openOrCreateDatabase(dbFile, null).use { db ->
            db.execSQL(
                "CREATE TABLE `categories` " +
                    "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
            )
            db.execSQL(
                "CREATE TABLE `locations` " +
                    "(`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)"
            )
            db.execSQL(
                """
                CREATE TABLE `items` (
                    `id` TEXT NOT NULL,
                    `name` TEXT NOT NULL,
                    `category_id` INTEGER NOT NULL,
                    `quantity` REAL NOT NULL,
                    `unit` TEXT NOT NULL,
                    `unit_price` REAL NOT NULL,
                    `kcal_per_kg` INTEGER,
                    `expiry_date` TEXT,
                    `location_id` INTEGER NOT NULL,
                    `notes` TEXT NOT NULL,
                    `last_updated` INTEGER NOT NULL,
                    PRIMARY KEY(`id`)
                )
                """.trimIndent()
            )
            db.execSQL(
                "CREATE TABLE `settings` " +
                    "(`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))"
            )
            db.execSQL(
                """
                CREATE TABLE IF NOT EXISTS `pending_sync_ops` (
                    `id` TEXT NOT NULL,
                    `item_id` TEXT NOT NULL,
                    `operation` TEXT NOT NULL,
                    `payload` TEXT NOT NULL,
                    `created_at` INTEGER NOT NULL,
                    PRIMARY KEY(`id`)
                )
                """.trimIndent()
            )
            db.version = 3
        }
    }

    // -------------------------------------------------------------------------
    // Tests
    // -------------------------------------------------------------------------

    @Test
    fun migrate1To2_itemDataIsPreserved() {
        createV1Database(includeTestData = true)

        val db = openMigratedDb()
        try {
            val items = runBlocking { db.itemDao().getAll().first() }

            assertEquals("Ein Item muss nach der Migration erhalten sein", 1, items.size)
            val item = items[0]
            assertEquals("item-uuid-1", item.id)
            assertEquals("Apfel", item.name)
            assertEquals(52, item.kcalPerKg)
            assertEquals("Testnotiz", item.notes)
            assertEquals(5.0, item.quantity, 0.0)
        } finally {
            db.close()
        }
    }

    @Test
    fun migrate1To2_schemaIsCorrect() {
        createV1Database()

        val db = openMigratedDb()
        try {
            val columns = mutableListOf<String>()
            db.openHelper.writableDatabase.query("PRAGMA table_info(items)").use { cursor ->
                while (cursor.moveToNext()) {
                    columns.add(cursor.getString(cursor.getColumnIndexOrThrow("name")))
                }
            }

            assertFalse("min_stock darf nicht mehr existieren", columns.contains("min_stock"))
            assertFalse("kcal_per_100g darf nicht mehr existieren", columns.contains("kcal_per_100g"))
            assertTrue("kcal_per_kg muss existieren", columns.contains("kcal_per_kg"))
            assertTrue("id muss existieren", columns.contains("id"))
            assertTrue("name muss existieren", columns.contains("name"))
            assertTrue("last_updated muss existieren", columns.contains("last_updated"))
        } finally {
            db.close()
        }
    }

    @Test
    fun migrate1To2_indicesExist() {
        createV1Database()

        val db = openMigratedDb()
        try {
            val indices = mutableListOf<String>()
            db.openHelper.writableDatabase.query("PRAGMA index_list(items)").use { cursor ->
                while (cursor.moveToNext()) {
                    indices.add(cursor.getString(cursor.getColumnIndexOrThrow("name")))
                }
            }

            assertTrue(
                "index_items_category_id muss vorhanden sein",
                indices.any { it.contains("category_id") }
            )
            assertTrue(
                "index_items_location_id muss vorhanden sein",
                indices.any { it.contains("location_id") }
            )
        } finally {
            db.close()
        }
    }

    @Test
    fun freshInstall_worksWithoutMigration() {
        // Fresh-Install: Room.onCreate() läuft direkt, keine Migration nötig
        val db = Room.inMemoryDatabaseBuilder(context, KrisenvorratDatabase::class.java)
            .addMigrations(Migrations.MIGRATION_1_2, Migrations.MIGRATION_2_3, Migrations.MIGRATION_3_4)
            .build()
        try {
            // Tabellen anlegen und Basis-Operationen prüfen
            runBlocking {
                db.categoryDao().insert(CategoryEntity(name = "Testkat"))
                db.locationDao().insert(LocationEntity(name = "Testort"))
                val cats = db.categoryDao().getAll().first()
                assertEquals(1, cats.size)
            }
        } finally {
            db.close()
        }
    }

    @Test
    fun migrate2To3_pendingSyncOpsTableExists() {
        createV2Database()

        val db = openMigratedDbV3()
        try {
            val tables = mutableListOf<String>()
            db.openHelper.writableDatabase.query(
                "SELECT name FROM sqlite_master WHERE type='table'"
            ).use { cursor ->
                while (cursor.moveToNext()) {
                    tables.add(cursor.getString(0))
                }
            }

            assertTrue("pending_sync_ops Tabelle muss existieren", tables.contains("pending_sync_ops"))

            // Eine Row kann eingetragen und gelesen werden
            db.openHelper.writableDatabase.execSQL(
                "INSERT INTO pending_sync_ops (id, item_id, operation, payload, created_at) " +
                    "VALUES ('op1', 'item1', 'PATCH', '{\"key\":\"val\"}', 1000)"
            )
            var rowCount = 0
            db.openHelper.writableDatabase.query(
                "SELECT COUNT(*) FROM pending_sync_ops"
            ).use { cursor ->
                if (cursor.moveToNext()) rowCount = cursor.getInt(0)
            }
            assertEquals("Genau eine Row muss in pending_sync_ops stehen", 1, rowCount)
        } finally {
            db.close()
        }
    }

    @Test
    fun migrate3To4_messagesTableExists() {
        createV3Database()

        val db = openMigratedDbV4()
        try {
            val tables = mutableListOf<String>()
            db.openHelper.writableDatabase.query(
                "SELECT name FROM sqlite_master WHERE type='table'"
            ).use { cursor ->
                while (cursor.moveToNext()) {
                    tables.add(cursor.getString(0))
                }
            }

            assertTrue("messages Tabelle muss existieren", tables.contains("messages"))

            // Spalten prüfen
            val columns = mutableListOf<String>()
            db.openHelper.writableDatabase.query("PRAGMA table_info(messages)").use { cursor ->
                while (cursor.moveToNext()) {
                    columns.add(cursor.getString(cursor.getColumnIndexOrThrow("name")))
                }
            }
            assertTrue("id muss existieren", columns.contains("id"))
            assertTrue("sender_id muss existieren", columns.contains("sender_id"))
            assertTrue("sender_username muss existieren", columns.contains("sender_username"))
            assertTrue("receiver_id muss existieren", columns.contains("receiver_id"))
            assertTrue("body muss existieren", columns.contains("body"))
            assertTrue("sent_at muss existieren", columns.contains("sent_at"))
            assertTrue("is_pending muss existieren", columns.contains("is_pending"))

            // Eine Row kann eingetragen und gelesen werden
            db.openHelper.writableDatabase.execSQL(
                "INSERT INTO messages (id, sender_id, sender_username, receiver_id, body, sent_at, is_pending) " +
                    "VALUES ('msg1', 'user1', 'Alice', 'user2', 'Hallo', 1000, 0)"
            )
            var rowCount = 0
            db.openHelper.writableDatabase.query(
                "SELECT COUNT(*) FROM messages"
            ).use { cursor ->
                if (cursor.moveToNext()) rowCount = cursor.getInt(0)
            }
            assertEquals("Genau eine Row muss in messages stehen", 1, rowCount)
        } finally {
            db.close()
        }
    }

    @Test
    fun migrate1To4_fullMigrationPathPreservesData() {
        // Given: V1-DB mit Testdaten
        createV1Database(includeTestData = true)

        // When: Vollständige Migration v1 → v2 → v3 → v4
        val db = openMigratedDb()
        try {
            // Then: Items sind erhalten
            val items = runBlocking { db.itemDao().getAll().first() }
            assertEquals("Item muss nach Full-Path-Migration erhalten sein", 1, items.size)
            val item = items[0]
            assertEquals("item-uuid-1", item.id)
            assertEquals("Apfel", item.name)
            assertEquals(52, item.kcalPerKg)

            // Alle Tabellen existieren
            val tables = mutableListOf<String>()
            db.openHelper.writableDatabase.query(
                "SELECT name FROM sqlite_master WHERE type='table'"
            ).use { cursor ->
                while (cursor.moveToNext()) {
                    tables.add(cursor.getString(0))
                }
            }
            assertTrue("items Tabelle muss existieren", tables.contains("items"))
            assertTrue("pending_sync_ops Tabelle muss existieren", tables.contains("pending_sync_ops"))
            assertTrue("messages Tabelle muss existieren", tables.contains("messages"))
            assertTrue("categories Tabelle muss existieren", tables.contains("categories"))
            assertTrue("locations Tabelle muss existieren", tables.contains("locations"))
            assertTrue("settings Tabelle muss existieren", tables.contains("settings"))
        } finally {
            db.close()
        }
    }
}
