/* 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.support.webextensions

import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.WebExtensionState
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.webextension.ActionHandler
import mozilla.components.concept.engine.webextension.Action
import mozilla.components.concept.engine.webextension.Metadata
import mozilla.components.concept.engine.webextension.TabHandler
import mozilla.components.concept.engine.webextension.WebExtension
import mozilla.components.concept.engine.webextension.WebExtensionDelegate
import mozilla.components.support.base.Component
import mozilla.components.support.base.facts.Action as FactsAction
import mozilla.components.support.base.facts.processor.CollectionProcessor
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import mozilla.components.support.webextensions.facts.WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
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.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify

@RunWith(AndroidJUnit4::class)
class WebExtensionSupportTest {

    private val testDispatcher = TestCoroutineDispatcher()

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
        WebExtensionSupport.installedExtensions.clear()
    }

    @Test
    fun `sets web extension delegate on engine`() {
        val engine: Engine = mock()
        val store = BrowserStore()

        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(any())
    }

    @Test
    fun `queries engine for installed extensions and adds state to the store`() {
        val store = spy(BrowserStore())

        val ext1: WebExtension = mock()
        val ext1Meta: Metadata = mock()
        whenever(ext1Meta.name).thenReturn("ext1")
        val ext2: WebExtension = mock()
        whenever(ext1.id).thenReturn("1")
        whenever(ext1.url).thenReturn("url1")
        whenever(ext1.getMetadata()).thenReturn(ext1Meta)
        whenever(ext1.isEnabled()).thenReturn(true)
        whenever(ext1.isAllowedInPrivateBrowsing()).thenReturn(true)

        whenever(ext2.id).thenReturn("2")
        whenever(ext2.url).thenReturn("url2")
        whenever(ext2.isEnabled()).thenReturn(false)
        whenever(ext2.isAllowedInPrivateBrowsing()).thenReturn(false)

        val engine: Engine = mock()
        val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
        whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
            callbackCaptor.value.invoke(listOf(ext1, ext2))
        }

        CollectionProcessor.withFactCollection { facts ->
            WebExtensionSupport.initialize(engine, store)

            val interactionFact = facts[0]
            assertEquals(FactsAction.INTERACTION, interactionFact.action)
            assertEquals(Component.SUPPORT_WEBEXTENSIONS, interactionFact.component)
            assertEquals(WEB_EXTENSIONS_INITIALIZED, interactionFact.item)
            assertEquals(2, interactionFact.metadata?.size)
            assertTrue(interactionFact.metadata?.containsKey("installed")!!)
            assertTrue(interactionFact.metadata?.containsKey("enabled")!!)
            assertEquals(listOf(ext1.id, ext2.id), interactionFact.metadata?.get("installed"))
            assertEquals(listOf(ext1.id), interactionFact.metadata?.get("enabled"))
        }
        assertEquals(ext1, WebExtensionSupport.installedExtensions[ext1.id])
        assertEquals(ext2, WebExtensionSupport.installedExtensions[ext2.id])

        val actionCaptor = argumentCaptor<WebExtensionAction.InstallWebExtensionAction>()
        verify(store, times(2)).dispatch(actionCaptor.capture())
        assertEquals(
            WebExtensionState(ext1.id, ext1.url, "ext1", true, true),
            actionCaptor.allValues[0].extension
        )
        assertEquals(
            WebExtensionState(ext2.id, ext2.url, null, false, false),
            actionCaptor.allValues[1].extension
        )
    }

    @Test
    fun `reacts to new tab being opened by adding tab to store`() {
        val store = spy(BrowserStore())
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        val engineSession: EngineSession = mock()

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org")
        val actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
        verify(store, times(2)).dispatch(actionCaptor.capture())
        assertEquals(
            "https://mozilla.org",
            (actionCaptor.allValues.first() as TabListAction.AddTabAction).tab.content.url
        )
        assertEquals(
            engineSession,
            (actionCaptor.allValues.last() as EngineAction.LinkEngineSessionAction).engineSession
        )
    }

    @Test
    fun `allows overriding onNewTab behaviour`() {
        val store = BrowserStore()
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        val engineSession: EngineSession = mock()
        var onNewTabCalled = false

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store, onNewTabOverride = { _, _, _ ->
            onNewTabCalled = true
            "123"
        })
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onNewTab(ext, engineSession, true, "https://mozilla.org")
        assertTrue(onNewTabCalled)
    }

    @Test
    fun `reacts to tab being closed by removing tab from store`() {
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")
        whenever(ext.isEnabled()).thenReturn(true)
        val engineSession: EngineSession = mock()
        val tabId = "testTabId"
        val store = spy(
            BrowserStore(
                BrowserState(
                    tabs = listOf(
                        createTab(id = tabId, url = "https://www.mozilla.org")
                    )
                )
            )
        )
        val installedList = mutableListOf(ext)
        val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
        whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
            callbackCaptor.value.invoke(installedList)
        }

        val tabHandlerCaptor = argumentCaptor<TabHandler>()
        WebExtensionSupport.initialize(engine, store)

        store.dispatch(EngineAction.LinkEngineSessionAction(tabId, engineSession)).joinBlocking()
        verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
        tabHandlerCaptor.value.onCloseTab(ext, engineSession)
        verify(store).dispatch(TabListAction.RemoveTabAction(tabId))
        store.waitUntilIdle()
        tabHandlerCaptor.value.onCloseTab(ext, engineSession)
        verify(store, times(1)).dispatch(TabListAction.RemoveTabAction(tabId))
    }

    @Test
    fun `allows overriding onCloseTab behaviour`() {
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")
        whenever(ext.isEnabled()).thenReturn(true)
        val engineSession: EngineSession = mock()
        var onCloseTabCalled = false
        val tabId = "testTabId"
        val store = spy(
            BrowserStore(
                BrowserState(
                    tabs = listOf(
                        createTab(id = tabId, url = "https://www.mozilla.org")
                    )
                )
            )
        )

        val installedList = mutableListOf(ext)
        val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
        whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
            callbackCaptor.value.invoke(installedList)
        }

        val tabHandlerCaptor = argumentCaptor<TabHandler>()
        WebExtensionSupport.initialize(
            engine,
            store,
            onSelectTabOverride = { _, _ -> },
            onCloseTabOverride = { _, _ -> onCloseTabCalled = true })

        store.dispatch(EngineAction.LinkEngineSessionAction(tabId, engineSession)).joinBlocking()
        verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
        tabHandlerCaptor.value.onCloseTab(ext, engineSession)
        assertTrue(onCloseTabCalled)
    }

    @Test
    fun `reacts to tab being updated`() {
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")
        whenever(ext.isEnabled()).thenReturn(true)
        val engineSession: EngineSession = mock()
        val tabId = "testTabId"
        val store = spy(
            BrowserStore(
                BrowserState(
                    tabs = listOf(
                        createTab(id = tabId, url = "https://www.mozilla.org")
                    )
                )
            )
        )
        val installedList = mutableListOf(ext)
        val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
        whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
            callbackCaptor.value.invoke(installedList)
        }

        val tabHandlerCaptor = argumentCaptor<TabHandler>()
        WebExtensionSupport.initialize(engine, store)
        store.dispatch(EngineAction.LinkEngineSessionAction(tabId, engineSession)).joinBlocking()

        // Update tab to select it
        verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
        assertNull(store.state.selectedTabId)
        assertTrue(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, null))
        store.waitUntilIdle()
        assertEquals("testTabId", store.state.selectedTabId)

        // Update URL of tab
        assertTrue(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, false, "url"))
        verify(engineSession).loadUrl("url")

        // Update non-existing tab
        store.dispatch(TabListAction.RemoveTabAction(tabId)).joinBlocking()
        assertFalse(tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, "url"))
    }

    @Test
    fun `reacts to new extension being installed`() {
        val store = spy(
            BrowserStore(
                BrowserState(
                    tabs = listOf(
                        createTab(id = "1", url = "https://www.mozilla.org")
                    )
                )
            )
        )
        val engineSession: EngineSession = mock()
        store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession)).joinBlocking()

        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("extensionId")
        whenever(ext.url).thenReturn("url")
        whenever(ext.supportActions).thenReturn(true)

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        // Verify that we dispatch to the store and mark the extension as installed
        delegateCaptor.value.onInstalled(ext)
        verify(store).dispatch(
            WebExtensionAction.InstallWebExtensionAction(
                WebExtensionState(ext.id, ext.url, ext.getMetadata()?.name, ext.isEnabled())
            )
        )
        assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])

        // Verify that we register action and tab handlers for all existing sessions on the extension
        val actionHandlerCaptor = argumentCaptor<ActionHandler>()
        val webExtensionActionCaptor = argumentCaptor<WebExtensionAction>()
        val tabHandlerCaptor = argumentCaptor<TabHandler>()
        val selectTabActionCaptor = argumentCaptor<TabListAction.SelectTabAction>()
        verify(ext).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture())
        verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
        whenever(ext.hasActionHandler(engineSession)).thenReturn(true)
        whenever(ext.hasTabHandler(engineSession)).thenReturn(true)

        // Verify we only register the handlers once
        store.dispatch(ContentAction.UpdateUrlAction(sessionId = "1", url = "https://www.firefox.com")).joinBlocking()
        verify(ext, times(1)).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture())
        verify(ext, times(1)).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())

        actionHandlerCaptor.value.onBrowserAction(ext, engineSession, mock())
        verify(store, times(4)).dispatch(webExtensionActionCaptor.capture())
        assertEquals(ext.id, (webExtensionActionCaptor.allValues.last() as WebExtensionAction.UpdateTabBrowserAction).extensionId)

        reset(store)

        tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, null)
        verify(store).dispatch(selectTabActionCaptor.capture())
        assertEquals("1", selectTabActionCaptor.value.tabId)
        tabHandlerCaptor.value.onUpdateTab(ext, engineSession, true, "url")
        verify(engineSession).loadUrl("url")
    }

    @Test
    fun `reacts to install permission request`() {
        val store = spy(BrowserStore())
        val engine: Engine = mock()
        val ext: WebExtension = mock()

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        // Verify they we confirm the permission request
        assertTrue(delegateCaptor.value.onInstallPermissionRequest(ext))
    }

    @Test
    fun `reacts to extension being uninstalled`() {
        val store = spy(BrowserStore())

        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("extensionId")
        whenever(ext.url).thenReturn("url")
        whenever(ext.supportActions).thenReturn(true)

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onInstalled(ext)
        verify(store).dispatch(
            WebExtensionAction.InstallWebExtensionAction(
                WebExtensionState(ext.id, ext.url, ext.getMetadata()?.name, ext.isEnabled())
            )
        )
        assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])

        // Verify that we dispatch to the store and mark the extension as uninstalled
        delegateCaptor.value.onUninstalled(ext)
        verify(store).dispatch(WebExtensionAction.UninstallWebExtensionAction(ext.id))
        assertNull(WebExtensionSupport.installedExtensions[ext.id])
    }

    @Test
    fun `reacts to extension being enabled`() {
        val store = spy(BrowserStore())

        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("extensionId")
        whenever(ext.url).thenReturn("url")
        whenever(ext.supportActions).thenReturn(true)

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onEnabled(ext)
        verify(store).dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(ext.id, true))
        assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
    }

    @Test
    fun `reacts to extension being disabled`() {
        val store = spy(BrowserStore())

        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("extensionId")
        whenever(ext.url).thenReturn("url")
        whenever(ext.supportActions).thenReturn(true)

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onDisabled(ext)
        verify(store).dispatch(WebExtensionAction.UpdateWebExtensionEnabledAction(ext.id, false))
        assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
    }

    @Test
    fun `observes store and registers handlers on new engine sessions`() {
        val store = spy(
            BrowserStore(
                BrowserState(
                    tabs = listOf(
                        createTab(id = "1", url = "https://www.mozilla.org")
                    )
                )
            )
        )

        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("extensionId")
        whenever(ext.url).thenReturn("url")
        whenever(ext.supportActions).thenReturn(true)

        // Install extension
        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
        delegateCaptor.value.onInstalled(ext)

        // Verify that action/tab handler is registered when a new engine session is created
        val engineSession: EngineSession = mock()
        val actionHandlerCaptor = argumentCaptor<ActionHandler>()
        val tabHandlerCaptor = argumentCaptor<TabHandler>()
        verify(ext, never()).registerActionHandler(any(), any())
        verify(ext, never()).registerTabHandler(any(), any())
        store.dispatch(EngineAction.LinkEngineSessionAction("1", engineSession)).joinBlocking()
        verify(ext).registerActionHandler(eq(engineSession), actionHandlerCaptor.capture())
        verify(ext).registerTabHandler(eq(engineSession), tabHandlerCaptor.capture())
    }

    @Test
    fun `reacts to browser action being defined by dispatching to the store`() {
        val store = spy(BrowserStore())
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        val browserAction: Action = mock()
        whenever(ext.id).thenReturn("test")

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onBrowserActionDefined(ext, browserAction)
        val actionCaptor = argumentCaptor<WebExtensionAction.UpdateBrowserAction>()
        verify(store).dispatch(actionCaptor.capture())
        assertEquals("test", actionCaptor.value.extensionId)
        assertEquals(browserAction, actionCaptor.value.browserAction)
    }

    @Test
    fun `reacts to page action being defined by dispatching to the store`() {
        val store = spy(BrowserStore())
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        val pageAction: Action = mock()
        whenever(ext.id).thenReturn("test")

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onPageActionDefined(ext, pageAction)
        val actionCaptor = argumentCaptor<WebExtensionAction.UpdatePageAction>()
        verify(store).dispatch(actionCaptor.capture())
        assertEquals("test", actionCaptor.value.extensionId)
        assertEquals(pageAction, actionCaptor.value.pageAction)
    }

    @Test
    fun `reacts to action popup being toggled by opening and closing tab if configured`() {
        val engine: Engine = mock()

        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")

        val engineSession: EngineSession = mock()
        val browserAction: Action = mock()
        val store = spy(
            BrowserStore(
                BrowserState(
                    extensions = mapOf(ext.id to WebExtensionState(ext.id))
                )
            )
        )

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store, openPopupInTab = true)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        // Toggling should open tab
        delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
        var actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
        verify(store, times(3)).dispatch(actionCaptor.capture())
        var values = actionCaptor.allValues
        assertEquals("", (values[0] as TabListAction.AddTabAction).tab.content.url)
        assertEquals(
            engineSession,
            (values[1] as EngineAction.LinkEngineSessionAction).engineSession
        )
        assertEquals("test", (values[2] as WebExtensionAction.UpdatePopupSessionAction).extensionId)

        // Update store
        store.dispatch(
            WebExtensionAction.UpdatePopupSessionAction(
                "test",
                (values[2] as WebExtensionAction.UpdatePopupSessionAction).popupSessionId
            )
        ).joinBlocking()

        val popupSessionId = store.state.extensions["test"]?.popupSessionId
        assertNotNull(popupSessionId)

        // Select different tab
        store.dispatch(TabListAction.SelectTabAction("otherTab")).joinBlocking()

        // Toggling again should select popup tab
        delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
        actionCaptor = argumentCaptor()
        verify(store, times(6)).dispatch(actionCaptor.capture())
        assertEquals(popupSessionId, (actionCaptor.value as TabListAction.SelectTabAction).tabId)

        store.waitUntilIdle()

        // Toggling again should close tab
        delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
        actionCaptor = argumentCaptor()
        verify(store, times(7)).dispatch(actionCaptor.capture())
        assertEquals(popupSessionId, (actionCaptor.value as TabListAction.RemoveTabAction).tabId)
    }

    @Test
    fun `reacts to action popup being toggled by opening a popup`() {
        val engine: Engine = mock()

        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")

        val engineSession: EngineSession = mock()
        val browserAction: Action = mock()
        val store = spy(
            BrowserStore(
                BrowserState(
                    extensions = mapOf(ext.id to WebExtensionState(ext.id))
                )
            )
        )

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()

        WebExtensionSupport.initialize(engine, store)
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        // Toggling should allow state to have popup EngineSession instance
        delegateCaptor.value.onToggleActionPopup(ext, engineSession, browserAction)
        val actionCaptor = argumentCaptor<mozilla.components.browser.state.action.BrowserAction>()
        verify(store, times(1)).dispatch(actionCaptor.capture())

        val value = actionCaptor.value
        assertNotNull((value as WebExtensionAction.UpdatePopupSessionAction).popupSession)
    }

    @Test
    fun `invokes onUpdatePermissionRequest callback`() {
        var executed = false
        val engine: Engine = mock()
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")
        val store = spy(
            BrowserStore(
                BrowserState(
                    extensions = mapOf(ext.id to WebExtensionState(ext.id))
                )
            )
        )

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(
            runtime = engine,
            store = store,
            onUpdatePermissionRequest = { _, _, _, _ ->
                executed = true
            })

        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())
        delegateCaptor.value.onUpdatePermissionRequest(mock(), mock(), mock(), mock())
        assertTrue(executed)
    }

    @Test
    fun `invokes onExtensionsLoaded callback`() {
        var executed = false
        val engine: Engine = mock()

        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")
        whenever(ext.isBuiltIn()).thenReturn(false)

        val builtInExt: WebExtension = mock()
        whenever(builtInExt.id).thenReturn("test2")
        whenever(builtInExt.isBuiltIn()).thenReturn(true)

        val store = spy(BrowserStore(BrowserState(extensions = mapOf(ext.id to WebExtensionState(ext.id)))))

        val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
        whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
            callbackCaptor.value.invoke(listOf(ext, builtInExt))
        }

        val onExtensionsLoaded: ((List<WebExtension>) -> Unit) = {
            assertEquals(1, it.size)
            assertEquals(ext, it[0])
            executed = true
        }
        WebExtensionSupport.initialize(runtime = engine, store = store, onExtensionsLoaded = onExtensionsLoaded)
        assertTrue(executed)
    }

    @Test
    fun `reacts to extension list being updated in the engine`() {
        val store = spy(BrowserStore())
        val ext: WebExtension = mock()
        whenever(ext.id).thenReturn("test")
        whenever(ext.isEnabled()).thenReturn(true)
        val installedList = mutableListOf(ext)

        val engine: Engine = mock()
        val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
        whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
            callbackCaptor.value.invoke(installedList)
        }

        val delegateCaptor = argumentCaptor<WebExtensionDelegate>()
        WebExtensionSupport.initialize(engine, store)
        assertEquals(1, WebExtensionSupport.installedExtensions.size)
        assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])
        verify(engine).registerWebExtensionDelegate(delegateCaptor.capture())

        delegateCaptor.value.onExtensionListUpdated()
        store.waitUntilIdle()

        val actionCaptor = argumentCaptor<WebExtensionAction>()
        verify(store, times(3)).dispatch(actionCaptor.capture())
        assertEquals(3, actionCaptor.allValues.size)
        // Initial install
        assertTrue(actionCaptor.allValues[0] is WebExtensionAction.InstallWebExtensionAction)
        assertEquals(WebExtensionState(ext.id), (actionCaptor.allValues[0] as WebExtensionAction.InstallWebExtensionAction).extension)

        // Uninstall all
        assertTrue(actionCaptor.allValues[1] is WebExtensionAction.UninstallAllWebExtensionsAction)

        // Reinstall
        assertTrue(actionCaptor.allValues[2] is WebExtensionAction.InstallWebExtensionAction)
        assertEquals(WebExtensionState(ext.id), (actionCaptor.allValues[2] as WebExtensionAction.InstallWebExtensionAction).extension)
        assertEquals(ext, WebExtensionSupport.installedExtensions[ext.id])

        // Verify installed extensions are cleared
        installedList.clear()
        delegateCaptor.value.onExtensionListUpdated()
        store.waitUntilIdle()
        assertTrue(WebExtensionSupport.installedExtensions.isEmpty())
    }

    @Test
    fun `closes unsupported extension`() {
        val store = spy(BrowserStore(BrowserState(
            tabs = listOf(
                createTab(id = "1", url = "https://www.mozilla.org", source = SessionState.Source.RESTORED),
                createTab(id = "2", url = "moz-extension://1234-5678/test", source = SessionState.Source.RESTORED),
                createTab(id = "3", url = "moz-extension://1234-5678-9/", source = SessionState.Source.RESTORED)
            )
        )))

        val ext1: WebExtension = mock()
        val ext1Meta: Metadata = mock()
        whenever(ext1Meta.baseUrl).thenReturn("moz-extension://1234-5678/")
        whenever(ext1.id).thenReturn("1")
        whenever(ext1.url).thenReturn("url1")
        whenever(ext1.getMetadata()).thenReturn(ext1Meta)
        whenever(ext1.isEnabled()).thenReturn(true)
        whenever(ext1.isAllowedInPrivateBrowsing()).thenReturn(true)

        val ext2: WebExtension = mock()
        whenever(ext2.id).thenReturn("2")
        whenever(ext2.url).thenReturn("url2")
        whenever(ext2.isEnabled()).thenReturn(true)
        whenever(ext2.isAllowedInPrivateBrowsing()).thenReturn(false)

        val engine: Engine = mock()
        val callbackCaptor = argumentCaptor<((List<WebExtension>) -> Unit)>()
        whenever(engine.listInstalledWebExtensions(callbackCaptor.capture(), any())).thenAnswer {
            callbackCaptor.value.invoke(listOf(ext1, ext2))
        }

        WebExtensionSupport.initialize(engine, store)

        store.waitUntilIdle()
        assertNotNull(store.state.findTab("1"))
        assertNotNull(store.state.findTab("2"))
        assertNull(store.state.findTab("3"))

        // Make sure we're running a single cleanup and stop the scope after
        store.dispatch(TabListAction.AddTabAction(createTab(id = "4", url = "moz-extension://1234-5678-90/")))
                .joinBlocking()

        store.waitUntilIdle()
        assertNotNull(store.state.findTab("4"))
    }
}
