/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.session

import android.view.View
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createCustomTab
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.support.test.any
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify

class SessionFeatureTest {
    private val testDispatcher = TestCoroutineDispatcher()
    private val scope = TestCoroutineScope(testDispatcher)

    @Before
    @ExperimentalCoroutinesApi
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
    }

    @After
    @ExperimentalCoroutinesApi
    fun tearDown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }

    @Test
    fun `start renders selected session`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking()

        val feature = SessionFeature(store, mock(), view)
        verify(view, never()).render(any())

        feature.start()
        testDispatcher.advanceUntilIdle()

        store.waitUntilIdle()
        verify(view).render(engineSession)
    }

    @Test
    fun `start renders fixed session`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("C", engineSession)).joinBlocking()

        val feature = SessionFeature(store, mock(), view, tabId = "C")
        verify(view, never()).render(any())

        feature.start()

        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()

        verify(view).render(engineSession)
    }

    @Test
    fun `start renders custom tab session`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking()

        val feature = SessionFeature(store, mock(), view, tabId = "D")
        verify(view, never()).render(any())
        feature.start()

        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()

        verify(view).render(engineSession)
    }

    @Test
    fun `renders selected tab after changes`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSessionA: EngineSession = mock()
        val engineSessionB: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("A", engineSessionA)).joinBlocking()
        store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSessionB)).joinBlocking()

        val feature = SessionFeature(store, mock(), view)
        verify(view, never()).render(any())

        feature.start()
        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()
        verify(view).render(engineSessionB)

        store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()
        verify(view).render(engineSessionA)
    }

    @Test
    fun `creates engine session if needed`() {
        val store = spy(prepareStore())
        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val feature = SessionFeature(store, mock(), view)
        verify(view, never()).render(any())

        feature.start()
        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()
        verify(store).dispatch(EngineAction.CreateEngineSessionAction("B"))
    }

    @Test
    fun `does not render new selected session after stop`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSessionA: EngineSession = mock()
        val engineSessionB: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("A", engineSessionA)).joinBlocking()
        store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSessionB)).joinBlocking()

        val feature = SessionFeature(store, mock(), view)
        verify(view, never()).render(any())

        feature.start()
        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()
        verify(view).render(engineSessionB)

        feature.stop()

        store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()
        verify(view, never()).render(engineSessionA)
    }

    @Test
    fun `releases when last selected session gets removed`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking()
        val feature = SessionFeature(store, mock(), view)

        feature.start()

        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()

        verify(view).render(engineSession)
        verify(view, never()).release()

        store.dispatch(TabListAction.RemoveAllTabsAction).joinBlocking()
        verify(view).release()
    }

    @Test
    fun `release stops observing and releases session from view`() {
        val store = prepareStore()
        val actualView: View = mock()

        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("B", engineSession)).joinBlocking()

        val feature = SessionFeature(store, mock(), view)
        verify(view, never()).render(any())

        feature.start()

        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()

        verify(view).render(engineSession)

        val newEngineSession: EngineSession = mock()
        feature.release()
        verify(view).release()

        store.dispatch(TabListAction.SelectTabAction("A")).joinBlocking()
        verify(view, never()).render(newEngineSession)
    }

    @Test
    fun `releases when custom tab gets removed`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking()

        val feature = SessionFeature(store, mock(), view, tabId = "D")
        verify(view, never()).render(any())

        feature.start()

        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()

        verify(view).render(engineSession)
        verify(view, never()).release()

        store.dispatch(CustomTabListAction.RemoveCustomTabAction("D")).joinBlocking()
        verify(view).release()
    }

    @Test
    fun `onBackPressed clears selection if it exists`() {
        run {
            val view: EngineView = mock()
            doReturn(false).`when`(view).canClearSelection()

            val feature = SessionFeature(BrowserStore(), mock(), view)
            assertFalse(feature.onBackPressed())

            verify(view, never()).clearSelection()
        }

        run {
            val view: EngineView = mock()
            doReturn(true).`when`(view).canClearSelection()

            val feature = SessionFeature(BrowserStore(), mock(), view)
            assertTrue(feature.onBackPressed())

            verify(view).clearSelection()
        }
    }

    @Test
    fun `onBackPressed() invokes GoBackUseCase if back navigation is possible`() {
        run {
            val store = BrowserStore(BrowserState(
                tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
                selectedTabId = "A"
            ))

            val useCase: SessionUseCases.GoBackUseCase = mock()

            val feature = SessionFeature(store, useCase, mock())

            assertFalse(feature.onBackPressed())
            verify(useCase, never()).invoke("A")
        }

        run {
            val store = BrowserStore(BrowserState(
                tabs = listOf(createTab("https://www.mozilla.org", id = "A")),
                selectedTabId = "A"
            ))

            store.dispatch(ContentAction.UpdateBackNavigationStateAction(
                "A",
                canGoBack = true
            )).joinBlocking()

            val useCase: SessionUseCases.GoBackUseCase = mock()

            val feature = SessionFeature(store, useCase, mock())

            assertTrue(feature.onBackPressed())
            verify(useCase).invoke("A")
        }
    }

    @Test
    fun `stop releases engine view`() {
        val store = prepareStore()

        val actualView: View = mock()
        val view: EngineView = mock()
        doReturn(actualView).`when`(view).asView()

        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("D", engineSession)).joinBlocking()

        val feature = SessionFeature(store, mock(), view, tabId = "D")
        verify(view, never()).render(any())
        feature.start()

        testDispatcher.advanceUntilIdle()
        store.waitUntilIdle()

        verify(view).render(engineSession)

        feature.stop()
        verify(view).release()
    }

    private fun prepareStore(): BrowserStore = BrowserStore(
        BrowserState(
            tabs = listOf(
                createTab("https://www.mozilla.org", id = "A"),
                createTab("https://getpocket.com", id = "B"),
                createTab("https://www.firefox.com", id = "C")
            ),
            customTabs = listOf(
                createCustomTab("https://hubs.mozilla.com/", id = "D")
            ),
            selectedTabId = "B"
        ),
        middleware = EngineMiddleware.create(
            engine = mock(),
            sessionLookup = { null },
            scope = scope
        )
    )
}
