/*
 * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.analysis.low.level.api.fir.caches

import org.jetbrains.kotlin.analysis.low.level.api.fir.caches.cleanable.CleanableWeakValueReferenceCache
import org.jetbrains.kotlinx.lincheck.RandomProvider
import org.jetbrains.kotlinx.lincheck.annotations.Operation
import org.jetbrains.kotlinx.lincheck.annotations.Param
import org.jetbrains.kotlinx.lincheck.check
import org.jetbrains.kotlinx.lincheck.paramgen.ParameterGenerator
import org.jetbrains.kotlinx.lincheck.paramgen.StringGen
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions
import org.jetbrains.kotlinx.lincheck.strategy.stress.StressOptions
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.io.Serializable

/**
 * Verifies the linearizability of [CleanableWeakValueReferenceCache]. As the weak value cache only differs from
 * [CleanableSoftValueReferenceCache][org.jetbrains.kotlin.analysis.low.level.api.fir.caches.cleanable.CleanableSoftValueReferenceCache] in
 * the choice of reference and Lincheck tests can be expensive, we avoid a duplicate test for the soft value cache.
 *
 * While this test cannot rely on garbage collector and reference queue semantics to test cleanup after garbage collection, it still
 * verifies that values are cleaned up after they have been removed with [remove] and [put]. We can't check the same for the compute
 * functions as they don't return the removed value.
 *
 * Also, we cannot test that operations returning a new value always return values which haven't been cleaned up yet, because the value
 * might be cleaned up by another thread between the return point of the operation and cleanup checking.
 *
 * Various functions of the cache are not checked by Lincheck:
 *
 * - [CleanableWeakValueReferenceCache.clear] is not checked because it must be executed in a write action, which guarantees
 *   single-threadedness.
 * - [CleanableWeakValueReferenceCache.size] and [CleanableWeakValueReferenceCache.isEmpty] are not checked because the underlying
 *   `ConcurrentHashMap`'s implementation of these properties isn't guaranteed to immediately take effect after map operations (see the
 *   `ConcurrentHashMap` section in the book "Java Concurrency in Practice").
 * - [CleanableWeakValueReferenceCache.keys] is not checked for linearizability because it is only weakly consistent via the underlying
 *   concurrent hash map implementation.
 */
@Param(name = "value", gen = ValueWithCleanupGenerator::class)
@Disabled("KT-75018 Analysis API: Flaky failures in `CleanableWeakValueReferenceCacheLincheckTest`")
class CleanableWeakValueReferenceCacheLincheckTest {
    private val cache = CleanableWeakValueReferenceCache<Int, ValueWithCleanup> { it.cleanupMarker }

    @Operation
    fun get(key: Int): ValueWithCleanup? = cache[key]

    @Operation
    fun computeIfAbsent(
        key: Int,
        @Param(name = "value") value: ValueWithCleanup,
    ): ValueWithCleanup = cache.computeIfAbsent(key) { value }

    /**
     * Models a computation that replaces the cache's existing value for [key] with [newValue].
     */
    @Operation
    fun computeReplaceValue(
        key: Int,
        @Param(name = "value") newValue: ValueWithCleanup,
    ): ValueWithCleanup? = cache.compute(key) { _, _ -> newValue }

    /**
     * Models a computation that keeps the cache's existing value for [key].
     */
    @Operation
    fun computeKeepValue(key: Int): ValueWithCleanup? = cache.compute(key) { _, existingValue -> existingValue }

    /**
     * Models a computation that removes the cache's existing value for [key] (if any).
     */
    @Operation
    fun computeRemoveValue(key: Int): ValueWithCleanup? = cache.compute(key) { _, _ -> null }

    @Operation
    fun put(
        key: Int,
        @Param(name = "value") value: ValueWithCleanup,
    ): RemovalResult? = withRemovalResult { cache.put(key, value) }

    @Operation
    fun remove(key: Int): RemovalResult? = withRemovalResult { cache.remove(key) }

    /**
     * [RemovalResult] makes the cleanup state of [value] explicit, allowing Lincheck to detect differences between the cleanup state of a
     * single-threaded and concurrent execution.
     *
     * [CleanableValueReferenceCacheTest] separately ensures that deterministic cleanup behaves correctly in non-concurrent scenarios.
     *
     * This class implements [Serializable] so that it can be contained in scenarios generated by Lincheck.
     */
    data class RemovalResult(
        val value: ValueWithCleanup,
        val isCleanedUp: Boolean,
    ) : Serializable

    private fun withRemovalResult(operation: () -> ValueWithCleanup?): RemovalResult? {
        val value = operation() ?: return null
        return RemovalResult(value, value.isCleanedUp)
    }

    @Test
    fun modelCheckingTest() = ModelCheckingOptions().check(this::class)

    @Test
    fun stressTest() = StressOptions().check(this::class)
}

class ValueWithCleanupGenerator(randomProvider: RandomProvider, configuration: String) : ParameterGenerator<ValueWithCleanup> {
    private val stringGenerator: StringGen = StringGen(randomProvider, "")

    override fun generate(): ValueWithCleanup = ValueWithCleanup(stringGenerator.generate())
}
