/*
 * 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 test.native.concurrent

import kotlin.concurrent.AtomicInt
import kotlin.native.concurrent.*
import kotlin.test.*

class WorkerTest {
    @Test
    fun execute() = withWorker {
        val future = execute(TransferMode.SAFE, { "Input" }) { "$it processed" }
        assertEquals("Input processed", future.result)
    }

    class MyError : Exception("My error")

    @Test
    fun executeWithException() = withWorker(errorReporting = false) {
        val future = execute(TransferMode.SAFE, {}) { throw MyError() }
        // Not `MyError`.
        assertFailsWith<IllegalStateException> { future.result }
        assertEquals("Still working", execute(TransferMode.SAFE, { "Still" }) { "$it working" }.result)
    }

    @Suppress("DEPRECATION_ERROR") // Freezing API
    @Test
    fun executeWithDetachedObjectGraph() = withWorker {
        data class SharedDataMember(val double: Double)
        data class SharedData(val string: String, val int: Int, val member: SharedDataMember)
        // Here we do rather strange thing. To test object detach API we detach object graph,
        // pass detached graph to a worker, where we manually reattached passed value.
        val future = execute(TransferMode.SAFE, {
            DetachedObjectGraph { SharedData("Hello", 10, SharedDataMember(0.1)) }.asCPointer()
        }) {
            DetachedObjectGraph<SharedData>(it).attach()
        }
        assertEquals(SharedData("Hello", 10, SharedDataMember(double = 0.1)), future.result)
    }

    @Test
    fun executeOnWrongThread() {
        val worker = Worker.start()
        worker.requestTermination().result

        val exception = assertFailsWith<IllegalStateException> {
            worker.execute(TransferMode.SAFE, {}, {}).result
        }
        assertEquals("Worker is already terminated", exception.message)
    }

    @Test
    fun waitForMultipleFutures() {
        val workers = Array(5) { Worker.start() }

        val futures = (1..3).flatMap { attempt ->
            workers.mapIndexed { index, worker ->
                worker.execute(TransferMode.SAFE, { "$attempt: Input $index" }) { "$it processed" }
            }
        }

        val actual = waitForMultipleFutures(futures, 10000)

        val expected = (1..3).flatMap { attempt ->
            (0 until 5).map { index -> "$attempt: Input $index processed" }
        }.toSet()

        // actual cannot be empty.
        assertTrue(actual.isNotEmpty())
        // Everything in actual must be in expected.
        // The reverse is not required to be true: waitForMultipleFutures may return when
        // only some futures have completed.
        actual.forEach { future ->
            // Every actual future is also computed.
            assertEquals(FutureState.COMPUTED, future.state)
            assertTrue(expected.contains(future.result))
        }

        workers.forEach {
            it.requestTermination().result
        }
    }

    @Test
    fun executeWithConcurrentArrayModification() {
        val workers = Array(100) { Worker.start() }
        val array = Array(workers.size) { it }
        val futures = workers.mapIndexed { index, worker ->
            worker.execute(TransferMode.SAFE, { array to index }) { (array, index) ->
                array[index] += index
            }
        }

        while (waitForMultipleFutures(futures, 10000).size < futures.size) {
        }

        array.forEachIndexed { index, value ->
            assertEquals(index * 2, value)
        }

        workers.forEach {
            it.requestTermination().result
        }
    }

    @Test
    fun executeAfter() = withWorker {
        val counter = AtomicInt(0)

        executeAfter(0) {
            assertTrue(Worker.current.park(10_000_000, false))
            assertEquals(counter.value, 0)
            assertTrue(Worker.current.processQueue())
            assertEquals(1, counter.value)
            // Let main proceed.
            counter.incrementAndGet()  // counter becomes 2 here.
            assertTrue(Worker.current.park(10_000_000, true))
            assertEquals(3, counter.value)
        }

        executeAfter(0) {
            counter.incrementAndGet()
            Unit
        }

        while (counter.value < 2) {
            Worker.current.park(1_000)
        }

        executeAfter(0) {
            counter.incrementAndGet()
            Unit
        }

        while (counter.value == 2) {
            Worker.current.park(1_000)
        }
    }

    @Test
    fun executeAfterNegativeDelay() = withWorker {
        assertFailsWith<IllegalArgumentException> { executeAfter(-1) {} }
        Unit
    }

    @Test
    fun executeAfterModify() = withWorker {
        var v = 1
        val done = AtomicInt(0)
        executeAfter(0) {
            v++
            assertEquals(2, v)
            done.value = 1
        }
        while (done.value == 0) {
        }
        assertEquals(2, v)
    }

    @Test
    fun executeAfterOrdering() = withWorker {
        val counter = AtomicInt(0)
        val lastTask = AtomicInt(0)
        executeAfter(500_000) {
            lastTask.value = 1
            counter.incrementAndGet()
        }
        executeAfter(1_000) {
            lastTask.value = 2
            counter.incrementAndGet()
        }
        // Wait for both tasks to complete.
        while (counter.value != 2) {
        }
        // Task with id 1 was scheduled to execute later, so it has won.
        assertEquals(1, lastTask.value)
    }

    @Test
    fun terminateDuringExecute() {
        val worker = Worker.start()

        val stage = AtomicInt(0)

        val duringTerminationFuture = worker.execute(TransferMode.SAFE, { stage }) { stage ->
            // Signal, that the first job is started
            stage.value = 1
            // Wait for the termination request
            while (stage.value != 2) {}
            // Here we processed termination request.
            assertEquals(false, Worker.current.processQueue())
            // The job will complete successfully
            42
        }

        // Wait until we start executing the first job, so no other queued jobs start working
        while (stage.value != 1) {}

        // Pass `stage` to make sure there's some non-trivial state attached to these jobs
        val afterTerminationFuture = worker.execute(TransferMode.SAFE, { stage }) { stage ->
            assertEquals(2, stage.value)
            error("FAILURE")
        }
        worker.executeAfter(0L) {
            assertEquals(2, stage.value)
            error("FAILURE")
        }
        worker.executeAfter(1_000_000_000L) {
            assertEquals(2, stage.value)
            error("FAILURE")
        }

        // Request worker to terminate, and move that job to the beginning of the queue.
        // The first job will then process this job in `processQueue`
        val terminationRequest = worker.requestTermination(processScheduledJobs = false)
        // Allow the first job to process the termination request.
        stage.value = 2

        // Now wait until `processQueue` processes termination request.
        terminationRequest.result

        // And now wait for the worker to complete termination, cleaning up after itself.
        // Worker would have finished the first job and cancelled all others.
        waitWorkerTermination(worker)

        assertEquals(42, duringTerminationFuture.result)
        // This job was cancelled, because termination job got executed before it.
        val exception = assertFailsWith<IllegalStateException> { afterTerminationFuture.result }
        assertEquals("Future is cancelled", exception.message)
    }

    @Test
    fun cancelJobsAfterTerminationRequest() {
        val worker = Worker.start()

        val canUnblockQueue = AtomicInt(0)
        val blockQueueFuture = worker.execute(TransferMode.SAFE, { canUnblockQueue }) { canUnblockQueue ->
            while (canUnblockQueue.value == 0) {}
        }
        val terminateRequestFuture = worker.requestTermination()
        val afterTerminationFuture = worker.execute(TransferMode.SAFE, {}) {
            error("Executed job after termination")
        }

        // Now that the entire queue is ready, unblock the first task, it will be successful, and wait for the termination request.
        canUnblockQueue.value = 1
        blockQueueFuture.result
        terminateRequestFuture.result
        // And now wait for the worker to complete termination, cleaning up after itself.
        waitWorkerTermination(worker)
        val exception = assertFailsWith<IllegalStateException> { afterTerminationFuture.result }
        assertEquals("Future is cancelled", exception.message)
    }

    @Test
    fun executeAfterOnMain() {
        var done = false
        Worker.current.executeAfter(0) {
            done = true
        }
        // Not executed immediately.
        assertFalse(done)
        // The current worker's queue may be filled with other tasks, so we must loop.
        while (!done) {
            Worker.current.processQueue()
        }
    }

    // This test checks that when multiple `executeAfter` jobs are submitted to `targetWorker` and have the
    // same scheduled execution time (in micros since an epoch), nether of them gets lost.
    @Test
    fun executeAfterScheduledTimeClash() = withWorker {
        val targetWorker = this
        val mainWorker = Worker.current

        // Configuration of the test.
        val numberOfSubmitters = 2
        val numberOfTasks = 100
        val delayInMicroseconds = 100L

        val submitters = Array(numberOfSubmitters) { Worker.start() }
        try {
            val readySubmittersCounter = AtomicInt(0)
            val executedTasksCounter = AtomicInt(0)
            val finishedBatchesCounter = AtomicInt(0)

            submitters.forEach {
                it.executeAfter(0L) {
                    readySubmittersCounter.incrementAndGet()
                    // Wait for other submitters, to make them all start at the same time:
                    while (readySubmittersCounter.value != numberOfSubmitters) {
                    }

                    // Concurrently submit tasks with matching scheduled execution time:
                    repeat(numberOfTasks) {
                        targetWorker.executeAfter(delayInMicroseconds) {
                            executedTasksCounter.incrementAndGet()
                            Unit
                        }
                    }

                    // Use larger delay for the task below, to make sure it gets executed after
                    // the tasks above submitted by the same worker.
                    // If the order is wrong, the test will fail as well.
                    // NOTE: the code below was affected by the same problem with clashing times, so despite all the effort
                    // the test still might hang without a fix.
                    targetWorker.executeAfter(delayInMicroseconds + 1) {
                        mainWorker.executeAfter(0L) {
                            finishedBatchesCounter.incrementAndGet()
                            Unit
                        }
                    }
                }
            }

            while (finishedBatchesCounter.value != numberOfSubmitters) {
                // Wait and allow processing the `finishedBatchesCounter.increment()` tasks above:
                Worker.current.park(delayInMicroseconds, process = true)
            }

            // Note: we could have just waited for the condition above to become true,
            // but this would mean that the test would hang in case of failure, which is not quite convenient.

            assertEquals(numberOfSubmitters * numberOfTasks, executedTasksCounter.value)
        } finally {
            submitters.forEach { it.requestTermination().result }
        }
    }

    @Test
    fun executeAfterOnWrongThread() {
        val worker = Worker.start()
        worker.requestTermination().result

        val exception = assertFailsWith<IllegalStateException> {
            worker.executeAfter(0L) {}
        }
        assertEquals("Worker is already terminated", exception.message)
    }

    @Test
    fun withName() = withWorker(name = "Lumberjack") {
        execute(TransferMode.SAFE, {}) {
            assertEquals("Lumberjack", Worker.current.name)
        }.result
        assertEquals("Lumberjack", name)
    }

    @Test
    fun nameOnWrongThread() {
        val worker = Worker.start(name = "Lumberjack")
        worker.requestTermination().result
        val exception = assertFailsWith<IllegalStateException> {
            worker.name
        }
        assertEquals("Worker is already terminated", exception.message)
    }

    @Test
    fun park() = withWorker {
        val counter = AtomicInt(0)
        val f1 = execute(TransferMode.SAFE, { counter }) { counter ->
            Worker.current.park(Long.MAX_VALUE / 1000L, process = true)
            counter.incrementAndGet()
        }
        // wait a bit
        Worker.current.park(10_000L)
        // submit a task
        val f2 = execute(TransferMode.SAFE, { counter }) { counter ->
            counter.incrementAndGet()
        }
        f1.result
        f2.result
        assertEquals(2, counter.value)
    }

    @Test
    fun parkMain() = withWorker {
        val main = Worker.current
        val counter = AtomicInt(0)
        executeAfter(1000) {
            main.executeAfter(1) {
                counter.incrementAndGet()
                Unit
            }
        }
        assertTrue(main.park(1_000_000_000L, process = true))
        assertEquals(1, counter.value)
    }

    @Test
    fun parkOnWrongThread() = withWorker {
        val exception = assertFailsWith<IllegalStateException> {
            park(1L)
        }
        assertEquals("Worker is not current or already terminated", exception.message)
    }

    @Test
    fun parkZeroTimeout() {
        Worker.current.park(0, process = true)
    }

    @Test
    fun processQueue() = withWorker {
        val counter = AtomicInt(0)
        val future1 = execute(TransferMode.SAFE, { counter }) { counter ->
            assertEquals(0, counter.value)
            // Process following request.
            while (!Worker.current.processQueue()) {
            }
            // Ensure it has an effect.
            assertEquals(1, counter.value)
            // No more non-terminating tasks in this worker queue.
            assertEquals(false, Worker.current.processQueue())
        }
        val future2 = execute(TransferMode.SAFE, { counter }) { counter ->
            counter.incrementAndGet()
        }
        future2.result
        future1.result
    }

    @Test
    fun processQueueOnWrongThread() = withWorker {
        val exception = assertFailsWith<IllegalStateException> {
            processQueue()
        }
        assertEquals("Worker is not current or already terminated", exception.message)
    }

    @Test
    fun requestTerminationOnWrongThread() {
        val worker = Worker.start()
        worker.requestTermination().result

        val exception = assertFailsWith<IllegalStateException> {
            worker.requestTermination()
        }
        assertEquals("Worker is already terminated", exception.message)
    }

    @Test
    fun activeWorkers() {
        val workers = Array(10) { Worker.start() }

        val actualWorkers = Worker.activeWorkers.toSet()

        assertTrue(actualWorkers.size - workers.size == 1 || actualWorkers.size - workers.size == 2,
                "actualWorkers.size = ${actualWorkers.size} workers.size = ${workers.size} actual size must be greater by 1 (main worker) or 2 (cleaners worker)")
        workers.forEach {
            actualWorkers.contains(it)
        }
        actualWorkers.contains(Worker.current)

        val terminatedWorkers = mutableSetOf<Worker>()
        (workers.indices step 2).forEach {
            val worker = workers[it]
            worker.requestTermination().result
            terminatedWorkers.add(worker)
        }

        val actualWorkersAfterTermination = Worker.activeWorkers.toSet()
        assertEquals(terminatedWorkers, actualWorkers - actualWorkersAfterTermination)

        (workers.indices step 2).forEach {
            workers[it + 1].requestTermination().result
        }
    }

    @Test
    fun futureConsumedTwice(): Unit = withWorker {
        val future = execute(TransferMode.SAFE, {}) {
            42
        }
        assertEquals(42, future.result)
        val exception = assertFailsWith<IllegalStateException> {
            future.result
        }
        assertEquals("Future is in an invalid state", exception.message)
    }
}
