package com.ponzischeme89.memby.data

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.mutablePreferencesOf
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test

/** Mirrors the private constant, so a schema bump that forgets these tests fails loudly. */
private const val CURRENT_SETTINGS_SCHEMA_FOR_TEST = 2

class SettingsMigrationTest {
    private val json = Json { ignoreUnknownKeys = true }

    @Test
    fun `migration repairs profiles individually and retains malformed source`() {
        val valid = EmbyProfile(
            id = "https://emby::alex",
            serverUrl = "https://emby",
            token = "token",
            userId = "alex",
            username = "Alex",
        )
        val raw = "[${json.encodeToString(valid)},{\"id\":17}]"
        val source = mutablePreferencesOf(stringPreferencesKey("profiles") to raw)

        val migrated = SettingsMigrationLogic.migrateToCurrent(source)
        val repaired = json.decodeFromString<List<EmbyProfile>>(
            migrated[stringPreferencesKey("profiles")]!!,
        )

        assertEquals(listOf(valid), repaired)
        assertEquals(raw, migrated[stringPreferencesKey("profiles_recovery_v0")])
        assertEquals(
            CURRENT_SETTINGS_SCHEMA_FOR_TEST,
            migrated[intPreferencesKey("settings_schema_version")],
        )
    }

    @Test
    fun `migration recovers a complete profile from legacy flat settings`() {
        val source = mutablePreferencesOf(
            stringPreferencesKey("server_url") to "https://emby/",
            stringPreferencesKey("token") to "token",
            stringPreferencesKey("user_id") to "alex",
            intPreferencesKey("for_you_minutes") to 45,
            booleanPreferencesKey("has_opened_for_you") to true,
            booleanPreferencesKey("show_ratings_strip") to false,
        )

        val migrated = SettingsMigrationLogic.migrateToCurrent(source)
        val profile = json.decodeFromString<List<EmbyProfile>>(
            migrated[stringPreferencesKey("profiles")]!!,
        ).single()

        assertEquals("https://emby::alex", profile.id)
        assertEquals("Memby user", profile.username)
        assertEquals(45, profile.forYouMinutes)
        assertTrue(profile.hasOpenedForYou)
        assertFalse(profile.showRatingsStrip)
    }

    @Test
    fun `migration accepts profiles written by a newer app version`() {
        val raw = """[{"id":"server::user","serverUrl":"server","token":"token","userId":"user","username":"User","futureSetting":"kept-compatible"}]"""
        val migrated = SettingsMigrationLogic.migrateToCurrent(
            mutablePreferencesOf(stringPreferencesKey("profiles") to raw),
        )
        val profiles = json.decodeFromString<List<EmbyProfile>>(
            migrated[stringPreferencesKey("profiles")]!!,
        )

        assertEquals("user", profiles.single().userId)
        assertEquals(null, migrated[stringPreferencesKey("profiles_recovery_v0")])
    }

    /**
     * The regression this schema step exists for. Before settings moved to the server the
     * three toggles were device-wide, so an existing install holds them in the flat keys
     * and in no profile at all. Nothing looks wrong until the viewer switches profile —
     * `applyProfile` copies profile values into the flat keys, so all three would come
     * back on, and the sync would then push that up as a deliberate choice.
     */
    @Test
    fun `device-wide toggles are carried into every stored profile`() {
        val alex = EmbyProfile(
            id = "https://emby::alex", serverUrl = "https://emby",
            token = "token", userId = "alex", username = "Alex",
        )
        val sam = EmbyProfile(
            id = "https://emby::sam", serverUrl = "https://emby",
            token = "token", userId = "sam", username = "Sam",
        )
        // Written by the previous build: schema 1, and the profiles carry no toggles.
        val stored = """[${json.encodeToString(alex)},${json.encodeToString(sam)}]"""
            .replace(Regex(""","showTitleLogo":[a-z]+"""), "")
        val source = mutablePreferencesOf(
            intPreferencesKey("settings_schema_version") to 1,
            stringPreferencesKey("profiles") to stored,
            booleanPreferencesKey("show_title_logo") to false,
            booleanPreferencesKey("auto_play_next_episode") to false,
            booleanPreferencesKey("show_ten_minute_reminder") to true,
        )

        val migrated = SettingsMigrationLogic.migrateToCurrent(source)
        val profiles = json.decodeFromString<List<EmbyProfile>>(
            migrated[stringPreferencesKey("profiles")]!!,
        )

        assertEquals(2, profiles.size)
        profiles.forEach { profile ->
            assertFalse("${profile.userId} lost the title logo choice", profile.showTitleLogo)
            assertFalse("${profile.userId} lost the auto-play choice", profile.autoPlayNextEpisode)
            assertTrue(profile.showTenMinuteReminder)
        }
        assertEquals(2, migrated[intPreferencesKey("settings_schema_version")])
    }

