package xyz.cottongin.radio247.audio

import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test

class Mp3FrameSyncTest {

    /**
     * Build MPEG1 Layer3 header: 0xFF, (0xE0 | version<<3 | layer<<1 | crc), byte2, byte3
     * MPEG1=11, Layer3=01, noCRC=1. Bitrate in 15-12, sample in 11-10, padding in bit 9.
     * channelMode: 0=Stereo, 1=JointStereo, 2=DualChannel, 3=Mono
     */
    private fun buildMpeg1Layer3Header(
        bitrateIndex: Int,
        sampleRateIndex: Int,
        padding: Boolean,
        channelMode: Int = 0
    ): ByteArray {
        val byte1 = 0xFB.toByte()
        val byte2 = ((bitrateIndex shl 4) or (sampleRateIndex shl 2) or (if (padding) 2 else 0)).toByte()
        val byte3 = (channelMode shl 6).toByte()
        return byteArrayOf(0xFF.toByte(), byte1, byte2, byte3)
    }

    /**
     * Build MPEG2 Layer3 header. MPEG2 = 10, Layer3 = 01.
     * channelMode: 0=Stereo, 1=JointStereo, 2=DualChannel, 3=Mono
     */
    private fun buildMpeg2Layer3Header(
        bitrateIndex: Int,
        sampleRateIndex: Int,
        padding: Boolean,
        channelMode: Int = 0
    ): ByteArray {
        val byte1 = 0xF3.toByte()
        val byte2 = ((bitrateIndex shl 4) or (sampleRateIndex shl 2) or (if (padding) 2 else 0)).toByte()
        val byte3 = (channelMode shl 6).toByte()
        return byteArrayOf(0xFF.toByte(), byte1, byte2, byte3)
    }

    private fun buildFrame(header: ByteArray, bodySize: Int): ByteArray {
        return header + ByteArray(bodySize) { 0 }
    }

