/*
 * Copyright (C) 2022 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.server.input

import android.content.Context
import android.content.ContextWrapper
import android.hardware.input.IInputManager
import android.hardware.input.InputManager
import android.hardware.input.InputManagerGlobal
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.provider.Settings
import android.view.InputDevice
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream

private fun createKeyboard(deviceId: Int): InputDevice =
    InputDevice.Builder()
        .setId(deviceId)
        .setName("Device $deviceId")
        .setDescriptor("descriptor $deviceId")
        .setSources(InputDevice.SOURCE_KEYBOARD)
        .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
        .setExternal(true)
        .build()

/**
 * Tests for {@link KeyRemapper}.
 *
 * Build/Install/Run:
 * atest InputTests:KeyRemapperTests
 */
@Presubmit
class KeyRemapperTests {

    companion object {
        const val DEVICE_ID = 1
        val REMAPPABLE_KEYS = intArrayOf(
            KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT,
            KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT,
            KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT,
            KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT,
            KeyEvent.KEYCODE_CAPS_LOCK
        )
    }

    @get:Rule
    val rule = MockitoJUnit.rule()!!

    @Mock
    private lateinit var iInputManager: IInputManager
    @Mock
    private lateinit var native: NativeInputManagerService
    private lateinit var mKeyRemapper: KeyRemapper
    private lateinit var context: Context
    private lateinit var dataStore: PersistentDataStore
    private lateinit var testLooper: TestLooper
    private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession

    @Before
    fun setup() {
        context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
        dataStore = PersistentDataStore(object : PersistentDataStore.Injector() {
            override fun openRead(): InputStream? {
                throw FileNotFoundException()
            }

            override fun startWrite(): FileOutputStream? {
                throw IOException()
            }

            override fun finishWrite(fos: FileOutputStream?, success: Boolean) {}
        })
        testLooper = TestLooper()
        mKeyRemapper = KeyRemapper(
            context,
            native,
            dataStore,
            testLooper.looper
        )
        inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
        val inputManager = InputManager(context)
        Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE)))
            .thenReturn(inputManager)
        Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID))
    }

    @After
    fun tearDown() {
        if (this::inputManagerGlobalSession.isInitialized) {
            inputManagerGlobalSession.close()
        }
    }

    @Test
    fun testKeyRemapping_whenRemappingEnabled() {
        ModifierRemappingFlag(true).use {
            val keyboard = createKeyboard(DEVICE_ID)
            Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard)

            for (i in REMAPPABLE_KEYS.indices) {
                val fromKeyCode = REMAPPABLE_KEYS[i]
                val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
                mKeyRemapper.remapKey(fromKeyCode, toKeyCode)
                testLooper.dispatchNext()
            }

            val remapping = mKeyRemapper.keyRemapping
            val expectedSize = REMAPPABLE_KEYS.size
            assertEquals("Remapping size should be $expectedSize", expectedSize, remapping.size)

            for (i in REMAPPABLE_KEYS.indices) {
                val fromKeyCode = REMAPPABLE_KEYS[i]
                val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size]
                assertEquals(
                    "Remapping should include mapping from $fromKeyCode to $toKeyCode",
                    toKeyCode,
                    remapping.getOrDefault(fromKeyCode, -1)
                )
            }

            mKeyRemapper.clearAllKeyRemappings()
            testLooper.dispatchNext()

            assertEquals(
                "Remapping size should be 0 after clearAllModifierKeyRemappings",
                0,
                mKeyRemapper.keyRemapping.size
            )
        }
    }

    @Test
    fun testKeyRemapping_whenRemappingDisabled() {
        ModifierRemappingFlag(false).use {
            val keyboard = createKeyboard(DEVICE_ID)
            Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard)

            mKeyRemapper.remapKey(REMAPPABLE_KEYS[0], REMAPPABLE_KEYS[1])
            testLooper.dispatchAll()

            val remapping = mKeyRemapper.keyRemapping
            assertEquals(
                "Remapping should not be done if modifier key remapping is disabled",
                0,
                remapping.size
            )
        }
    }

    private inner class ModifierRemappingFlag constructor(enabled: Boolean) : AutoCloseable {
        init {
            Settings.Global.putString(
                context.contentResolver,
                "settings_new_keyboard_modifier_key", enabled.toString()
            )
        }

        override fun close() {
            Settings.Global.putString(
                context.contentResolver,
                "settings_new_keyboard_modifier_key",
                ""
            )
        }
    }
}