/* 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.session

import android.graphics.Bitmap
import mozilla.components.browser.session.tab.CustomTabConfig
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.support.test.mock
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.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.mock
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
import org.mockito.Mockito.reset
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.spy

class SessionManagerTest {
    @Test
    fun `default session can be specified`() {
        val manager = SessionManager(mock(), defaultSession = { Session("http://www.mozilla.org") })
        assertEquals(0, manager.size)
    }

    @Test
    fun `default session is used when manager becomes empty`() {
        val session0 = Session("about:blank")
        val session1 = Session("http://www.firefox.com")

        val manager = SessionManager(mock(), defaultSession = { session0 })

        manager.add(session1)
        assertEquals(1, manager.size)
        assertEquals("http://www.firefox.com", manager.selectedSessionOrThrow.url)

        manager.remove(session1)
        assertEquals(1, manager.size)
        assertEquals("about:blank", manager.selectedSessionOrThrow.url)

        manager.add(session1)
        manager.removeAll()
        assertEquals(1, manager.size)
        assertEquals("about:blank", manager.selectedSessionOrThrow.url)

        manager.removeSessions()
        assertEquals(1, manager.size)
        assertEquals("about:blank", manager.selectedSessionOrThrow.url)
    }

    @Test
    fun `session can be added`() {
        val manager = SessionManager(mock())
        manager.add(Session("http://getpocket.com"))
        manager.add(Session("http://www.firefox.com"), true)

        assertEquals(2, manager.size)
        assertEquals("http://www.firefox.com", manager.selectedSessionOrThrow.url)
    }

    @Test
    fun `session can be added by specifying parent`() {
        val manager = SessionManager(mock())
        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://www.firefox.com")
        val session3 = Session("https://wiki.mozilla.org")
        val session4 = Session("https://github.com/mozilla-mobile/android-components")

        manager.add(session1)
        manager.add(session2)
        manager.add(session3, parent = session1)
        manager.add(session4, parent = session2)

        assertNull(manager.sessions[0].parentId)
        assertNull(manager.sessions[2].parentId)
        assertEquals(session1.id, manager.sessions[1].parentId)
        assertEquals(session2.id, manager.sessions[3].parentId)
    }

    @Test(expected = IllegalArgumentException::class)
    fun `session manager throws exception if parent is not in session manager`() {
        val parent = Session("https://www.mozilla.org")
        val session = Session("https://www.firefox.com")

        val manager = SessionManager(mock())
        manager.add(session, parent = parent)
    }

    @Test
    fun `session can be selected`() {
        val session1 = Session("http://www.mozilla.org")
        val session2 = Session("http://www.firefox.com")

        val manager = SessionManager(mock())
        manager.add(session1)
        manager.add(session2)

        assertEquals("http://www.mozilla.org", manager.selectedSessionOrThrow.url)
        manager.select(session2)
        assertEquals("http://www.firefox.com", manager.selectedSessionOrThrow.url)
    }

    @Test
    fun `observer gets notified when session gets selected`() {
        val session1 = Session("http://www.mozilla.org")
        val session2 = Session("http://www.firefox.com")

        val manager = SessionManager(mock())
        manager.add(session1)
        manager.add(session2)

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        manager.select(session2)

        verify(observer).onSessionSelected(session2)
    }

    @Test(expected = IllegalArgumentException::class)
    fun `manager throws exception if unknown session is selected`() {
        val manager = SessionManager(mock())
        manager.add(Session("http://www.mozilla.org"))

        manager.select(Session("https://getpocket.com"))
    }

    @Test
    fun `observer does not get notified after unregistering`() {
        val session1 = Session("http://www.mozilla.org")
        val session2 = Session("http://www.firefox.com")

        val manager = SessionManager(mock())
        manager.add(session1)
        manager.add(session2)

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        manager.select(session2)

        verify(observer).onSessionSelected(session2)
        verifyNoMoreInteractions(observer)

        manager.unregister(observer)

        manager.select(session1)

        verify(observer, never()).onSessionSelected(session1)
        verifyNoMoreInteractions(observer)
    }

    @Test
    fun `observer is called when session is added`() {
        val manager = SessionManager(mock())
        val session = Session("https://www.mozilla.org")

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        manager.add(session)

        verify(observer).onSessionAdded(session)
        verify(observer).onSessionSelected(session) // First session is selected automatically
        verifyNoMoreInteractions(observer)
    }

    @Test
    fun `observer is called when all sessions removed and default session present`() {
        val session0 = Session("https://www.mozilla.org")
        val manager = SessionManager(mock(), defaultSession = { session0 })
        val observer: SessionManager.Observer = mock()

        manager.register(observer)

        manager.removeAll()

        assertEquals(1, manager.size)
        verify(observer).onSessionAdded(session0)
        verify(observer).onSessionSelected(session0)
        verify(observer).onAllSessionsRemoved()

        val observer2: SessionManager.Observer = mock()

        manager.register(observer2)

        manager.removeSessions()

        assertEquals(1, manager.size)
        verify(observer2).onSessionAdded(session0)
        verify(observer2).onSessionSelected(session0)
        verify(observer2).onAllSessionsRemoved()
    }

    @Test
    fun `default session not used when all sessions were removed and they were all CustomTab`() {
        val session1 = Session("https://www.mozilla.org")
        session1.customTabConfig = Mockito.mock(CustomTabConfig::class.java)

        val manager = SessionManager(mock(), defaultSession = { Session("about:blank") })
        val observer: SessionManager.Observer = mock()

        manager.add(session1)
        manager.register(observer)

        manager.removeAll()

        assertEquals(0, manager.size)
        verify(observer).onAllSessionsRemoved()
    }

    @Test
    fun `default session is used when all sessions were removed and they were mixed CustomTab and regular`() {
        val session1 = Session("https://www.mozilla.org")
        session1.customTabConfig = Mockito.mock(CustomTabConfig::class.java)
        val session2 = Session("https://www.firefox.com")

        val session0 = Session("about:blank")
        val manager = SessionManager(mock(), defaultSession = { session0 })
        val observer: SessionManager.Observer = mock()

        manager.register(observer)

        manager.add(session1)
        manager.add(session2)
        manager.removeAll()

        assertEquals(1, manager.size)
        assertEquals("about:blank", manager.selectedSessionOrThrow.url)

        verify(observer).onAllSessionsRemoved()
        verify(observer).onSessionSelected(session0)
        verify(observer).onSessionAdded(session0)
    }

    @Test
    fun `observer is called when session is removed`() {
        val manager = SessionManager(mock())
        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://www.firefox.com")

        manager.add(session1)
        manager.add(session2)

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        manager.remove(session1)

        verify(observer).onSessionRemoved(session1)
        verifyNoMoreInteractions(observer)
    }

    @Test
    fun `observer is not called when session to remove is not in list`() {
        val manager = SessionManager(mock())
        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://www.firefox.com")

        manager.add(session1)

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        manager.remove(session2)

        verifyNoMoreInteractions(observer)
    }

    @Test
    fun `initial session is selected`() {
        val session = Session("https://www.mozilla.org")

        val manager = SessionManager(mock())
        manager.add(session)

        assertEquals(1, manager.size)
        assertEquals(session, manager.selectedSession)
    }

    @Test
    fun `manager can have no session`() {
        val manager = SessionManager(mock())

        assertEquals(0, manager.size)
    }

    @Test
    fun `createSnapshot works when manager has no sessions`() {
        val manager = SessionManager(mock())
        assertTrue(manager.createSnapshot().isEmpty())
    }

    @Test
    fun `createSnapshot ignores private sessions`() {
        val manager = SessionManager(mock())
        val session = Session("http://mozilla.org", true)
        manager.add(session)

        assertTrue(manager.createSnapshot().isEmpty())
    }

    @Test
    fun `createSnapshot ignores CustomTab sessions`() {
        val manager = SessionManager(mock())
        val session = Session("http://mozilla.org")
        session.customTabConfig = Mockito.mock(CustomTabConfig::class.java)
        manager.add(session)

        assertTrue(manager.createSnapshot().isEmpty())
    }

    @Test
    fun `createSnapshot ignores private CustomTab sessions`() {
        val manager = SessionManager(mock())
        val session = Session("http://mozilla.org", true)
        session.customTabConfig = Mockito.mock(CustomTabConfig::class.java)
        manager.add(session)

        assertTrue(manager.createSnapshot().isEmpty())
    }

    @Test
    fun `restore checks validity of a snapshot - empty`() {
        val manager = SessionManager(mock())

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        manager.restore(SessionManager.Snapshot(listOf(), selectedSessionIndex = 0))

        verify(observer, never()).onSessionsRestored()
    }

    @Test
    fun `restore may be used to bulk-add session from a SessionsSnapshot`() {
        val manager = SessionManager(mock())

        // Just one session in the snapshot.
        manager.restore(
            SessionManager.Snapshot(
                listOf(SessionManager.Snapshot.Item(session = Session("http://www.mozilla.org"))),
                selectedSessionIndex = 0
            )
        )
        assertEquals(1, manager.size)
        assertEquals("http://www.mozilla.org", manager.selectedSessionOrThrow.url)

        // Multiple sessions in the snapshot.
        val regularSession = Session("http://www.firefox.com")
        val engineSessionState: EngineSessionState = mock()
        val engineSession = mock(EngineSession::class.java)
        `when`(engineSession.saveState()).thenReturn(engineSessionState)

        val snapshot = SessionManager.Snapshot(
            listOf(
                SessionManager.Snapshot.Item(
                    session = regularSession,
                    engineSession = engineSession
                ),
                SessionManager.Snapshot.Item(session = Session("http://www.wikipedia.org"))
            ),
            selectedSessionIndex = 0
        )
        manager.restore(snapshot)
        assertEquals(3, manager.size)
        assertEquals("http://www.firefox.com", manager.selectedSessionOrThrow.url)
        assertEquals(engineSession, manager.selectedSessionOrThrow.engineSessionHolder.engineSession)
        assertNull(manager.selectedSessionOrThrow.engineSessionHolder.engineSessionState)
    }

    @Test
    fun `restore fires correct notifications`() {
        val manager = SessionManager(mock())

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        val session = Session("http://www.mozilla.org")
        // Snapshot with a single session.
        manager.restore(SessionManager.Snapshot(listOf(
            SessionManager.Snapshot.Item(
                session
            )
        ), 0))

        verify(observer, times(1)).onSessionsRestored()
        verify(observer, never()).onSessionAdded(session)
        verify(observer, times(1)).onSessionSelected(session)

        manager.removeAll()
        reset(observer)

        val session2 = Session("http://www.firefox.com")
        val session3 = Session("http://www.wikipedia.org")
        // Snapshot with multiple sessions.
        manager.restore(SessionManager.Snapshot(
            listOf(
                SessionManager.Snapshot.Item(session2),
                SessionManager.Snapshot.Item(session3),
                SessionManager.Snapshot.Item(session)
            ),
            1
        ))

        assertEquals(3, manager.size)
        verify(observer, times(1)).onSessionsRestored()
        verify(observer, never()).onSessionAdded(session)
        verify(observer, never()).onSessionAdded(session2)
        verify(observer, never()).onSessionAdded(session3)
        verify(observer, never()).onSessionSelected(session)
        verify(observer, never()).onSessionSelected(session2)
        verify(observer, times(1)).onSessionSelected(session3)
    }

    @Test
    fun `createSnapshot produces a correct snapshot of sessions`() {
        val manager = SessionManager(mock())
        val customTabSession = Session("http://mozilla.org")
        customTabSession.customTabConfig = Mockito.mock(CustomTabConfig::class.java)
        val privateSession = Session("http://www.secret.com", true)
        val privateCustomTabSession = Session("http://very.secret.com", true)
        privateCustomTabSession.customTabConfig = Mockito.mock(CustomTabConfig::class.java)

        val regularSession = Session("http://www.firefox.com")
        val engineSessionState: EngineSessionState = mock()
        val engineSession = mock(EngineSession::class.java)
        `when`(engineSession.saveState()).thenReturn(engineSessionState)

        val engine = mock(Engine::class.java)
        `when`(engine.name()).thenReturn("gecko")
        `when`(engine.createSession()).thenReturn(mock(EngineSession::class.java))

        manager.add(regularSession, false, engineSession)
        manager.add(Session("http://firefox.com"), true, engineSession)
        manager.add(Session("http://wikipedia.org"), false, engineSession)
        manager.add(privateSession)
        manager.add(customTabSession)
        manager.add(privateCustomTabSession)

        val snapshot = manager.createSnapshot()
        assertEquals(3, snapshot!!.sessions.size)
        assertEquals(1, snapshot.selectedSessionIndex)

        val snapshotSession = snapshot.sessions[0]
        assertEquals("http://www.firefox.com", snapshotSession.session.url)

        val snapshotState = snapshotSession.engineSession!!.saveState()
        assertEquals(engineSessionState, snapshotState)
    }

    @Test
    fun `createSnapshot resets selection index if selected session was private`() {
        val manager = SessionManager(mock())

        val privateSession = Session("http://www.secret.com", true)
        val regularSession1 = Session("http://www.firefox.com")
        val regularSession2 = Session("http://www.mozilla.org")

        val engine = mock(Engine::class.java)
        `when`(engine.name()).thenReturn("gecko")
        `when`(engine.createSession()).thenReturn(mock(EngineSession::class.java))

        manager.add(regularSession1, false)
        manager.add(regularSession2, false)
        manager.add(privateSession, true)

        val snapshot = manager.createSnapshot()
        assertEquals(2, snapshot!!.sessions.size)
        assertEquals(0, snapshot.selectedSessionIndex)
    }

    @Test
    fun `selectedSession is null with no selection`() {
        val manager = SessionManager(mock())
        assertNull(manager.selectedSession)
    }

    @Test
    fun `selected session will be recalculated when selected session gets removed`() {
        val manager = SessionManager(mock())

        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://www.firefox.com")
        val session3 = Session("https://wiki.mozilla.org")
        val session4 = Session("https://github.com/mozilla-mobile/android-components")

        manager.add(session1)
        manager.add(session2)
        manager.add(session3)
        manager.add(session4)

        // (1), 2, 3, 4
        assertEquals(session1, manager.selectedSession)

        // 1, 2, 3, (4)
        manager.select(session4)
        assertEquals(session4, manager.selectedSession)

        // 1, 2, (3)
        manager.remove(session4)
        assertEquals(session3, manager.selectedSession)

        // 2, (3)
        manager.remove(session1)
        assertEquals(session3, manager.selectedSession)

        // (2), 3
        manager.select(session2)
        assertEquals(session2, manager.selectedSession)

        // (2)
        manager.remove(session3)
        assertEquals(session2, manager.selectedSession)

        // -
        manager.remove(session2)
        assertEquals(0, manager.size)
    }

    @Test
    fun `sessions property removes immutable copy`() {
        val manager = SessionManager(mock())

        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://www.firefox.com")
        val session3 = Session("https://wiki.mozilla.org")
        val session4 = Session("https://github.com/mozilla-mobile/android-components")

        manager.add(session1)
        manager.add(session2)
        manager.add(session3)
        manager.add(session4)

        val sessions = manager.sessions

        assertEquals(4, sessions.size)
        assertTrue(sessions.contains(session1))
        assertTrue(sessions.contains(session2))
        assertTrue(sessions.contains(session3))
        assertTrue(sessions.contains(session4))

        manager.remove(session1)

        assertEquals(3, manager.size)
        assertEquals(4, sessions.size)
    }

    @Test
    fun `removeAll removes all sessions and notifies observer`() {
        val manager = SessionManager(mock())

        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://www.firefox.com")
        val session3 = Session("https://wiki.mozilla.org")
        val session4 = Session("https://github.com/mozilla-mobile/android-components")
        session4.customTabConfig = Mockito.mock(CustomTabConfig::class.java)

        manager.add(session1)
        manager.add(session2)
        manager.add(session3)
        manager.add(session4)

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        assertEquals(4, manager.size)

        manager.removeAll()

        assertEquals(0, manager.size)

        verify(observer).onAllSessionsRemoved()
        verifyNoMoreInteractions(observer)
    }

    @Test
    fun `findSessionById returns session with same id`() {
        val manager = SessionManager(mock())

        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://www.firefox.com")
        val session3 = Session("https://wiki.mozilla.org")
        val session4 = Session("https://github.com/mozilla-mobile/android-components")

        manager.add(session1)
        manager.add(session2)
        manager.add(session3)
        manager.add(session4)

        assertEquals(session1, manager.findSessionById(session1.id))
        assertEquals(session2, manager.findSessionById(session2.id))
        assertEquals(session3, manager.findSessionById(session3.id))
        assertEquals(session4, manager.findSessionById(session4.id))

        assertNull(manager.findSessionById("banana"))
    }

    @Test
    fun `session manager creates and links engine session`() {
        val engine: Engine = mock()

        val actualEngineSession: EngineSession = mock()
        doReturn(actualEngineSession).`when`(engine).createSession(false)
        val privateEngineSession: EngineSession = mock()
        doReturn(privateEngineSession).`when`(engine).createSession(true)

        val sessionManager = SessionManager(engine)

        val session = Session("https://www.mozilla.org")
        sessionManager.add(session)

        assertNull(sessionManager.getEngineSession(session))
        assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session))
        assertEquals(actualEngineSession, sessionManager.getEngineSession(session))
        assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session))

        val privateSession = Session("https://www.mozilla.org", true, Session.Source.NONE)
        sessionManager.add(privateSession)
        assertNull(sessionManager.getEngineSession(privateSession))
        assertEquals(privateEngineSession, sessionManager.getOrCreateEngineSession(privateSession))
    }

    @Test
    fun `removing a session unlinks the engine session`() {
        val engine: Engine = mock()

        val actualEngineSession: EngineSession = mock()
        doReturn(actualEngineSession).`when`(engine).createSession()

        val sessionManager = SessionManager(engine)

        val session = Session("https://www.mozilla.org")
        sessionManager.add(session)

        assertNotNull(sessionManager.getOrCreateEngineSession(session))
        assertNotNull(session.engineSessionHolder.engineSession)
        assertNotNull(session.engineSessionHolder.engineObserver)

        sessionManager.remove(session)

        assertNull(session.engineSessionHolder.engineSession)
        assertNull(session.engineSessionHolder.engineObserver)
    }

    @Test
    fun `add will link an engine session if provided`() {
        val engine: Engine = mock()

        val actualEngineSession: EngineSession = mock()
        val sessionManager = SessionManager(engine)

        val session = Session("https://www.mozilla.org")
        assertNull(session.engineSessionHolder.engineSession)
        assertNull(session.engineSessionHolder.engineObserver)

        sessionManager.add(session, engineSession = actualEngineSession)

        assertNotNull(session.engineSessionHolder.engineSession)
        assertNotNull(session.engineSessionHolder.engineObserver)

        assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session))
        assertEquals(actualEngineSession, sessionManager.getEngineSession(session))
        assertEquals(actualEngineSession, sessionManager.getOrCreateEngineSession(session))
    }

    @Test
    fun `removeSessions retains customtab sessions`() {
        val manager = SessionManager(mock())

        val session1 = Session("https://www.mozilla.org")
        val session2 = Session("https://getPocket.com")
        val session3 = Session("https://www.firefox.com")
        session2.customTabConfig = Mockito.mock(CustomTabConfig::class.java)

        manager.add(session1)
        manager.add(session2)
        manager.add(session3)

        val observer: SessionManager.Observer = mock()
        manager.register(observer)

        assertEquals(3, manager.size)

        manager.removeSessions()

        assertEquals(1, manager.size)
        assertEquals(session2, manager.all[0])

        verify(observer).onAllSessionsRemoved()
        verifyNoMoreInteractions(observer)
    }

    @Test(expected = IllegalStateException::class)
    fun `exception is thrown from selectedSessionOrThrow with no selection`() {
        val manager = SessionManager(mock())
        manager.selectedSessionOrThrow
    }

    @Test
    fun `all not selected sessions should be removed on Low Memory`() {
        val manager = SessionManager(mock())

        val emptyBitmap = spy(Bitmap::class.java)

        val session1 = Session("https://www.mozilla.org")
        session1.thumbnail = emptyBitmap

        val session2 = Session("https://getPocket.com")
        session2.thumbnail = emptyBitmap

        val session3 = Session("https://www.firefox.com")
        session3.thumbnail = emptyBitmap

        manager.add(session1, true)
        manager.add(session2, false)
        manager.add(session3, false)

        val allSessionsMustHaveAThumbnail = manager.all.all { it.thumbnail != null }

        assertTrue(allSessionsMustHaveAThumbnail)

        manager.onLowMemory()

        val onlySelectedSessionMustHaveAThumbnail =
                session1.thumbnail != null && session2.thumbnail == null && session3.thumbnail == null

        assertTrue(onlySelectedSessionMustHaveAThumbnail)
    }

    @Test
    fun `custom tab session will not be selected if it is the first session`() {
        val session = Session("about:blank")
        session.customTabConfig = Mockito.mock(CustomTabConfig::class.java)

        val manager = SessionManager(mock())
        manager.add(session)

        assertNull(manager.selectedSession)
    }

    @Test
    fun `parent will be selected if child is removed and flag is set to true`() {
        val parent = Session("https://www.mozilla.org")

        val session1 = Session("https://www.firefox.com")
        val session2 = Session("https://getpocket.com")
        val child = Session("https://www.mozilla.org/en-US/internet-health/")

        val manager = SessionManager(mock())
        manager.add(parent)
        manager.add(session1)
        manager.add(session2)
        manager.add(child, parent = parent)

        manager.select(child)
        manager.remove(child, selectParentIfExists = true)

        assertEquals(parent, manager.selectedSession)
        assertEquals("https://www.mozilla.org", manager.selectedSessionOrThrow.url)
    }

    @Test
    fun `parent will not be selected if child is removed and flag is set to false`() {
        val parent = Session("https://www.mozilla.org")

        val session1 = Session("https://www.firefox.com")
        val session2 = Session("https://getpocket.com")
        val child1 = Session("https://www.mozilla.org/en-US/internet-health/")
        val child2 = Session("https://www.mozilla.org/en-US/technology/")

        val manager = SessionManager(mock())
        manager.add(parent)
        manager.add(session1)
        manager.add(session2)
        manager.add(child1, parent = parent)
        manager.add(child2, parent = parent)

        manager.select(child1)
        manager.remove(child1, selectParentIfExists = false)

        assertEquals(session1, manager.selectedSession)
        assertEquals("https://www.firefox.com", manager.selectedSessionOrThrow.url)
    }

    @Test
    fun `Setting selectParentIfExists when removing session without parent has no effect`() {
        val session1 = Session("https://www.firefox.com")
        val session2 = Session("https://getpocket.com")
        val session3 = Session("https://www.mozilla.org/en-US/internet-health/")

        val manager = SessionManager(mock())
        manager.add(session1)
        manager.add(session2)
        manager.add(session3)

        manager.select(session3)
        manager.remove(session3, selectParentIfExists = true)

        assertEquals(session2, manager.selectedSession)
        assertEquals("https://getpocket.com", manager.selectedSessionOrThrow.url)
    }

    @Test
    fun `Sessions with parent are added after parent`() {
        val parent01 = Session("https://www.mozilla.org")
        val parent02 = Session("https://getpocket.com")

        val session1 = Session("https://www.firefox.com")
        val session2 = Session("https://developer.mozilla.org/en-US/")
        val child001 = Session("https://www.mozilla.org/en-US/internet-health/")
        val child002 = Session("https://www.mozilla.org/en-US/technology/")
        val child003 = Session("https://getpocket.com/add/")

        val manager = SessionManager(mock())
        manager.add(parent01)
        manager.add(session1)
        manager.add(child001, parent = parent01)
        manager.add(session2)
        manager.add(parent02)
        manager.add(child002, parent = parent01)
        manager.add(child003, parent = parent02)

        assertEquals(parent01, manager.sessions[0]) // ├── parent 1
        assertEquals(child002, manager.sessions[1]) // │   ├── child 2
        assertEquals(child001, manager.sessions[2]) // │   └── child 1
        assertEquals(session1, manager.sessions[3]) // ├──session 1
        assertEquals(session2, manager.sessions[4]) // ├──session 2
        assertEquals(parent02, manager.sessions[5]) // └── parent 2
        assertEquals(child003, manager.sessions[6]) //     └── child 3
    }

    @Test
    fun `SessionManager updates parent id of children after updating parent`() {
        val session1 = Session("https://www.firefox.com")
        val session2 = Session("https://developer.mozilla.org/en-US/")
        val session3 = Session("https://www.mozilla.org/en-US/internet-health/")
        val session4 = Session("https://www.mozilla.org/en-US/technology/")

        val manager = SessionManager(mock())
        manager.add(session1)
        manager.add(session2, parent = session1)
        manager.add(session3, parent = session2)
        manager.add(session4, parent = session3)

        // session 1 <- session2 <- session3 <- session4
        assertNull(session1.parentId)
        assertEquals(session1.id, session2.parentId)
        assertEquals(session2.id, session3.parentId)
        assertEquals(session3.id, session4.parentId)

        manager.remove(session3)

        assertEquals(session1, manager.sessions[0])
        assertEquals(session2, manager.sessions[1])
        assertEquals(session4, manager.sessions[2])

        // session1 <- session2 <- session4
        assertNull(session1.parentId)
        assertEquals(session1.id, session2.parentId)
        assertEquals(session2.id, session4.parentId)

        manager.remove(session1)

        assertEquals(session2, manager.sessions[0])
        assertEquals(session4, manager.sessions[1])

        // session2 <- session4
        assertNull(session2.parentId)
        assertEquals(session2.id, session4.parentId)
    }

    @Test
    fun `SessionManager should not select custom tab session after removing selected session`() {
        val manager = SessionManager(mock())
        assertNull(manager.selectedSession)

        val customTabSession = Session("https://www.mozilla.org")
        customTabSession.customTabConfig = mock()
        manager.add(customTabSession)

        assertNull(manager.selectedSession)

        val session = Session("https://www.firefox.com")
        manager.add(session)

        assertEquals(session, manager.selectedSession)

        manager.remove(session)

        assertNull(manager.selectedSession)
    }

    @Test
    fun `SessionManager should not select parent if it is a custom tab session`() {
        val parent = Session("https://www.mozilla.org")
        parent.customTabConfig = mock()

        val session1 = Session("https://www.firefox.com")
        val session2 = Session("https://getpocket.com")
        val child = Session("https://www.mozilla.org/en-US/internet-health/")

        val manager = SessionManager(mock())
        manager.add(parent)
        manager.add(session1)
        manager.add(session2)
        manager.add(child, parent = parent)

        manager.select(child)
        manager.remove(child, selectParentIfExists = true)

        assertEquals(session2, manager.selectedSession)
        assertEquals("https://getpocket.com", manager.selectedSessionOrThrow.url)
    }

    @Test
    fun `SessionManager will select new non custom tab session if selected session gets removed`() {
        val session1 = Session("https://www.firefox.com").apply { customTabConfig = mock() }
        val session2 = Session("https://developer.mozilla.org/en-US/")
        val session3 = Session("https://www.mozilla.org/en-US/internet-health/").apply { customTabConfig = mock() }
        val session4 = Session("https://www.mozilla.org/en-US/technology/")
        val session5 = Session("https://example.org/555").apply { customTabConfig = mock() }
        val session6 = Session("https://example.org/Hello").apply { customTabConfig = mock() }
        val session7 = Session("https://example.org/World").apply { customTabConfig = mock() }
        val session8 = Session("https://example.org/JustTestingThings").apply { customTabConfig = mock() }
        val session9 = Session("https://example.org/NoCustomTab")

        val manager = SessionManager(mock())
        manager.add(session1)
        manager.add(session2)
        manager.add(session3)
        manager.add(session4)
        manager.add(session5)
        manager.add(session6)
        manager.add(session7)
        manager.add(session8)
        manager.add(session9)

        manager.select(session4)
        assertEquals(session4, manager.selectedSession)

        manager.remove(session4)
        assertEquals(session2, manager.selectedSession)

        manager.remove(session2)
        assertEquals(session9, manager.selectedSession)

        manager.remove(session9)
        assertNull(manager.selectedSession)
    }

    @Test(expected = java.lang.IllegalStateException::class)
    fun `SessionManager throws if parent to be selected cannot be found`() {
        // This should never be possible with the current implementation of SessionManager. However this test makes
        // sure that if this situation occurs (bug) then we throw an exception.

        val parent = Session("https://www.mozilla.org")
        val session = Session("https://www.firefox.com")

        val manager = SessionManager(mock())
        manager.add(parent)
        manager.add(session, parent = parent)

        manager.select(session)

        // The parent id is marked as "internal". Component consuemrs normally cannot modify it.
        session.parentId = "DoesNotExist"

        manager.remove(session, selectParentIfExists = true)
    }

    @Test
    fun `SessionManager will select nearby private session if selected private session gets removed`() {
        val manager = SessionManager(mock())
        assertNull(manager.selectedSession)

        val private1 = Session("https://example.org/private1", private = true)
        manager.add(private1)

        val regular1 = Session("https://www.mozilla.org", private = false)
        manager.add(regular1)

        val regular2 = Session("https://www.firefox.com", private = false)
        manager.add(regular2)

        val private2 = Session("https://example.org/private2", private = true)
        manager.add(private2)

        val private3 = Session("https://example.org/private3", private = true)
        manager.add(private3)

        manager.select(private2)
        manager.remove(private2)
        assertEquals(private3, manager.selectedSession)

        manager.remove(private3)
        assertEquals(private1, manager.selectedSession)

        // Removing the last private session should cause a regular session to be selected
        manager.remove(private1)
        assertEquals(regular2, manager.selectedSession)
    }

    @Test
    fun `SessionManager will select nearby regular session if selected regular session gets removed`() {
        val manager = SessionManager(mock())
        assertNull(manager.selectedSession)

        val regular1 = Session("https://www.mozilla.org", private = false)
        manager.add(regular1)

        val private1 = Session("https://example.org/private1", private = true)
        manager.add(private1)

        val private2 = Session("https://example.org/private2", private = true)
        manager.add(private2)

        val regular2 = Session("https://www.firefox.com", private = false)
        manager.add(regular2)

        val regular3 = Session("https://www.firefox.org", private = false)
        manager.add(regular3)

        manager.select(regular2)
        manager.remove(regular2)
        assertEquals(regular3, manager.selectedSession)

        manager.remove(regular3)
        assertEquals(regular1, manager.selectedSession)

        // Removing the last regular session should NOT cause a private session to be selected
        manager.remove(regular1)
        assertNull(manager.selectedSession)
    }

    @Test
    fun `SessionManager will select default if regular session is removed an no other regular session is left`() {
        val defaultSession = Session("http://www.mozilla.com")
        val manager = SessionManager(mock(), defaultSession = { defaultSession })
        assertNull(manager.selectedSession)

        val regular1 = Session("https://www.getpocket.com", private = false)
        manager.add(regular1)

        val private1 = Session("https://example.org/private1", private = true)
        manager.add(private1)

        val private2 = Session("https://example.org/private2", private = true)
        manager.add(private2)

        val regular2 = Session("https://www.firefox.com", private = false)
        manager.add(regular2)

        val regular3 = Session("https://www.firefox.org", private = false)
        manager.add(regular3, selected = true)

        manager.remove(regular3)
        assertEquals(regular2, manager.selectedSession)

        manager.remove(regular2)
        assertEquals(regular1, manager.selectedSession)

        // Removing the last regular session should NOT cause a private session to be selected,
        // but a default session was provided, so it should be selected now.
        manager.remove(regular1)
        assertEquals(manager.defaultSession?.invoke(), manager.selectedSession)
        assertEquals("http://www.mozilla.com", manager.selectedSession?.url)
    }

    @Test
    fun `SessionManager#runWithSession executes the block when session found`() {
        val sessionManager = spy(SessionManager(mock()))

        `when`(sessionManager.findSessionById(anyString())).thenReturn(mock())

        val executed = sessionManager.runWithSession("123") { true }

        assertTrue(executed)
    }

    @Test
    fun `SessionManager#runWithSession with null session ID`() {
        val sessionManager = spy(SessionManager(mock()))

        val executed = sessionManager.runWithSession(null) { true }

        assertFalse(executed)
    }

    @Test
    fun `SessionManager#runWithSession with null session`() {
        val sessionManager = spy(SessionManager(mock()))

        val executed = sessionManager.runWithSession("123") { true }

        assertFalse(executed)
    }
}
