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

import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.isActive
import kotlinx.coroutines.runBlocking
import mozilla.components.concept.toolbar.AutocompleteDelegate
import mozilla.components.concept.toolbar.AutocompleteResult
import mozilla.components.support.test.mock
import mozilla.components.ui.autocomplete.AutocompleteView
import mozilla.components.ui.autocomplete.InlineAutocompleteEditText
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Test
import org.mockito.Mockito.atLeast
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import java.util.concurrent.Executor

class AsyncFilterListenerTest {
    @Test
    fun `filter listener cancels prior filter executions`() = runBlocking {
        val urlView: AutocompleteView = mock()
        val filter: suspend (String, AutocompleteDelegate) -> Unit = mock()

        val dispatcher = spy(Executor {
            it.run()
        }.asCoroutineDispatcher())

        val listener = AsyncFilterListener(urlView, dispatcher, filter)

        verify(dispatcher, never()).cancelChildren()

        listener("test")

        verify(dispatcher, atLeastOnce()).cancelChildren()
    }

    @Test
    fun `filter delegate checks for cancellations before it runs, passes results to autocomplete view`() = runBlocking {
        var filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
            assertEquals("test", query)
            delegate.applyAutocompleteResult(AutocompleteResult(
                input = "test",
                text = "testing.com",
                url = "http://www.testing.com",
                source = "asyncTest",
                totalItems = 1
            ))
        }

        val dispatcher = spy(Executor {
            it.run()
        }.asCoroutineDispatcher())

        var didCallApply = 0

        var listener = AsyncFilterListener(object : AutocompleteView {
            override val originalText: String = "test"

            override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
                assertEquals("asyncTest", result.source)
                assertEquals("testing.com", result.text)
                assertEquals(1, result.totalItems)
                didCallApply += 1
            }

            override fun noAutocompleteResult() {
                fail()
            }
        }, dispatcher, filter, this.coroutineContext)

        verify(dispatcher, never()).isActive

        async { listener("test") }.await()

        // Checked if parent scope is still active. Somehow, each access to 'isActive' registers as 4?
        verify(dispatcher, atLeast(4)).isActive
        // Passed the result to the view's apply method exactly once.
        assertEquals(1, didCallApply)

        filter = { query, delegate ->
            assertEquals("moz", query)
            delegate.applyAutocompleteResult(AutocompleteResult(
                input = "moz",
                text = "mozilla.com",
                url = "http://www.mozilla.com",
                source = "asyncTestTwo",
                totalItems = 2
            ))
        }
        listener = AsyncFilterListener(object : AutocompleteView {
            override val originalText: String = "moz"

            override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
                assertEquals("asyncTestTwo", result.source)
                assertEquals("mozilla.com", result.text)
                assertEquals(2, result.totalItems)
                didCallApply += 1
            }

            override fun noAutocompleteResult() {
                fail()
            }
        }, dispatcher, filter, this.coroutineContext)

        async { listener("moz") }.await()

        verify(dispatcher, atLeast(8)).isActive
        assertEquals(2, didCallApply)
    }

    @Test
    fun `delegate discards stale results`() = runBlocking {
        val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
            assertEquals("test", query)
            delegate.applyAutocompleteResult(AutocompleteResult(
                input = "test",
                text = "testing.com",
                url = "http://www.testing.com",
                source = "asyncTest",
                totalItems = 1
            ))
        }

        val dispatcher = Executor {
            it.run()
        }.asCoroutineDispatcher()

        val listener = AsyncFilterListener(object : AutocompleteView {
            override val originalText: String = "nolongertest"

            override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
                fail()
            }

            override fun noAutocompleteResult() {
                fail()
            }
        }, dispatcher, filter, this.coroutineContext)

        listener("test")
    }

    @Test
    fun `delegate discards stale lack of results`() = runBlocking {
        val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
            assertEquals("test", query)
            delegate.noAutocompleteResult("test")
        }

        val dispatcher = Executor {
            it.run()
        }.asCoroutineDispatcher()

        val listener = AsyncFilterListener(object : AutocompleteView {
            override val originalText: String = "nolongertest"

            override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
                fail()
            }

            override fun noAutocompleteResult() {
                fail()
            }
        }, dispatcher, filter, this.coroutineContext)

        listener("test")
    }

    @Test
    fun `delegate passes through non-stale lack of results`() = runBlocking {
        val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
            assertEquals("test", query)
            delegate.noAutocompleteResult("test")
        }

        val dispatcher = Executor {
            it.run()
        }.asCoroutineDispatcher()

        var calledNoResults = 0
        val listener = AsyncFilterListener(object : AutocompleteView {
            override val originalText: String = "test"

            override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
                fail()
            }

            override fun noAutocompleteResult() {
                calledNoResults += 1
            }
        }, dispatcher, filter, this.coroutineContext)

        async { listener("test") }.await()

        assertEquals(1, calledNoResults)
    }

    @Test
    fun `delegate discards results if parent scope was cancelled`() = runBlocking {
        var preservedDelegate: AutocompleteDelegate? = null

        val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
            preservedDelegate = delegate
            assertEquals("test", query)
            delegate.applyAutocompleteResult(AutocompleteResult(
                input = "test",
                text = "testing.com",
                url = "http://www.testing.com",
                source = "asyncTest",
                totalItems = 1
            ))
        }

        val dispatcher = Executor {
            it.run()
        }.asCoroutineDispatcher()

        var calledResults = 0
        val listener = AsyncFilterListener(object : AutocompleteView {
            override val originalText: String = "test"

            override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
                assertEquals("asyncTest", result.source)
                assertEquals("testing.com", result.text)
                assertEquals(1, result.totalItems)
                calledResults += 1
            }

            override fun noAutocompleteResult() {
                fail()
            }
        }, dispatcher, filter, this.coroutineContext)

        async {
            listener("test")
            listener("test")
        }.await()

        // This result application should be discarded, because scope has been cancelled by the second
        // 'listener' call above.
        preservedDelegate!!.applyAutocompleteResult(AutocompleteResult(
            input = "test",
            text = "testing.com",
            url = "http://www.testing.com",
            source = "asyncCancelled",
            totalItems = 1
        ))

        assertEquals(2, calledResults)
    }

    @Test
    fun `delegate discards lack of results if parent scope was cancelled`() = runBlocking {
        var preservedDelegate: AutocompleteDelegate? = null

        val filter: suspend (String, AutocompleteDelegate) -> Unit = { query, delegate ->
            preservedDelegate = delegate
            assertEquals("test", query)
            delegate.noAutocompleteResult("test")
        }

        val dispatcher = Executor {
            it.run()
        }.asCoroutineDispatcher()

        var calledResults = 0
        val listener = AsyncFilterListener(object : AutocompleteView {
            override val originalText: String = "test"

            override fun applyAutocompleteResult(result: InlineAutocompleteEditText.AutocompleteResult) {
                fail()
            }

            override fun noAutocompleteResult() {
                calledResults += 1
            }
        }, dispatcher, filter, this.coroutineContext)

        async {
            listener("test")
            listener("test")
        }.await()

        // This "no results" call should be discarded, because scope has been cancelled by the second
        // 'listener' call above.
        preservedDelegate!!.noAutocompleteResult("test")

        assertEquals(2, calledResults)
    }
}