    @Test
    fun findsFrameAtStartOfData() {
        // MPEG1 Layer3, 128kbps, 44100Hz, no padding → frameSize = 417 bytes.
        // Header bytes: 0xFF, 0xFB, 0x90, 0x00 per task spec
        val header = byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x90.toByte(), 0x00)
        val bodySize = 417 - 4
        val frame1 = buildFrame(header, bodySize)
        // Add another valid header for two-frame validation (4 bytes only)
        val nextHeader = byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x90.toByte(), 0x00)
        val data = frame1 + nextHeader

        val frames = mutableListOf<ByteArray>()
        Mp3FrameSync { frames.add(it) }.feed(data)
        assertEquals(1, frames.size)
        assertEquals(417, frames[0].size)
        assertArrayEquals(header, frames[0].copyOfRange(0, 4))
    }

    @Test
    fun resyncsAfterGarbageBytes() {
        val garbage = ByteArray(100) { 0x42 }
        val header = byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x90.toByte(), 0x00)
        val bodySize = 417 - 4
        val frame = buildFrame(header, bodySize)
        // Need second frame for two-frame validation
        val frame2 = buildFrame(header, bodySize)
        val data = garbage + frame + frame2

        val frames = mutableListOf<ByteArray>()
        Mp3FrameSync { frames.add(it) }.feed(data)

        assertEquals(2, frames.size)
        assertEquals(417, frames[0].size)
    }

    @Test
    fun calculatesFrameSizeCorrectlyForVariousBitrates() {
        // MPEG1 Layer3: 128kbps/44100Hz = 417, 192kbps/44100Hz = 626, 320kbps/44100Hz = 1044
        val configs = listOf(
            Triple(9, 0, 417),  // 128kbps, 44100Hz
            Triple(11, 0, 626), // 192kbps, 44100Hz
            Triple(14, 0, 1044) // 320kbps, 44100Hz
        )
        for ((bitrateIdx, sampleIdx, expectedSize) in configs) {
            val header = buildMpeg1Layer3Header(bitrateIdx, sampleIdx, padding = false)
            val bodySize = expectedSize - 4
            val frame = buildFrame(header, bodySize)
            val nextHeader = buildMpeg1Layer3Header(bitrateIdx, sampleIdx, padding = false)
            val nextFrame = buildFrame(nextHeader, expectedSize - 4)
            val data = frame + nextFrame

            val frames = mutableListOf<ByteArray>()
            Mp3FrameSync { frames.add(it) }.feed(data)

            assertEquals("bitrateIndex=$bitrateIdx", 2, frames.size)
            assertEquals("bitrateIndex=$bitrateIdx", expectedSize, frames[0].size)
            assertEquals("bitrateIndex=$bitrateIdx", expectedSize, frames[1].size)
        }
    }

    @Test
    fun handlesPaddingBit() {
        // MPEG1 Layer3, 128kbps, 44100Hz, WITH padding → frameSize = 418. 0x92 = bitrate 9, padding.
        val header = byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x92.toByte(), 0x00) // padding bit set
        val bodySize = 418 - 4
        val frame = buildFrame(header, bodySize)
        val nextHeader = byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x92.toByte(), 0x00)
        val nextFrame = buildFrame(nextHeader, 418 - 4)
        val data = frame + nextFrame

        val frames = mutableListOf<ByteArray>()
        Mp3FrameSync { frames.add(it) }.feed(data)

        assertEquals(2, frames.size)
        assertEquals(418, frames[0].size)
    }

    @Test
    fun emitsCompleteFramesViaCallback() {
        val header = buildMpeg1Layer3Header(9, 0, false)
        val frame1 = buildFrame(header, 417 - 4)
        val frame2 = buildFrame(header, 417 - 4)
        val data = frame1 + frame2

        val frames = mutableListOf<ByteArray>()
        Mp3FrameSync { frames.add(it) }.feed(data)

        assertEquals(2, frames.size)
        assertEquals(417, frames[0].size)
        assertEquals(417, frames[1].size)
    }

    @Test
    fun handlesTruncatedFrameAtEnd() {
        val header = byteArrayOf(0xFF.toByte(), 0xFB.toByte(), 0x90.toByte(), 0x00)
        // Only half the body (413/2 ≈ 206 bytes instead of 413)
        val truncatedBody = ByteArray(206) { 0 }
        val data = header + truncatedBody

        val frames = mutableListOf<ByteArray>()
        Mp3FrameSync { frames.add(it) }.feed(data)

        assertEquals(0, frames.size)
    }

    @Test
    fun validatesFrameHeaderRejectsInvalidBitrate() {
        // Bitrate index 0 (free) - invalid
        val header0 = buildMpeg1Layer3Header(0, 0, false)
        val frame0 = buildFrame(header0, 100)
        val next0 = buildFrame(header0, 100)
        val data0 = frame0 + next0

        val frames0 = mutableListOf<ByteArray>()
        Mp3FrameSync { frames0.add(it) }.feed(data0)
        assertEquals(0, frames0.size)

        // Bitrate index 15 (bad) - invalid
        val header15 = buildMpeg1Layer3Header(15, 0, false)
        val frame15 = buildFrame(header15, 100)
        val next15 = buildFrame(header15, 100)
        val data15 = frame15 + next15

        val frames15 = mutableListOf<ByteArray>()
        Mp3FrameSync { frames15.add(it) }.feed(data15)
        assertEquals(0, frames15.size)
    }

    @Test
    fun handlesIncrementalFeeding() {
        val header = buildMpeg1Layer3Header(9, 0, false)
        val frame = buildFrame(header, 417 - 4)
        val nextFrame = buildFrame(header, 417 - 4)
        val data = frame + nextFrame

        val frames = mutableListOf<ByteArray>()
        val sync = Mp3FrameSync { frames.add(it) }
        var offset = 0
        val chunkSize = 10
        while (offset < data.size) {
            val len = minOf(chunkSize, data.size - offset)
            sync.feed(data, offset, len)
            offset += len
        }

        assertEquals(2, frames.size)
        assertEquals(417, frames[0].size)
        assertEquals(417, frames[1].size)
    }

    @Test
    fun handlesMpeg2Frames() {
        // MPEG2 Layer3, 128kbps, 22050Hz → frameSize = (72 * 128000 / 22050) + padding
        // 72 * 128000 / 22050 = 417.95... → 417 without padding, 418 with
        val header = buildMpeg2Layer3Header(12, 0, false) // index 12 = 128kbps for MPEG2
        val expectedSize = (72 * 128 * 1000 / 22050) // = 417
        val bodySize = expectedSize - 4
        val frame = buildFrame(header, bodySize)
        val nextFrame = buildFrame(header, bodySize)
        val data = frame + nextFrame

        val frames = mutableListOf<ByteArray>()
        Mp3FrameSync { frames.add(it) }.feed(data)

        assertEquals(2, frames.size)
        assertEquals(expectedSize, frames[0].size)
    }

    @Test
    fun detectedFormatForMpeg1Stereo44100() {
        val header = buildMpeg1Layer3Header(9, 0, false, channelMode = 1)
        val frame = buildFrame(header, 417 - 4)
        val nextFrame = buildFrame(header, 417 - 4)

        val sync = Mp3FrameSync { }
        sync.feed(frame + nextFrame)

        val format = sync.detectedFormat
        assertNotNull(format)
        assertEquals(44100, format!!.sampleRate)
        assertEquals(2, format.channelCount)
        assertEquals(1152, format.samplesPerFrame)
    }

    @Test
    fun detectedFormatForMpeg1Mono48000() {
        // sampleRateIndex=1 → 48000 Hz, channelMode=3 → Mono
        val header = buildMpeg1Layer3Header(9, 1, false, channelMode = 3)
        val expectedSize = 144 * 128 * 1000 / 48000 // = 384
        val frame = buildFrame(header, expectedSize - 4)
        val nextFrame = buildFrame(header, expectedSize - 4)

        val sync = Mp3FrameSync { }
        sync.feed(frame + nextFrame)

        val format = sync.detectedFormat
        assertNotNull(format)
        assertEquals(48000, format!!.sampleRate)
        assertEquals(1, format.channelCount)
        assertEquals(1152, format.samplesPerFrame)
    }

    @Test
    fun detectedFormatForMpeg2Mono22050() {
        // MPEG2 Layer3, 128kbps (index 12), 22050 Hz (index 0), mono
        val header = buildMpeg2Layer3Header(12, 0, false, channelMode = 3)
        val expectedSize = 72 * 128 * 1000 / 22050 // = 417
        val frame = buildFrame(header, expectedSize - 4)
        val nextFrame = buildFrame(header, expectedSize - 4)

        val sync = Mp3FrameSync { }
        sync.feed(frame + nextFrame)

        val format = sync.detectedFormat
        assertNotNull(format)
        assertEquals(22050, format!!.sampleRate)
        assertEquals(1, format.channelCount)
        assertEquals(576, format.samplesPerFrame)
    }

    @Test
    fun detectedFormatIsNullBeforeFirstFrame() {
        val sync = Mp3FrameSync { }
        assertNull(sync.detectedFormat)
    }

    @Test
    fun parseFrameInfoMpeg1Stereo44100() {
        val header = buildMpeg1Layer3Header(9, 0, false, channelMode = 0)
        val frame = buildFrame(header, 417 - 4)

        val info = Mp3FrameSync.parseFrameInfo(frame)
        assertNotNull(info)
        assertEquals(44100, info!!.sampleRate)
        assertEquals(2, info.channelCount)
        assertEquals(1152, info.samplesPerFrame)
    }

    @Test
    fun parseFrameInfoMpeg2Mono22050() {
        val header = buildMpeg2Layer3Header(12, 0, false, channelMode = 3)
        val frame = buildFrame(header, 417 - 4)

        val info = Mp3FrameSync.parseFrameInfo(frame)
        assertNotNull(info)
        assertEquals(22050, info!!.sampleRate)
        assertEquals(1, info.channelCount)
        assertEquals(576, info.samplesPerFrame)
    }

    @Test
    fun parseFrameInfoReturnsNullForGarbage() {
        val garbage = ByteArray(100) { 0x42 }
        assertNull(Mp3FrameSync.parseFrameInfo(garbage))
    }

    @Test
    fun parseFrameInfoReturnsNullForTooShort() {
        assertNull(Mp3FrameSync.parseFrameInfo(byteArrayOf(0xFF.toByte(), 0xFB.toByte())))
    }

    @Test
    fun parseFrameInfoJointStereoIsTwoChannels() {
        // Joint stereo (channelMode=1) should report 2 channels
        val header = buildMpeg1Layer3Header(9, 0, false, channelMode = 1)
        val frame = buildFrame(header, 417 - 4)

        val info = Mp3FrameSync.parseFrameInfo(frame)
        assertNotNull(info)
        assertEquals(2, info!!.channelCount)
    }

    @Test
    fun parseFrameInfoDualChannelIsTwoChannels() {
        // Dual channel (channelMode=2) should report 2 channels
        val header = buildMpeg1Layer3Header(9, 0, false, channelMode = 2)
        val frame = buildFrame(header, 417 - 4)

        val info = Mp3FrameSync.parseFrameInfo(frame)
        assertNotNull(info)
        assertEquals(2, info!!.channelCount)
    }
}
