package xyz.cottongin.radio247.audio

import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.ByteArrayInputStream

class IcyParserTest {

    private data class IcyBlock(
        val audioData: ByteArray,
        val metadata: String? = null // null means length byte = 0
    ) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false
            other as IcyBlock
            if (!audioData.contentEquals(other.audioData)) return false
            if (metadata != other.metadata) return false
            return true
        }

        override fun hashCode(): Int {
            var result = audioData.contentHashCode()
            result = 31 * result + (metadata?.hashCode() ?: 0)
            return result
        }
    }

    private fun padMetadata(meta: String): ByteArray {
        val bytes = meta.toByteArray(Charsets.UTF_8)
        val paddedSize = ((bytes.size + 15) / 16) * 16
        return bytes.copyOf(paddedSize)
    }

    private fun buildIcyStream(metaint: Int, vararg blocks: IcyBlock): ByteArray {
        val output = mutableListOf<Byte>()
        for (block in blocks) {
            require(block.audioData.size == metaint) {
                "Each block must have exactly metaint=$metaint audio bytes, got ${block.audioData.size}"
            }
            output.addAll(block.audioData.toList())
            if (block.metadata == null) {
                output.add(0) // length byte = 0
            } else {
                val padded = padMetadata(block.metadata)
                output.add((padded.size / 16).toByte()) // length byte
                output.addAll(padded.toList())
            }
        }
        return output.toByteArray()
    }

    @Test
    fun separatesAudioAndMetadataCorrectly() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x42 }
        val stream = buildIcyStream(
            metaint,
            IcyBlock(audioChunk, "StreamTitle='Test Title';"),
            IcyBlock(audioChunk, null)
        )

        val audioCollected = mutableListOf<Byte>()
        val metadataEvents = mutableListOf<IcyMetadata>()

        IcyParser(
            input = ByteArrayInputStream(stream),
            metaint = metaint,
            onAudioData = { buf, off, len -> (0 until len).forEach { audioCollected.add(buf[off + it]) } },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(16, audioCollected.size)
        assertTrue(audioCollected.all { it == 0x42.toByte() })
        assertEquals(1, metadataEvents.size)
        assertEquals("Test Title", metadataEvents[0].title)
        assertEquals(null, metadataEvents[0].artist)
    }

    @Test
    fun parsesArtistAndTitleFromStreamTitle() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x00 }
        val stream = buildIcyStream(
            metaint,
            IcyBlock(audioChunk, "StreamTitle='Daft Punk - Around The World';")
        )

        val metadataEvents = mutableListOf<IcyMetadata>()
        IcyParser(
            input = ByteArrayInputStream(stream),
            metaint = metaint,
            onAudioData = { _, _, _ -> },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(1, metadataEvents.size)
        assertEquals("Daft Punk", metadataEvents[0].artist)
        assertEquals("Around The World", metadataEvents[0].title)
    }

    @Test
    fun handlesTitleWithoutArtistSeparator() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x00 }
        val stream = buildIcyStream(
            metaint,
            IcyBlock(audioChunk, "StreamTitle='The Morning Show';")
        )

        val metadataEvents = mutableListOf<IcyMetadata>()
        IcyParser(
            input = ByteArrayInputStream(stream),
            metaint = metaint,
            onAudioData = { _, _, _ -> },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(1, metadataEvents.size)
        assertEquals(null, metadataEvents[0].artist)
        assertEquals("The Morning Show", metadataEvents[0].title)
    }

    @Test
    fun handlesEmptyMetadataBlocks() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0xAB.toByte() }
        val stream = buildIcyStream(
            metaint,
            IcyBlock(audioChunk, null),
            IcyBlock(audioChunk, null),
            IcyBlock(audioChunk, null)
        )

        val audioCollected = mutableListOf<Byte>()
        val metadataEvents = mutableListOf<IcyMetadata>()

        IcyParser(
            input = ByteArrayInputStream(stream),
            metaint = metaint,
            onAudioData = { buf, off, len -> (0 until len).forEach { audioCollected.add(buf[off + it]) } },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(24, audioCollected.size)
        assertTrue(audioCollected.all { it == 0xAB.toByte() })
        assertEquals(0, metadataEvents.size)
    }

    @Test
    fun passthroughModeWhenMetaintIsNull() {
        val data = ByteArray(1000) { it.toByte() }
        val audioCollected = mutableListOf<Byte>()
        val metadataEvents = mutableListOf<IcyMetadata>()

        IcyParser(
            input = ByteArrayInputStream(data),
            metaint = null,
            onAudioData = { buf, off, len -> (0 until len).forEach { audioCollected.add(buf[off + it]) } },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(1000, audioCollected.size)
        assertTrue(audioCollected.withIndex().all { (i, b) -> b == data[i] })
        assertEquals(0, metadataEvents.size)
    }

    @Test
    fun handlesMaximumSizeMetadata() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x00 }
        // 255 * 16 = 4080 bytes. Build metadata that pads to exactly that size.
        val metaContent = "StreamTitle='Big';" + "x".repeat(4060)
        val padded = padMetadata(metaContent)
        assertEquals(4080, padded.size)
        assertEquals(255, padded.size / 16)

        val stream = buildIcyStream(metaint, IcyBlock(audioChunk, metaContent))

        val metadataEvents = mutableListOf<IcyMetadata>()
        IcyParser(
            input = ByteArrayInputStream(stream),
            metaint = metaint,
            onAudioData = { _, _, _ -> },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(1, metadataEvents.size)
        assertEquals("Big", metadataEvents[0].title)
    }

    @Test
    fun handlesMultipleConsecutiveMetadataChanges() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x00 }
        val stream = buildIcyStream(
            metaint,
            IcyBlock(audioChunk, "StreamTitle='First Song';"),
            IcyBlock(audioChunk, "StreamTitle='Second Song';"),
            IcyBlock(audioChunk, "StreamTitle='Third Song';")
        )

        val metadataEvents = mutableListOf<IcyMetadata>()
        IcyParser(
            input = ByteArrayInputStream(stream),
            metaint = metaint,
            onAudioData = { _, _, _ -> },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(3, metadataEvents.size)
        assertEquals("First Song", metadataEvents[0].title)
        assertEquals("Second Song", metadataEvents[1].title)
        assertEquals("Third Song", metadataEvents[2].title)
    }

    @Test
    fun handlesMetadataWithStreamUrlField() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x00 }
        val stream = buildIcyStream(
            metaint,
            IcyBlock(audioChunk, "StreamTitle='Song';StreamUrl='http://art.jpg';")
        )

        val metadataEvents = mutableListOf<IcyMetadata>()
        IcyParser(
            input = ByteArrayInputStream(stream),
            metaint = metaint,
            onAudioData = { _, _, _ -> },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(1, metadataEvents.size)
        assertEquals("Song", metadataEvents[0].title)
        assertEquals("http://art.jpg", metadataEvents[0].streamUrl)
        assertTrue(
            metadataEvents[0].raw.contains("StreamTitle") &&
                metadataEvents[0].raw.contains("StreamUrl")
        )
    }

    @Test
    fun handlesTruncatedStreamGracefully() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x42 }
        val fullStream = buildIcyStream(
            metaint,
            IcyBlock(audioChunk, null),
            IcyBlock(audioChunk, null)
        )
        // Truncate mid-second-audio-chunk (after 8 audio + 1 length byte = 9 bytes, plus 4 more audio = 13 total)
        val truncated = fullStream.copyOf(13)

        val audioCollected = mutableListOf<Byte>()
        val metadataEvents = mutableListOf<IcyMetadata>()

        IcyParser(
            input = ByteArrayInputStream(truncated),
            metaint = metaint,
            onAudioData = { buf, off, len -> (0 until len).forEach { audioCollected.add(buf[off + it]) } },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        // First 8 bytes audio, then 4 bytes of second chunk before EOF
        assertEquals(12, audioCollected.size)
        assertTrue(audioCollected.all { it == 0x42.toByte() })
        assertEquals(0, metadataEvents.size)
    }

    @Test
    fun handlesTruncationMidMetadata_noCrash_noIncompleteMetadataEmitted() {
        val metaint = 8
        val audioChunk = ByteArray(8) { 0x42 }
        val fullStream = buildIcyStream(metaint, IcyBlock(audioChunk, "StreamTitle='Test';"))
        // Full audio (8) + length byte (2 = 32 bytes) + only 16 bytes of metadata (truncated)
        val truncated =
            fullStream.copyOf(8 + 1 + 16)

        val audioCollected = mutableListOf<Byte>()
        val metadataEvents = mutableListOf<IcyMetadata>()

        IcyParser(
            input = ByteArrayInputStream(truncated),
            metaint = metaint,
            onAudioData = { buf, off, len -> (0 until len).forEach { audioCollected.add(buf[off + it]) } },
            onMetadata = { metadataEvents.add(it) }
        ).readAll()

        assertEquals(8, audioCollected.size)
        assertTrue(audioCollected.all { it == 0x42.toByte() })
        assertEquals(0, metadataEvents.size)
    }
}
