package com.doublesymmetry.trackplayer.models import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMapKeySetIterator import io.mockk.every import io.mockk.mockk import org.junit.Test import org.junit.Assert.* import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class PlayerConfigTest { // ---- Default config ---- @Test fun `default config has sensible defaults`() { val config = PlayerConfig() assertEquals("music", config.contentType) assertTrue(config.handleAudioBecomingNoisy) assertTrue(config.autoUpdateMetadataFromStream) assertEquals(WakeMode.NONE, config.wakeMode) assertFalse(config.skipSilenceEnabled) assertEquals(TaskRemovedBehavior.CONTINUE, config.taskRemovedBehavior) assertEquals(listOf(PlayerCommand.PLAY_PAUSE), config.availableCommands) assertEquals(RemoteControlHandling.NATIVE, config.remoteControlHandling) assertTrue(config.perCommandHandling.isEmpty()) assertEquals(15L, config.forwardInterval) assertEquals(15L, config.backwardInterval) assertNull(config.notificationChannelId) assertNull(config.notificationChannelName) assertNull(config.notificationSmallIcon) assertNull(config.cacheMaxSizeBytes) assertNull(config.castReceiverAppId) assertEquals(0.0, config.progressSyncIntervalSeconds, 0.001) assertNull(config.progressSyncHttpUrl) assertNull(config.progressSyncHttpHeaders) } // ---- Store and load round-trip ---- @Test fun `store and load round-trip preserves config`() { val context = RuntimeEnvironment.getApplication() val config = PlayerConfig( contentType = "speech", handleAudioBecomingNoisy = false, autoUpdateMetadataFromStream = false, wakeMode = WakeMode.NETWORK, skipSilenceEnabled = true, forwardInterval = 30L, backwardInterval = 10L, notificationChannelId = "my-channel", notificationChannelName = "My Player", notificationSmallIcon = "ic_notification", cacheMaxSizeBytes = 50L * 1024 * 1024, castReceiverAppId = "ABCD1234", progressSyncIntervalSeconds = 5.0, progressSyncHttpUrl = "https://example.com/progress", taskRemovedBehavior = TaskRemovedBehavior.STOP, ) config.store(context) val loaded = config.load(context) assertEquals("speech", loaded.contentType) assertFalse(loaded.handleAudioBecomingNoisy) assertFalse(loaded.autoUpdateMetadataFromStream) assertEquals(WakeMode.NETWORK, loaded.wakeMode) assertTrue(loaded.skipSilenceEnabled) assertEquals(30L, loaded.forwardInterval) assertEquals(10L, loaded.backwardInterval) assertEquals("my-channel", loaded.notificationChannelId) assertEquals("My Player", loaded.notificationChannelName) assertEquals("ic_notification", loaded.notificationSmallIcon) assertEquals(50L * 1024 * 1024, loaded.cacheMaxSizeBytes) assertEquals("ABCD1234", loaded.castReceiverAppId) assertEquals(5.0, loaded.progressSyncIntervalSeconds, 0.001) assertEquals("https://example.com/progress", loaded.progressSyncHttpUrl) assertEquals(TaskRemovedBehavior.STOP, loaded.taskRemovedBehavior) } @Test fun `load with no stored data returns default config`() { val context = RuntimeEnvironment.getApplication() // Ensure a fresh prefs state by using a unique pref name val prefs = context.getSharedPreferences("TrackPlayerPrefs", 0) prefs.edit().remove("player_config").apply() val loaded = PlayerConfig().load(context) assertEquals("music", loaded.contentType) assertEquals(WakeMode.NONE, loaded.wakeMode) } // ---- fromReadableMap ---- @Test fun `fromReadableMap parses contentType`() { val map = mockk(relaxed = true) { every { getString("contentType") } returns "speech" every { getMap("android") } returns null every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals("speech", config.contentType) } @Test fun `fromReadableMap defaults contentType to music when absent`() { val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns null every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals("music", config.contentType) } @Test fun `fromReadableMap parses android-specific config`() { val androidMap = mockk(relaxed = true) { every { getString("wakeMode") } returns "network" every { hasKey("skipSilenceEnabled") } returns true every { getBoolean("skipSilenceEnabled") } returns true every { hasKey("cast") } returns true every { getString("cast") } returns "RECV_APP_ID" every { getMap("notification") } returns null } val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns androidMap every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals(WakeMode.NETWORK, config.wakeMode) assertTrue(config.skipSilenceEnabled) assertEquals("RECV_APP_ID", config.castReceiverAppId) } @Test fun `fromReadableMap parses notification fields`() { val notifMap = mockk(relaxed = true) { every { getString("channelId") } returns "rntp-channel" every { getString("channelName") } returns "Music Player" every { getString("smallIcon") } returns "ic_music" } val androidMap = mockk(relaxed = true) { every { getString("wakeMode") } returns null every { hasKey("skipSilenceEnabled") } returns false every { hasKey("cast") } returns false every { getMap("notification") } returns notifMap } val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns androidMap every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals("rntp-channel", config.notificationChannelId) assertEquals("Music Player", config.notificationChannelName) assertEquals("ic_music", config.notificationSmallIcon) } @Test fun `fromReadableMap parses cache config`() { val cacheMap = mockk(relaxed = true) { every { hasKey("maxSizeBytes") } returns true every { getDouble("maxSizeBytes") } returns (200.0 * 1024 * 1024) } val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns null every { getMap("cache") } returns cacheMap every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals(200L * 1024 * 1024, config.cacheMaxSizeBytes) } @Test fun `fromReadableMap uses default cache size when maxSizeBytes absent`() { val cacheMap = mockk(relaxed = true) { every { hasKey("maxSizeBytes") } returns false } val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns null every { getMap("cache") } returns cacheMap every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) // Default cache size is 500MB assertEquals(500L * 1024 * 1024, config.cacheMaxSizeBytes) } @Test fun `fromReadableMap parses wakeMode none`() { val androidMap = mockk(relaxed = true) { every { getString("wakeMode") } returns "none" every { hasKey("skipSilenceEnabled") } returns false every { hasKey("cast") } returns false every { getMap("notification") } returns null } val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns androidMap every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals(WakeMode.NONE, config.wakeMode) } @Test fun `fromReadableMap parses wakeMode local`() { val androidMap = mockk(relaxed = true) { every { getString("wakeMode") } returns "local" every { hasKey("skipSilenceEnabled") } returns false every { hasKey("cast") } returns false every { getMap("notification") } returns null } val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns androidMap every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals(WakeMode.LOCAL, config.wakeMode) } // ---- withCommands (command handling modes) ---- @Test fun `withCommands sets native handling`() { val commandsMap = mockk(relaxed = true) { every { getString("handling") } returns "native" every { hasKey("capabilities") } returns false every { hasKey("perCommandHandling") } returns false every { hasKey("forwardInterval") } returns false every { hasKey("backwardInterval") } returns false } val config = PlayerConfig().withCommands(commandsMap) assertEquals(RemoteControlHandling.NATIVE, config.remoteControlHandling) } @Test fun `withCommands sets js handling`() { val commandsMap = mockk(relaxed = true) { every { getString("handling") } returns "js" every { hasKey("capabilities") } returns false every { hasKey("perCommandHandling") } returns false every { hasKey("forwardInterval") } returns false every { hasKey("backwardInterval") } returns false } val config = PlayerConfig().withCommands(commandsMap) assertEquals(RemoteControlHandling.JS, config.remoteControlHandling) } @Test fun `withCommands sets hybrid handling`() { val commandsMap = mockk(relaxed = true) { every { getString("handling") } returns "hybrid" every { hasKey("capabilities") } returns false every { hasKey("perCommandHandling") } returns false every { hasKey("forwardInterval") } returns false every { hasKey("backwardInterval") } returns false } val config = PlayerConfig().withCommands(commandsMap) assertEquals(RemoteControlHandling.HYBRID, config.remoteControlHandling) } @Test fun `withCommands parses perCommandHandling for hybrid mode`() { val perCommandIterator = mockk { every { hasNextKey() } returnsMany listOf(true, true, false) every { nextKey() } returnsMany listOf("next", "previous") } val perCommandMap = mockk(relaxed = true) { every { keySetIterator() } returns perCommandIterator every { getString("next") } returns "js" every { getString("previous") } returns "native" } val commandsMap = mockk(relaxed = true) { every { getString("handling") } returns "hybrid" every { hasKey("capabilities") } returns false every { hasKey("perCommandHandling") } returns true every { getMap("perCommandHandling") } returns perCommandMap every { hasKey("forwardInterval") } returns false every { hasKey("backwardInterval") } returns false } val config = PlayerConfig().withCommands(commandsMap) assertEquals(RemoteControlHandling.HYBRID, config.remoteControlHandling) assertEquals(RemoteControlHandling.JS, config.perCommandHandling["next"]) assertEquals(RemoteControlHandling.NATIVE, config.perCommandHandling["previous"]) } @Test fun `withCommands parses capabilities`() { val capabilitiesArray = mockk(relaxed = true) { every { size() } returns 3 every { toArrayList() } returns arrayListOf("seek", "next", "previous") } val commandsMap = mockk(relaxed = true) { every { getString("handling") } returns null every { hasKey("capabilities") } returns true every { getArray("capabilities") } returns capabilitiesArray every { hasKey("perCommandHandling") } returns false every { hasKey("forwardInterval") } returns false every { hasKey("backwardInterval") } returns false } val config = PlayerConfig().withCommands(commandsMap) assertTrue(config.availableCommands.contains(PlayerCommand.SEEK)) assertTrue(config.availableCommands.contains(PlayerCommand.NEXT)) assertTrue(config.availableCommands.contains(PlayerCommand.PREVIOUS)) assertFalse(config.availableCommands.contains(PlayerCommand.PLAY_PAUSE)) } @Test fun `withCommands parses forwardInterval and backwardInterval`() { val commandsMap = mockk(relaxed = true) { every { getString("handling") } returns null every { hasKey("capabilities") } returns false every { hasKey("perCommandHandling") } returns false every { hasKey("forwardInterval") } returns true every { getDouble("forwardInterval") } returns 30.0 every { hasKey("backwardInterval") } returns true every { getDouble("backwardInterval") } returns 5.0 } val config = PlayerConfig().withCommands(commandsMap) assertEquals(30L, config.forwardInterval) assertEquals(5L, config.backwardInterval) } @Test fun `withCommands with unknown handling preserves existing handling`() { val commandsMap = mockk(relaxed = true) { every { getString("handling") } returns "unknown-value" every { hasKey("capabilities") } returns false every { hasKey("perCommandHandling") } returns false every { hasKey("forwardInterval") } returns false every { hasKey("backwardInterval") } returns false } val original = PlayerConfig(remoteControlHandling = RemoteControlHandling.JS) val config = original.withCommands(commandsMap) assertEquals(RemoteControlHandling.JS, config.remoteControlHandling) } @Test fun `fromReadableMap defaults autoUpdateMetadataFromStream to true`() { val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns null every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertTrue(config.autoUpdateMetadataFromStream) } @Test fun `fromReadableMap parses autoUpdateMetadataFromStream when false`() { val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns null every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false every { hasKey("autoUpdateMetadataFromStream") } returns true every { getBoolean("autoUpdateMetadataFromStream") } returns false } val config = PlayerConfig.fromReadableMap(map) assertFalse(config.autoUpdateMetadataFromStream) } @Test fun `fromReadableMap with null android section defaults wakeMode to NONE`() { val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns null every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals(WakeMode.NONE, config.wakeMode) assertFalse(config.skipSilenceEnabled) assertNull(config.castReceiverAppId) assertEquals(TaskRemovedBehavior.CONTINUE, config.taskRemovedBehavior) } @Test fun `fromReadableMap parses taskRemovedBehavior stop`() { val androidMap = mockk(relaxed = true) { every { getString("wakeMode") } returns null every { getString("taskRemovedBehavior") } returns "stop" every { hasKey("skipSilenceEnabled") } returns false every { hasKey("cast") } returns false every { getMap("notification") } returns null } val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getMap("android") } returns androidMap every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals(TaskRemovedBehavior.STOP, config.taskRemovedBehavior) } @Test fun `default audioMixing is exclusive`() { val config = PlayerConfig() assertEquals("exclusive", config.audioMixing) } @Test fun `store and load round-trip preserves audioMixing`() { val context = RuntimeEnvironment.getApplication() val config = PlayerConfig(audioMixing = "mix") config.store(context) val loaded = config.load(context) assertEquals("mix", loaded.audioMixing) } @Test fun `fromReadableMap parses audioMixing mix`() { val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getString("audioMixing") } returns "mix" every { getMap("android") } returns null every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals("mix", config.audioMixing) } @Test fun `fromReadableMap defaults audioMixing to exclusive when absent`() { val map = mockk(relaxed = true) { every { getString("contentType") } returns null every { getString("audioMixing") } returns null every { getMap("android") } returns null every { getMap("cache") } returns null every { getMap("progressSync") } returns null every { hasKey(any()) } returns false } val config = PlayerConfig.fromReadableMap(map) assertEquals("exclusive", config.audioMixing) } }