package org.schabi.newpipe.util

import android.content.Context
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.widget.Spinner
import androidx.collection.SparseArrayCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper

@MediumTest
@RunWith(AndroidJUnit4::class)
class StreamItemAdapterTest {
    private lateinit var context: Context
    private lateinit var spinner: Spinner

    @Before
    fun setUp() {
        context = ApplicationProvider.getApplicationContext()
        UiThreadStatement.runOnUiThread {
            spinner = Spinner(context)
        }
    }

    @Test
    fun videoStreams_noSecondaryStream() {
        val adapter = StreamItemAdapter<VideoStream, AudioStream>(
            getVideoStreams(true, true, true, true)
        )

        spinner.adapter = adapter
        assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
        assertIconVisibility(spinner, 1, VISIBLE, VISIBLE)
        assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
        assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
    }

    @Test
    fun videoStreams_hasSecondaryStream() {
        val adapter = StreamItemAdapter(
            getVideoStreams(false, true, false, true),
            getAudioStreams(false, true, false, true)
        )

        spinner.adapter = adapter
        assertIconVisibility(spinner, 0, GONE, GONE)
        assertIconVisibility(spinner, 1, GONE, GONE)
        assertIconVisibility(spinner, 2, GONE, GONE)
        assertIconVisibility(spinner, 3, GONE, GONE)
    }

    @Test
    fun videoStreams_Mixed() {
        val adapter = StreamItemAdapter(
            getVideoStreams(true, true, true, true, true, false, true, true),
            getAudioStreams(false, true, false, false, false, true, true, true)
        )

        spinner.adapter = adapter
        assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
        assertIconVisibility(spinner, 1, GONE, INVISIBLE)
        assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
        assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
        assertIconVisibility(spinner, 4, VISIBLE, VISIBLE)
        assertIconVisibility(spinner, 5, GONE, INVISIBLE)
        assertIconVisibility(spinner, 6, GONE, INVISIBLE)
        assertIconVisibility(spinner, 7, GONE, INVISIBLE)
    }

