/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package com.android.systemui.statusbar.phone

import android.content.res.Configuration
import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_LTR
import android.content.res.Configuration.SCREENLAYOUT_LAYOUTDIR_RTL
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.content.res.Configuration.UI_MODE_TYPE_CAR
import android.os.LocaleList
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import java.util.Locale

@RunWith(AndroidTestingRunner::class)
@SmallTest
class ConfigurationControllerImplTest : SysuiTestCase() {

    private lateinit var mConfigurationController: ConfigurationControllerImpl

    @Before
    fun setUp() {
        mConfigurationController = ConfigurationControllerImpl(mContext)
    }

    @Test
    fun testThemeChange() {
        val listener = mock(ConfigurationListener::class.java)
        mConfigurationController.addCallback(listener)

        mConfigurationController.notifyThemeChanged()
        verify(listener).onThemeChanged()
    }

    @Test
    fun testRemoveListenerDuringCallback() {
        val listener = mock(ConfigurationListener::class.java)
        mConfigurationController.addCallback(listener)
        val listener2 = mock(ConfigurationListener::class.java)
        mConfigurationController.addCallback(listener2)

        doAnswer {
            mConfigurationController.removeCallback(listener2)
            null
        }.`when`(listener).onThemeChanged()

        mConfigurationController.notifyThemeChanged()
        verify(listener).onThemeChanged()
        verify(listener2, never()).onThemeChanged()
    }

