package com.doublesymmetry.trackplayer.models import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.doubleOrNull import kotlinx.serialization.json.jsonPrimitive 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 BrowseTreeTest { @Test fun `store and load round-trip preserves data`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "t1", url = "https://example.com/t1.mp3", title = "Track 1") )) )) val context = RuntimeEnvironment.getApplication() tree.store(context) val loaded = BrowseTree.load(context) assertEquals(1, loaded.categories.size) assertEquals("Music", loaded.categories[0].title) assertEquals("t1", loaded.categories[0].items[0].mediaId) } @Test fun `load with corrupt JSON returns empty tree`() { val context = RuntimeEnvironment.getApplication() val prefs = context.getSharedPreferences("TrackPlayerPrefs", 0) prefs.edit().putString("browse_tree", "not valid json{{{").apply() val tree = BrowseTree.load(context) assertTrue(tree.categories.isEmpty()) } @Test fun `load with no saved data returns empty tree`() { val context = RuntimeEnvironment.getApplication() val tree = BrowseTree.load(context) assertTrue(tree.categories.isEmpty()) } @Test fun `findCategory returns matching category`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", emptyList()), BrowseCategory("cat-2", "Podcasts", emptyList()), )) assertEquals("Podcasts", tree.findCategory("cat-2")?.title) } @Test fun `findCategory returns null for unknown id`() { val tree = BrowseTree(listOf(BrowseCategory("cat-1", "Music", emptyList()))) assertNull(tree.findCategory("unknown")) } @Test fun `findItem finds top-level item`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "t1", url = "a.mp3", title = "Track 1"), BrowseMediaItem(mediaId = "t2", url = "b.mp3", title = "Track 2"), )) )) assertEquals("Track 2", tree.findItem("t2")?.title) } @Test fun `findItem finds deeply nested item`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "album-1", title = "Album", children = listOf( BrowseMediaItem(mediaId = "disc-1", title = "Disc 1", children = listOf( BrowseMediaItem(mediaId = "deep-track", url = "deep.mp3", title = "Deep Track") )) )) )) )) val found = tree.findItem("deep-track") assertEquals("Deep Track", found?.title) assertEquals("deep.mp3", found?.url) } @Test fun `findItem returns null for unknown id`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "t1", url = "a.mp3") )) )) assertNull(tree.findItem("nonexistent")) } @Test fun `isPlayable is true when url is present`() { val item = BrowseMediaItem(mediaId = "t1", url = "a.mp3") assertTrue(item.isPlayable) assertFalse(item.isBrowsable) } @Test fun `isBrowsable is true when children present and no url`() { val item = BrowseMediaItem(mediaId = "folder", children = listOf( BrowseMediaItem(mediaId = "t1", url = "a.mp3") )) assertTrue(item.isBrowsable) assertFalse(item.isPlayable) } @Test fun `url takes precedence over children for playability`() { val item = BrowseMediaItem(mediaId = "t1", url = "a.mp3", children = listOf( BrowseMediaItem(mediaId = "t2", url = "b.mp3") )) assertTrue(item.isPlayable) assertFalse(item.isBrowsable) } @Test fun `toMediaItem sets playable metadata for track`() { val item = BrowseMediaItem( mediaId = "t1", url = "https://example.com/t1.mp3", title = "Song", artist = "Artist", ) val mediaItem = item.toMediaItem() assertEquals("t1", mediaItem.mediaId) assertEquals("Song", mediaItem.mediaMetadata.title.toString()) assertEquals(true, mediaItem.mediaMetadata.isPlayable) assertEquals(false, mediaItem.mediaMetadata.isBrowsable) } @Test fun `toMediaItem sets browsable metadata for folder`() { val item = BrowseMediaItem( mediaId = "album-1", title = "Album", children = listOf(BrowseMediaItem(mediaId = "t1", url = "a.mp3")), ) val mediaItem = item.toMediaItem() assertEquals(false, mediaItem.mediaMetadata.isPlayable) assertEquals(true, mediaItem.mediaMetadata.isBrowsable) } @Test fun `store and load preserves nested children`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "album", title = "Album", children = listOf( BrowseMediaItem(mediaId = "t1", url = "a.mp3", title = "Track 1"), BrowseMediaItem(mediaId = "t2", url = "b.mp3", title = "Track 2"), )) )) )) val context = RuntimeEnvironment.getApplication() tree.store(context) val loaded = BrowseTree.load(context) val album = loaded.categories[0].items[0] assertEquals("album", album.mediaId) assertNotNull(album.children) assertEquals(2, album.children!!.size) assertEquals("t1", album.children!![0].mediaId) } @Test fun `store and load preserves extras on browse items`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem( mediaId = "t1", url = "https://example.com/t1.mp3", title = "Track 1", extras = JsonObject(mapOf( "source" to JsonPrimitive("recommendations"), "weight" to JsonPrimitive(0.85), )), ) )) )) val context = RuntimeEnvironment.getApplication() tree.store(context) val loaded = BrowseTree.load(context) val extras = loaded.categories[0].items[0].extras assertNotNull(extras) assertEquals("recommendations", extras!!["source"]!!.jsonPrimitive.content) assertEquals(0.85, extras["weight"]!!.jsonPrimitive.doubleOrNull!!, 0.001) } @Test fun `toMediaItem stores app extras under rntp extras key`() { val item = BrowseMediaItem( mediaId = "t1", url = "https://example.com/t1.mp3", title = "Song", duration = 120.0, extras = JsonObject(mapOf( "source" to JsonPrimitive("recommendations"), "isrc" to JsonPrimitive("USRC17607839"), )), ) val mediaItem = item.toMediaItem() val metadataExtras = mediaItem.mediaMetadata.extras assertNotNull(metadataExtras) assertEquals(120.0, metadataExtras!!.getDouble("duration"), 0.001) val nested = metadataExtras.getBundle(MediaItemExtras.METADATA_KEY) assertNotNull(nested) assertEquals("recommendations", nested!!.getString("source")) assertEquals("USRC17607839", nested.getString("isrc")) } @Test fun `browse item extras survive round-trip through TrackPlayerMediaItem`() { val item = BrowseMediaItem( mediaId = "t1", url = "https://example.com/t1.mp3", title = "Song", extras = JsonObject(mapOf( "source" to JsonPrimitive("recommendations"), "weight" to JsonPrimitive(0.85), )), ) val restored = TrackPlayerMediaItem.fromMediaItem(item.toMediaItem()) assertEquals("t1", restored.mediaId) assertEquals("Song", restored.title) assertNotNull(restored.extras) assertEquals("recommendations", restored.extras!!.getString("source")) assertEquals(0.85, restored.extras!!.getDouble("weight"), 0.001) } private fun playlistTree(): BrowseTree { return BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "t1", url = "a.mp3", title = "Track 1"), BrowseMediaItem(mediaId = "t2", url = "b.mp3", title = "Track 2"), BrowseMediaItem(mediaId = "t3", url = "c.mp3", title = "Track 3"), )) )) } @Test fun `findPlayableSiblings returns category-level siblings and start index`() { val tree = playlistTree() val (siblings, startIndex) = tree.findPlayableSiblings("t2")!! assertEquals(listOf("t1", "t2", "t3"), siblings.map { it.mediaId }) assertEquals(1, startIndex) } @Test fun `findPlayableSiblings skips browsable-only siblings`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "t1", url = "a.mp3"), BrowseMediaItem(mediaId = "album", title = "Album", children = listOf( BrowseMediaItem(mediaId = "t2", url = "b.mp3"), )), BrowseMediaItem(mediaId = "t3", url = "c.mp3"), )) )) val (siblings, startIndex) = tree.findPlayableSiblings("t3")!! assertEquals(listOf("t1", "t3"), siblings.map { it.mediaId }) assertEquals(1, startIndex) } @Test fun `findPlayableSiblings returns nested album children`() { val tree = BrowseTree(listOf( BrowseCategory("cat-1", "Music", listOf( BrowseMediaItem(mediaId = "album", title = "Album", children = listOf( BrowseMediaItem(mediaId = "t1", url = "a.mp3"), BrowseMediaItem(mediaId = "t2", url = "b.mp3"), BrowseMediaItem(mediaId = "t3", url = "c.mp3"), )) )) )) val (siblings, startIndex) = tree.findPlayableSiblings("t3")!! assertEquals(listOf("t1", "t2", "t3"), siblings.map { it.mediaId }) assertEquals(2, startIndex) } @Test fun `findPlayableSiblings returns null for unknown mediaId`() { val tree = playlistTree() assertNull(tree.findPlayableSiblings("missing")) } }