/* 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.browser.state.engine.middleware

import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.selector.findTab
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.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`

class CreateEngineSessionMiddlewareTest {

    @Test
    fun `creates engine session if needed`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val tab = createTab("https://www.mozilla.org", id = "1")
        val store = BrowserStore(
            initialState = BrowserState(tabs = listOf(tab)),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)

        store.dispatch(EngineAction.CreateEngineSessionAction(tab.id))
        testScheduler.advanceUntilIdle()
        verify(engine, times(1)).createSession(false)
        assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)

        store.dispatch(EngineAction.CreateEngineSessionAction(tab.id))
        testScheduler.advanceUntilIdle()
        verify(engine, times(1)).createSession(false)
        assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
    }

    @Test
    fun `restores engine session state if available`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)
        val engineSessionState: EngineSessionState = mock()

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val tab = createTab("https://www.mozilla.org", id = "1")
        val store = BrowserStore(
            initialState = BrowserState(tabs = listOf(tab)),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)

        store.dispatch(EngineAction.UpdateEngineSessionStateAction(tab.id, engineSessionState))
        store.dispatch(EngineAction.CreateEngineSessionAction(tab.id))
        testScheduler.advanceUntilIdle()

        verify(engineSession).restoreState(engineSessionState)
        Unit
    }

    @Test
    fun `creates no engine session if tab does not exist`() = runTest {
        val engine: Engine = mock()
        `when`(engine.createSession(anyBoolean(), anyString())).thenReturn(mock())

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val store = BrowserStore(
            initialState = BrowserState(tabs = listOf()),
            middleware = listOf(middleware),
        )

        store.dispatch(EngineAction.CreateEngineSessionAction("invalid"))
        testScheduler.advanceUntilIdle()

        verify(engine, never()).createSession(anyBoolean(), any())
        Unit
    }

    @Test
    fun `creates no engine session if session does not exist`() = runTest {
        val engine: Engine = mock()
        `when`(engine.createSession(anyBoolean(), anyString())).thenReturn(mock())

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val tab = createTab("https://www.mozilla.org", id = "1")

        val store = BrowserStore(
            initialState = BrowserState(tabs = listOf(tab)),
            middleware = listOf(middleware),
        )

        store.dispatch(
            EngineAction.CreateEngineSessionAction("non-existent"),
        )

        testScheduler.advanceUntilIdle()

        verify(engine, never()).createSession(anyBoolean(), any())
        Unit
    }

    @Test
    fun `dispatches follow-up action after engine session is created`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val tab = createTab("https://www.mozilla.org", id = "1")
        val store = BrowserStore(
            initialState = BrowserState(tabs = listOf(tab)),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)

        val followupAction = ContentAction.UpdateTitleAction(tab.id, "test")
        store.dispatch(EngineAction.CreateEngineSessionAction(tab.id, followupAction = followupAction))

        testScheduler.advanceUntilIdle()

        verify(engine, times(1)).createSession(false)
        assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
        assertEquals(followupAction.title, store.state.findTab(tab.id)?.content?.title)
    }

    @Test
    fun `dispatches follow-up action once engine session is created by pending action`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val tab = createTab("https://www.mozilla.org", id = "1")
        val store = BrowserStore(
            initialState = BrowserState(tabs = listOf(tab)),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findTab(tab.id)?.engineState?.engineSession)

        val followupAction = ContentAction.UpdateTitleAction(tab.id, "test")

        // Simulate two concurrent CreateEngineSessionActions
        store.dispatch(EngineAction.CreateEngineSessionAction(tab.id))
        store.dispatch(EngineAction.CreateEngineSessionAction(tab.id, followupAction = followupAction))

        testScheduler.advanceUntilIdle()

        verify(engine, times(1)).createSession(false)
        assertEquals(engineSession, store.state.findTab(tab.id)?.engineState?.engineSession)
        assertEquals(followupAction.title, store.state.findTab(tab.id)?.content?.title)
    }

    @Test
    fun `creating engine session for custom tab`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val customTab = createCustomTab("https://www.mozilla.org", id = "1")
        val store = BrowserStore(
            initialState = BrowserState(customTabs = listOf(customTab)),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findCustomTab(customTab.id)?.engineState?.engineSession)

        val followupAction = ContentAction.UpdateTitleAction(customTab.id, "test")
        store.dispatch(EngineAction.CreateEngineSessionAction(customTab.id, followupAction = followupAction))

        testScheduler.advanceUntilIdle()

        verify(engine, times(1)).createSession(false)
        assertEquals(engineSession, store.state.findCustomTab(customTab.id)?.engineState?.engineSession)
        assertEquals(followupAction.title, store.state.findCustomTab(customTab.id)?.content?.title)
    }

    @Test
    fun `creating engine session for custom tab with desktopMode enabled`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val customTab = createCustomTab("https://www.mozilla.org", id = "1", desktopMode = true)
        val store = BrowserStore(
            initialState = BrowserState(customTabs = listOf(customTab)),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findCustomTab(customTab.id)?.engineState?.engineSession)

        val followupAction = ContentAction.UpdateTitleAction(customTab.id, "test")
        store.dispatch(EngineAction.CreateEngineSessionAction(customTab.id, followupAction = followupAction))

        testScheduler.advanceUntilIdle()

        verify(engine, times(1)).createSession(false)
        assertEquals(engineSession, store.state.findCustomTab(customTab.id)?.engineState?.engineSession)
        assertEquals(followupAction.title, store.state.findCustomTab(customTab.id)?.content?.title)
        verify(engineSession).toggleDesktopMode(eq(true), eq(false))
    }

    @Test
    fun `set desktop mode on based on tab's property once engine session is created`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val tabs = listOf(
            createTab("https://www.mozilla.org", id = "1", desktopMode = true),
            createTab("https://www.mozilla.com", id = "2", desktopMode = false),
        )
        val store = BrowserStore(
            initialState = BrowserState(tabs = tabs),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findTab(tabs[0].id)?.engineState?.engineSession)

        store.dispatch(EngineAction.CreateEngineSessionAction(tabs[0].id))
        testScheduler.advanceUntilIdle()
        verify(engineSession).toggleDesktopMode(eq(true), eq(false))
    }

    @Test
    fun `set desktop mode off based on tab's property once engine session is created`() = runTest {
        val engine: Engine = mock()
        val engineSession: EngineSession = mock()
        whenever(engine.createSession(anyBoolean(), any())).thenReturn(engineSession)

        val middleware = CreateEngineSessionMiddleware(engine, this)
        val tabs = listOf(
            createTab("https://www.mozilla.org", id = "1", desktopMode = true),
            createTab("https://www.mozilla.com", id = "2", desktopMode = false),
        )
        val store = BrowserStore(
            initialState = BrowserState(tabs = tabs),
            middleware = listOf(middleware),
        )
        assertNull(store.state.findTab(tabs[0].id)?.engineState?.engineSession)

        store.dispatch(EngineAction.CreateEngineSessionAction(tabs[1].id))
        testScheduler.advanceUntilIdle()
        verify(engineSession).toggleDesktopMode(eq(false), eq(false))
    }
}