    @Test
    fun configChanged_listenerNotified() {
        val config = mContext.resources.configuration
        config.densityDpi = 12
        config.smallestScreenWidthDp = 240
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the config is updated
        config.densityDpi = 20
        config.smallestScreenWidthDp = 300
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.changedConfig?.densityDpi).isEqualTo(20)
        assertThat(listener.changedConfig?.smallestScreenWidthDp).isEqualTo(300)
    }

    @Test
    fun densityChanged_listenerNotified() {
        val config = mContext.resources.configuration
        config.densityDpi = 12
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the density is updated
        config.densityDpi = 20
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.densityOrFontScaleChanged).isTrue()
    }

    @Test
    fun fontChanged_listenerNotified() {
        val config = mContext.resources.configuration
        config.fontScale = 1.5f
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the font is updated
        config.fontScale = 1.4f
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.densityOrFontScaleChanged).isTrue()
    }

    @Test
    fun isCarAndUiModeChanged_densityListenerNotified() {
        val config = mContext.resources.configuration
        config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_YES
        // Re-create the controller since we calculate car mode on creation
        mConfigurationController = ConfigurationControllerImpl(mContext)

        val listener = createAndAddListener()

        // WHEN the ui mode is updated
        config.uiMode = UI_MODE_TYPE_CAR or UI_MODE_NIGHT_NO
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.densityOrFontScaleChanged).isTrue()
    }

    @Test
    fun isNotCarAndUiModeChanged_densityListenerNotNotified() {
        val config = mContext.resources.configuration
        config.uiMode = UI_MODE_NIGHT_YES
        // Re-create the controller since we calculate car mode on creation
        mConfigurationController = ConfigurationControllerImpl(mContext)

        val listener = createAndAddListener()

        // WHEN the ui mode is updated
        config.uiMode = UI_MODE_NIGHT_NO
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is not notified because it's not car mode
        assertThat(listener.densityOrFontScaleChanged).isFalse()
    }

    @Test
    fun smallestScreenWidthChanged_listenerNotified() {
        val config = mContext.resources.configuration
        config.smallestScreenWidthDp = 240
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the width is updated
        config.smallestScreenWidthDp = 300
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.smallestScreenWidthChanged).isTrue()
    }

    @Test
    fun maxBoundsChange_newConfigObject_listenerNotified() {
        val config = mContext.resources.configuration
        config.windowConfiguration.setMaxBounds(0, 0, 200, 200)
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN a new configuration object with new bounds is sent
        val newConfig = Configuration()
        newConfig.windowConfiguration.setMaxBounds(0, 0, 100, 100)
        mConfigurationController.onConfigurationChanged(newConfig)

        // THEN the listener is notified
        assertThat(listener.maxBoundsChanged).isTrue()
    }

    // Regression test for b/245799099
    @Test
    fun maxBoundsChange_sameObject_listenerNotified() {
        val config = mContext.resources.configuration
        config.windowConfiguration.setMaxBounds(0, 0, 200, 200)
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the existing config is updated with new bounds
        config.windowConfiguration.setMaxBounds(0, 0, 100, 100)
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.maxBoundsChanged).isTrue()
    }


    @Test
    fun localeListChanged_listenerNotified() {
        val config = mContext.resources.configuration
        config.setLocales(LocaleList(Locale.CANADA, Locale.GERMANY))
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the locales are updated
        config.setLocales(LocaleList(Locale.FRANCE, Locale.JAPAN, Locale.CHINESE))
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.localeListChanged).isTrue()
    }

    @Test
    fun uiModeChanged_listenerNotified() {
        val config = mContext.resources.configuration
        config.uiMode = UI_MODE_NIGHT_YES
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the ui mode is updated
        config.uiMode = UI_MODE_NIGHT_NO
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.uiModeChanged).isTrue()
    }

    @Test
    fun layoutDirectionUpdated_listenerNotified() {
        val config = mContext.resources.configuration
        config.screenLayout = SCREENLAYOUT_LAYOUTDIR_LTR
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the layout is updated
        config.screenLayout = SCREENLAYOUT_LAYOUTDIR_RTL
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.layoutDirectionChanged).isTrue()
    }

    @Test
    fun assetPathsUpdated_listenerNotified() {
        val config = mContext.resources.configuration
        config.assetsSeq = 45
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the assets sequence is updated
        config.assetsSeq = 46
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.themeChanged).isTrue()
    }

    @Test
    fun orientationUpdated_listenerNotified() {
        val config = mContext.resources.configuration
        config.orientation = Configuration.ORIENTATION_LANDSCAPE
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN the orientation is updated
        config.orientation = Configuration.ORIENTATION_PORTRAIT
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified
        assertThat(listener.orientationChanged).isTrue()
    }


    @Test
    fun multipleUpdates_listenerNotifiedOfAll() {
        val config = mContext.resources.configuration
        config.densityDpi = 14
        config.windowConfiguration.setMaxBounds(0, 0, 2, 2)
        config.uiMode = UI_MODE_NIGHT_YES
        mConfigurationController.onConfigurationChanged(config)

        val listener = createAndAddListener()

        // WHEN multiple fields are updated
        config.densityDpi = 20
        config.windowConfiguration.setMaxBounds(0, 0, 3, 3)
        config.uiMode = UI_MODE_NIGHT_NO
        mConfigurationController.onConfigurationChanged(config)

        // THEN the listener is notified of all of them
        assertThat(listener.densityOrFontScaleChanged).isTrue()
        assertThat(listener.maxBoundsChanged).isTrue()
        assertThat(listener.uiModeChanged).isTrue()
    }

    @Test
    @Ignore("b/261408895")
    fun equivalentConfigObject_listenerNotNotified() {
        val config = mContext.resources.configuration
        val listener = createAndAddListener()

        // WHEN we update with the new object that has all the same fields
        mConfigurationController.onConfigurationChanged(Configuration(config))

        listener.assertNoMethodsCalled()
    }

    private fun createAndAddListener(): TestListener {
        val listener = TestListener()
        mConfigurationController.addCallback(listener)
        // Adding a listener can trigger some callbacks, so we want to reset the values right
        // after the listener is added
        listener.reset()
        return listener
    }

    private class TestListener : ConfigurationListener {
        var changedConfig: Configuration? = null
        var densityOrFontScaleChanged = false
        var smallestScreenWidthChanged = false
        var maxBoundsChanged = false
        var uiModeChanged = false
        var themeChanged = false
        var localeListChanged = false
        var layoutDirectionChanged = false
        var orientationChanged = false

        override fun onConfigChanged(newConfig: Configuration?) {
            changedConfig = newConfig
        }
        override fun onDensityOrFontScaleChanged() {
            densityOrFontScaleChanged = true
        }
        override fun onSmallestScreenWidthChanged() {
            smallestScreenWidthChanged = true
        }
        override fun onMaxBoundsChanged() {
            maxBoundsChanged = true
        }
        override fun onUiModeChanged() {
            uiModeChanged = true
        }
        override fun onThemeChanged() {
            themeChanged = true
        }
        override fun onLocaleListChanged() {
            localeListChanged = true
        }
        override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
            layoutDirectionChanged = true
        }
        override fun onOrientationChanged(orientation: Int) {
            orientationChanged = true
        }

        fun assertNoMethodsCalled() {
            assertThat(densityOrFontScaleChanged).isFalse()
            assertThat(smallestScreenWidthChanged).isFalse()
            assertThat(maxBoundsChanged).isFalse()
            assertThat(uiModeChanged).isFalse()
            assertThat(themeChanged).isFalse()
            assertThat(localeListChanged).isFalse()
            assertThat(layoutDirectionChanged).isFalse()
        }

        fun reset() {
            changedConfig = null
            densityOrFontScaleChanged = false
            smallestScreenWidthChanged = false
            maxBoundsChanged = false
            uiModeChanged = false
            themeChanged = false
            localeListChanged = false
            layoutDirectionChanged = false
        }
    }
}