    @Test
    fun subtitleStreams_noIcon() {
        val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
            StreamItemAdapter.StreamInfoWrapper(
                (0 until 5).map {
                    SubtitlesStream.Builder()
                        .setContent("https://example.com", true)
                        .setMediaFormat(MediaFormat.SRT)
                        .setLanguageCode("pt-BR")
                        .setAutoGenerated(false)
                        .build()
                },
                context
            )
        )
        spinner.adapter = adapter
        for (i in 0 until spinner.count) {
            assertIconVisibility(spinner, i, GONE, GONE)
        }
    }

    @Test
    fun audioStreams_noIcon() {
        val adapter = StreamItemAdapter<AudioStream, Stream>(
            StreamItemAdapter.StreamInfoWrapper(
                (0 until 5).map {
                    AudioStream.Builder()
                        .setId(Stream.ID_UNKNOWN)
                        .setContent("https://example.com/$it", true)
                        .setMediaFormat(MediaFormat.OPUS)
                        .setAverageBitrate(192)
                        .build()
                },
                context
            )
        )
        spinner.adapter = adapter
        for (i in 0 until spinner.count) {
            assertIconVisibility(spinner, i, GONE, GONE)
        }
    }

    @Test
    fun retrieveMediaFormatFromFileTypeHeaders() {
        val streams = getIncompleteAudioStreams(5)
        val wrapper = StreamInfoWrapper(streams, context)
        val retrieveMediaFormat = { stream: AudioStream, response: Response ->
            StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response)
        }
        val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)

        helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
        helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1)

        helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF)
        helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3)
    }

    @Test
    fun retrieveMediaFormatFromContentDispositionHeader() {
        val streams = getIncompleteAudioStreams(11)
        val wrapper = StreamInfoWrapper(streams, context)
        val retrieveMediaFormat = { stream: AudioStream, response: Response ->
            StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response)
        }
        val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)

        helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
        helper.assertInvalidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
        )
        helper.assertInvalidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
        )
        helper.assertInvalidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
        )
        helper.assertInvalidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
        )

        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
            5, MediaFormat.OGG
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
            6, MediaFormat.FLAC
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
            7, MediaFormat.AIFF
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
            8, MediaFormat.M4A
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
            9, MediaFormat.OPUS
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
            10, MediaFormat.OPUS
        )
    }

    @Test
    fun retrieveMediaFormatFromContentTypeHeader() {
        val streams = getIncompleteAudioStreams(12)
        val wrapper = StreamInfoWrapper(streams, context)
        val retrieveMediaFormat = { stream: AudioStream, response: Response ->
            StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response)
        }
        val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)

        helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0)
        helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1)
        helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2)
        helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3)
        helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4)
        helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5)
        helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
        helper.assertInvalidResponse(getResponse(mapOf()), 7)

        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
        )
        helper.assertValidResponse(
            getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
        )
    }

    /**
     * @return a list of video streams, in which their video only property mirrors the provided
     * [videoOnly] vararg.
     */
    private fun getVideoStreams(vararg videoOnly: Boolean) =
        StreamItemAdapter.StreamInfoWrapper(
            videoOnly.map {
                VideoStream.Builder()
                    .setId(Stream.ID_UNKNOWN)
                    .setContent("https://example.com", true)
                    .setMediaFormat(MediaFormat.MPEG_4)
                    .setResolution("720p")
                    .setIsVideoOnly(it)
                    .build()
            },
            context
        )

    /**
     * @return a list of audio streams, containing valid and null elements mirroring the provided
     * [shouldBeValid] vararg.
     */
    private fun getAudioStreams(vararg shouldBeValid: Boolean) =
        getSecondaryStreamsFromList(
            shouldBeValid.map {
                if (it) {
                    AudioStream.Builder()
                        .setId(Stream.ID_UNKNOWN)
                        .setContent("https://example.com", true)
                        .setMediaFormat(MediaFormat.OPUS)
                        .setAverageBitrate(192)
                        .build()
                } else {
                    null
                }
            }
        )

    private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
        val list = ArrayList<AudioStream>(size)
        for (i in 1..size) {
            list.add(
                AudioStream.Builder()
                    .setId(Stream.ID_UNKNOWN)
                    .setContent("https://example.com/$i", true)
                    .build()
            )
        }
        return list
    }

    /**
     * Checks whether the item at [position] in the [spinner] has the correct icon visibility when
     * it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
     */
    private fun assertIconVisibility(
        spinner: Spinner,
        position: Int,
        normalVisibility: Int,
        dropDownVisibility: Int
    ) {
        spinner.setSelection(position)
        spinner.adapter.getView(position, null, spinner).run {
            Assert.assertEquals(
                "normal visibility (pos=[$position]) is not correct",
                findViewById<View>(R.id.wo_sound_icon).visibility,
                normalVisibility,
            )
        }
        spinner.adapter.getDropDownView(position, null, spinner).run {
            Assert.assertEquals(
                "drop down visibility (pos=[$position]) is not correct",
                findViewById<View>(R.id.wo_sound_icon).visibility,
                dropDownVisibility
            )
        }
    }

    /**
     * Helper function that builds a secondary stream list.
     */
    private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) =
        SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
            streams.forEachIndexed { index, stream ->
                val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
                    SecondaryStreamHelper(
                        StreamItemAdapter.StreamInfoWrapper(streams, context),
                        it
                    )
                }
                put(index, secondaryStreamHelper)
            }
        }

    private fun getResponse(headers: Map<String, String>): Response {
        val listHeaders = HashMap<String, List<String>>()
        headers.forEach { entry ->
            listHeaders[entry.key] = listOf(entry.value)
        }
        return Response(200, null, listHeaders, "", "")
    }

    /**
     * Helper class for assertion related to extractions of [MediaFormat]s.
     */
    class AssertionHelper<T : Stream>(
        private val streams: List<T>,
        private val wrapper: StreamInfoWrapper<T>,
        private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean
    ) {

        /**
         * Assert that an invalid response does not result in wrongly extracted [MediaFormat].
         */
        fun assertInvalidResponse(
            response: Response,
            index: Int
        ) {
            assertFalse(
                "invalid header returns valid value", retrieveMediaFormat(streams[index], response)
            )
            assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
        }

        /**
         * Assert that a valid response results in correctly extracted and handled [MediaFormat].
         */
        fun assertValidResponse(
            response: Response,
            index: Int,
            format: MediaFormat
        ) {
            assertTrue(
                "header was not recognized", retrieveMediaFormat(streams[index], response)
            )
            assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
        }
    }
}
