import Testing import Foundation @testable import Mobile_Music_Assistant // MARK: - MAPlayer Decoding @Suite("MAPlayer Decoding") struct MAPlayerDecodingTests { private func decode(_ json: String) throws -> MAPlayer { try JSONDecoder().decode(MAPlayer.self, from: Data(json.utf8)) } @Test("Decodes all required fields correctly") func decodesFullPlayer() throws { let json = """ { "player_id": "spotify://player/1", "name": "Living Room", "state": "playing", "volume_level": 75, "powered": true, "available": true } """ let player = try decode(json) #expect(player.playerId == "spotify://player/1") #expect(player.name == "Living Room") #expect(player.state == .playing) #expect(player.volume == 75) #expect(player.powered == true) #expect(player.available == true) } @Test("powered defaults to false when missing; available defaults to true when missing") func defaultsMissingBooleans() throws { let json = """ {"player_id": "x", "name": "Test", "state": "idle"} """ let player = try decode(json) #expect(player.powered == false) #expect(player.available == true) } @Test("Unknown state falls back to idle") func unknownStateFallsBackToIdle() throws { let json = """ {"player_id": "x", "name": "Test", "state": "banana"} """ let player = try decode(json) #expect(player.state == .idle) } @Test("Parses all known PlayerState values") func allPlayerStates() throws { for (raw, expected): (String, PlayerState) in [ ("idle", .idle), ("playing", .playing), ("paused", .paused) ] { let json = """ {"player_id": "x", "name": "T", "state": "\(raw)"} """ let player = try decode(json) #expect(player.state == expected) } } @Test("Accepts integer or double for volume_level") func volumeIntOrDouble() throws { let jsonInt = #"{"player_id":"x","name":"T","state":"idle","volume_level":50}"# let jsonDbl = #"{"player_id":"x","name":"T","state":"idle","volume_level":50.7}"# let p1 = try decode(jsonInt) let p2 = try decode(jsonDbl) #expect(p1.volume == 50) #expect(p2.volume == 50) } @Test("isGroupLeader is true when groupChilds is non-empty") func isGroupLeader() throws { let json = """ {"player_id":"x","name":"T","state":"idle","group_childs":["a","b"]} """ let player = try decode(json) #expect(player.isGroupLeader == true) } @Test("isGroupLeader is false when groupChilds is empty") func isNotGroupLeader() throws { let json = #"{"player_id":"x","name":"T","state":"idle","group_childs":[]}"# let player = try decode(json) #expect(player.isGroupLeader == false) } @Test("isSyncMember is true when syncLeader is set") func isSyncMember() throws { let json = """ {"player_id":"x","name":"T","state":"idle","sync_leader":"leader_id"} """ let player = try decode(json) #expect(player.isSyncMember == true) } @Test("isSyncMember is false when syncLeader is nil") func isNotSyncMember() throws { let json = #"{"player_id":"x","name":"T","state":"idle"}"# let player = try decode(json) #expect(player.isSyncMember == false) } } // MARK: - MAMediaItem Decoding @Suite("MAMediaItem Decoding") struct MAMediaItemDecodingTests { private func decode(_ json: String) throws -> MAMediaItem { try JSONDecoder().decode(MAMediaItem.self, from: Data(json.utf8)) } @Test("Extracts imageUrl from metadata.images") func imageUrlFromMetadata() throws { let json = """ { "uri": "spotify://track/1", "name": "Song", "metadata": { "images": [ {"type": "thumb", "path": "/img/thumb.jpg"} ] } } """ let item = try decode(json) #expect(item.imageUrl == "/img/thumb.jpg") } @Test("Extracts imageUrl from direct image field") func imageUrlFromDirectField() throws { let json = """ { "uri": "spotify://track/1", "name": "Song", "image": {"type": "thumb", "path": "/direct.jpg"} } """ let item = try decode(json) #expect(item.imageUrl == "/direct.jpg") } @Test("imageUrl is nil when no image data present") func imageUrlNilWhenMissing() throws { let json = #"{"uri":"x","name":"T"}"# let item = try decode(json) #expect(item.imageUrl == nil) } @Test("Prefers metadata over direct image field") func prefersMetadataOverDirectImage() throws { let json = """ { "uri": "x", "name": "T", "metadata": {"images": [{"type": "thumb", "path": "/meta.jpg"}]}, "image": {"type": "thumb", "path": "/direct.jpg"} } """ let item = try decode(json) #expect(item.imageUrl == "/meta.jpg") } @Test("Decodes artist name") func decodesArtistName() throws { let json = """ { "uri": "x", "name": "Song", "artists": [{"item_id": "1", "uri": "a://artist/1", "name": "Queen"}] } """ let item = try decode(json) #expect(item.artists?.first?.name == "Queen") } @Test("Accepts Int or Double for duration") func durationIntOrDouble() throws { let j1 = #"{"uri":"x","name":"T","duration":240}"# let j2 = #"{"uri":"x","name":"T","duration":240.9}"# let i1 = try decode(j1) let i2 = try decode(j2) #expect(i1.duration == 240) #expect(i2.duration == 240) } @Test("favorite defaults to false when missing") func favoriteDefaultsFalse() throws { let json = #"{"uri":"x","name":"T"}"# let item = try decode(json) #expect(item.favorite == false) } } // MARK: - MAQueueItem Decoding @Suite("MAQueueItem Decoding") struct MAQueueItemDecodingTests { private func decode(_ json: String) throws -> MAQueueItem { try JSONDecoder().decode(MAQueueItem.self, from: Data(json.utf8)) } @Test("Decodes queue item with media item") func decodesWithMediaItem() throws { let json = """ { "queue_item_id": "q1", "name": "Bohemian Rhapsody", "media_item": {"uri": "spotify://track/1", "name": "Bohemian Rhapsody"} } """ let item = try decode(json) #expect(item.queueItemId == "q1") #expect(item.name == "Bohemian Rhapsody") #expect(item.mediaItem?.name == "Bohemian Rhapsody") } @Test("mediaItem is nil when absent from JSON") func mediaItemNilWhenAbsent() throws { let json = #"{"queue_item_id":"q1","name":"Track"}"# let item = try decode(json) #expect(item.mediaItem == nil) } @Test("Tolerates malformed mediaItem without throwing") func toleratesMalformedMediaItem() throws { let json = #"{"queue_item_id":"q1","name":"T","media_item":"broken"}"# // Should decode successfully, mediaItem == nil let item = try decode(json) #expect(item.mediaItem == nil) } } // MARK: - PlayerState @Suite("PlayerState") struct PlayerStateTests { @Test("All raw values round-trip correctly") func rawValueRoundTrip() { let states: [(String, PlayerState)] = [ ("idle", .idle), ("playing", .playing), ("paused", .paused) ] for (raw, state) in states { #expect(PlayerState(rawValue: raw) == state) #expect(state.rawValue == raw) } } @Test("Unknown raw value returns nil") func unknownRawValueIsNil() { #expect(PlayerState(rawValue: "streaming") == nil) #expect(PlayerState(rawValue: "") == nil) } } // MARK: - MAPlayerQueue Decoding @Suite("MAPlayerQueue Decoding") struct MAPlayerQueueDecodingTests { private func decode(_ json: String) throws -> MAPlayerQueue { try JSONDecoder().decode(MAPlayerQueue.self, from: Data(json.utf8)) } @Test("Decodes queue with currentItem") func decodesWithCurrentItem() throws { let json = """ { "queue_id": "q://1", "current_index": 2, "elapsed_time": 45.5, "current_item": {"queue_item_id": "qi1", "name": "Track"} } """ let queue = try decode(json) #expect(queue.queueId == "q://1") #expect(queue.currentIndex == 2) #expect(queue.currentItem?.name == "Track") } @Test("currentItem is nil when absent") func currentItemNilWhenAbsent() throws { let json = #"{"queue_id":"q://1"}"# let queue = try decode(json) #expect(queue.currentItem == nil) } @Test("Tolerates malformed currentItem gracefully") func toleratesMalformedCurrentItem() throws { let json = #"{"queue_id":"q://1","current_item":null}"# let queue = try decode(json) #expect(queue.currentItem == nil) } }