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.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.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream

@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.StreamSizeWrapper(
                (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.StreamSizeWrapper(
                (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)
        }
    }

    /**
     * @return a list of video streams, in which their video only property mirrors the provided
     * [videoOnly] vararg.
     */
    private fun getVideoStreams(vararg videoOnly: Boolean) =
        StreamItemAdapter.StreamSizeWrapper(
            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
                }
            }
        )

    /**
     * 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.StreamSizeWrapper(streams, context),
                        it
                    )
                }
                put(index, secondaryStreamHelper)
            }
        }
}
