/*
 * Copyright (C) 2019 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.om

import android.content.pm.parsing.AndroidPackage
import android.net.Uri
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.testng.Assert.assertThrows

@RunWith(Parameterized::class)
class OverlayReferenceMapperTests {

    companion object {
        private const val TARGET_PACKAGE_NAME = "com.test.target"
        private const val OVERLAY_PACKAGE_NAME = "com.test.overlay"
        private const val ACTOR_PACKAGE_NAME = "com.test.actor"
        private const val ACTOR_NAME = "overlay://test/actorName"

        @JvmStatic
        @Parameterized.Parameters(name = "deferRebuild {0}")
        fun parameters() = arrayOf(true, false)
    }

    private lateinit var mapper: OverlayReferenceMapper

    @JvmField
    @Parameterized.Parameter(0)
    var deferRebuild = false

    @Before
    fun initMapper() {
        mapper = mapper()
    }

    @Test
    fun targetWithOverlay() {
        val target = mockTarget()
        val overlay = mockOverlay()
        val existing = mapper.addInOrder(overlay)
        assertEmpty()
        mapper.addInOrder(target, existing = existing)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay))
        mapper.remove(target)
        assertEmpty()
    }

    @Test
    fun targetWithMultipleOverlays() {
        val target = mockTarget()
        val overlay0 = mockOverlay(0)
        val overlay1 = mockOverlay(1)
        mapper = mapper(
                overlayToTargetToOverlayables = mapOf(
                        overlay0.packageName to mapOf(
                                target.packageName to target.overlayables.keys
                        ),
                        overlay1.packageName to mapOf(
                                target.packageName to target.overlayables.keys
                        )
                )
        )
        val existing = mapper.addInOrder(overlay0, overlay1)
        assertEmpty()
        mapper.addInOrder(target, existing = existing)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay0, overlay1))
        mapper.remove(overlay0)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay1))
        mapper.remove(target)
        assertEmpty()
    }

    @Test
    fun targetWithoutOverlay() {
        val target = mockTarget()
        mapper.addInOrder(target)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target))
        mapper.remove(target)
        assertEmpty()
    }

    @Test
    fun overlayWithTarget() {
        val target = mockTarget()
        val overlay = mockOverlay()
        val existing = mapper.addInOrder(target)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target))
        mapper.addInOrder(overlay, existing = existing)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target, overlay))
        mapper.remove(overlay)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target))
    }

    @Test
    fun overlayWithMultipleTargets() {
        val target0 = mockTarget(0)
        val target1 = mockTarget(1)
        val overlay = mockOverlay()
        mapper = mapper(
                overlayToTargetToOverlayables = mapOf(
                        overlay.packageName to mapOf(
                                target0.packageName to target0.overlayables.keys,
                                target1.packageName to target1.overlayables.keys
                        )
                )
        )
        mapper.addInOrder(target0, target1, overlay)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target0, target1, overlay))
        mapper.remove(target0)
        assertMapping(ACTOR_PACKAGE_NAME to setOf(target1, overlay))
        mapper.remove(target1)
        assertEmpty()
    }

    @Test
    fun overlayWithoutTarget() {
        val overlay = mockOverlay()
        mapper.addInOrder(overlay)
        // An overlay can only have visibility exposed through its target
        assertEmpty()
        mapper.remove(overlay)
        assertEmpty()
    }

    private fun OverlayReferenceMapper.addInOrder(
        vararg pkgs: AndroidPackage,
        existing: MutableMap<String, AndroidPackage> = mutableMapOf()
    ) = pkgs.fold(existing) { map, pkg ->
        addPkg(pkg, map)
        map[pkg.packageName] = pkg
        return@fold map
    }

    private fun OverlayReferenceMapper.remove(pkg: AndroidPackage) = removePkg(pkg.packageName)

    private fun assertMapping(vararg pairs: Pair<String, Set<AndroidPackage>>) {
        val expected = pairs.associate { it }
                .mapValues { pair -> pair.value.map { it.packageName }.toSet() }

        // This validates the API exposed for querying the relationships
        expected.forEach { (actorPkgName, expectedPkgNames) ->
            expectedPkgNames.forEach { expectedPkgName ->
                if (deferRebuild) {
                    assertThrows(IllegalStateException::class.java) {
                        mapper.isValidActor(expectedPkgName, actorPkgName)
                    }
                    mapper.rebuildIfDeferred()
                    deferRebuild = false
                }

                assertThat(mapper.isValidActor(expectedPkgName, actorPkgName)).isTrue()
            }
        }

        // This asserts no other relationships are defined besides those tested above
        assertThat(mapper.actorPkgToPkgs).containsExactlyEntriesIn(expected)
    }

    private fun assertEmpty() = assertMapping()

    private fun mapper(
        namedActors: Map<String, Map<String, String>> = Uri.parse(ACTOR_NAME).run {
            mapOf(authority!! to mapOf(pathSegments.first() to ACTOR_PACKAGE_NAME))
        },
        overlayToTargetToOverlayables: Map<String, Map<String, Set<String>>> = mapOf(
                mockOverlay().packageName to mapOf(
                        mockTarget().run { packageName to overlayables.keys }
                )
        )
    ) = OverlayReferenceMapper(deferRebuild, object : OverlayReferenceMapper.Provider {
        override fun getActorPkg(actor: String?) =
                OverlayActorEnforcer.getPackageNameForActor(actor, namedActors).first

        override fun getTargetToOverlayables(pkg: AndroidPackage) =
                overlayToTargetToOverlayables[pkg.packageName] ?: emptyMap()
    })

    private fun mockTarget(increment: Int = 0) = mockThrowOnUnmocked<AndroidPackage> {
        whenever(packageName) { "$TARGET_PACKAGE_NAME$increment" }
        whenever(overlayables) { mapOf("overlayableName$increment" to ACTOR_NAME) }
        whenever(toString()) { "Package{$packageName}" }
    }

    private fun mockOverlay(increment: Int = 0) = mockThrowOnUnmocked<AndroidPackage> {
        whenever(packageName) { "$OVERLAY_PACKAGE_NAME$increment" }
        whenever(overlayables) { emptyMap<String, String>() }
        whenever(toString()) { "Package{$packageName}" }
    }
}