    /**
     * The migration must run exactly once, and the schema version is the only thing making
     * that true — it cannot detect its own work, because the encoder omits default values
     * and so a profile that chose `true` is byte-identical to one that never chose.
     */
    @Test
    fun `the toggle migration does not run a second time`() {
        val profile = EmbyProfile(
            id = "https://emby::alex", serverUrl = "https://emby",
            token = "token", userId = "alex", username = "Alex",
        )
        val source = mutablePreferencesOf(
            intPreferencesKey("settings_schema_version") to 1,
            stringPreferencesKey("profiles") to "[${json.encodeToString(profile)}]",
            booleanPreferencesKey("show_title_logo") to false,
        )

        val once = SettingsMigrationLogic.migrateToCurrent(source)
        // The viewer turns it back on afterwards; the device-wide key is stale from here
        // on, and must never be applied again.
        val afterEdit = once.toMutablePreferences().apply {
            this[stringPreferencesKey("profiles")] =
                "[${json.encodeToString(profile.copy(showTitleLogo = true))}]"
        }

        val twice = SettingsMigrationLogic.migrateToCurrent(afterEdit)

        assertTrue(
            json.decodeFromString<List<EmbyProfile>>(
                twice[stringPreferencesKey("profiles")]!!,
            ).single().showTitleLogo,
        )
    }

    /** Nothing chosen means nothing to preserve, and no whole-file rewrite to pay for. */
    @Test
    fun `an install with default toggles is left alone`() {
        val profile = EmbyProfile(
            id = "https://emby::alex", serverUrl = "https://emby",
            token = "token", userId = "alex", username = "Alex",
        )
        val stored = "[${json.encodeToString(profile)}]"
        val source = mutablePreferencesOf(
            intPreferencesKey("settings_schema_version") to 1,
            stringPreferencesKey("profiles") to stored,
        )

        val migrated = SettingsMigrationLogic.migrateToCurrent(source)

        assertEquals(stored, migrated[stringPreferencesKey("profiles")])
        assertEquals(2, migrated[intPreferencesKey("settings_schema_version")])
    }

    /**
     * A first-ever launch of the new build on an install that predates the profile list
     * has to come out at the current schema in one pass, not stop halfway.
     */
    @Test
    fun `an unversioned install migrates all the way to the current schema`() {
        val source = mutablePreferencesOf(
            stringPreferencesKey("server_url") to "https://emby/",
            stringPreferencesKey("token") to "token",
            stringPreferencesKey("user_id") to "alex",
            booleanPreferencesKey("auto_play_next_episode") to false,
        )

        val migrated = SettingsMigrationLogic.migrateToCurrent(source)
        val profile = json.decodeFromString<List<EmbyProfile>>(
            migrated[stringPreferencesKey("profiles")]!!,
        ).single()

        assertEquals(CURRENT_SETTINGS_SCHEMA_FOR_TEST, migrated[intPreferencesKey("settings_schema_version")])
        assertFalse(profile.autoPlayNextEpisode)
        // Never synced, so the first sync pushes these values up rather than pulling
        // defaults down over them.
        assertEquals(0L, profile.preferencesRevision)
    }

    @Test
    fun `migration is idempotent and does not alter a future schema`() {
        val current = SettingsMigrationLogic.migrateToCurrent(mutablePreferencesOf())
        assertEquals(current, SettingsMigrationLogic.migrateToCurrent(current))

        val future = mutablePreferencesOf(
            intPreferencesKey("settings_schema_version") to 99,
            stringPreferencesKey("profiles") to "future-format",
        )
        val unchanged = SettingsMigrationLogic.migrateToCurrent(future)
        assertNotNull(unchanged)
        assertEquals("future-format", unchanged[stringPreferencesKey("profiles")])
        assertEquals(99, unchanged[intPreferencesKey("settings_schema_version")])
    }
}
