
/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

@file:Suppress("NAMED_ARGUMENTS_NOT_ALLOWED") // KT-21913

package kotlinx.coroutines

import kotlinx.coroutines.channels.*
import kotlin.test.*

class WithTimeoutOrNullTest : TestBase() {
    /**
     * Tests a case of no timeout and no suspension inside.
     */
    @Test
    fun testBasicNoSuspend() = runTest {
        expect(1)
        val result = withTimeoutOrNull(10_000) {
            expect(2)
            "OK"
        }
        assertEquals("OK", result)
        finish(3)
    }

    /**
     * Tests a case of no timeout and one suspension inside.
     */
    @Test
    fun testBasicSuspend() = runTest {
        expect(1)
        val result = withTimeoutOrNull(10_000) {
            expect(2)
            yield()
            expect(3)
            "OK"
        }
        assertEquals("OK", result)
        finish(4)
    }

    /**
     * Tests property dispatching of `withTimeoutOrNull` blocks
     */
    @Test
    fun testDispatch() = runTest {
        expect(1)
        launch {
            expect(4)
            yield() // back to main
            expect(7)
        }
        expect(2)
        // test that it does not yield to the above job when started
        val result = withTimeoutOrNull(1000) {
            expect(3)
            yield() // yield only now
            expect(5)
            "OK"
        }
        assertEquals("OK", result)
        expect(6)
        yield() // back to launch
        finish(8)
    }

    /**
     * Tests that a 100% CPU-consuming loop will react on timeout if it has yields.
     */
    @Test
    fun testYieldBlockingWithTimeout() = runTest {
        expect(1)
        val result = withTimeoutOrNull(100) {
            while (true) {
                yield()
            }
        }
        assertEquals(null, result)
        finish(2)
    }

    @Test
    fun testSmallTimeout() = runTest {
        val channel = Channel<Int>(1)
        val value = withTimeoutOrNull(1) {
            channel.receive()
        }
        assertNull(value)
    }

    @Test
    fun testThrowException() = runTest(expected = {it is AssertionError}) {
        withTimeoutOrNull(Long.MAX_VALUE) {
            throw AssertionError()
        }
    }

    @Test
    fun testInnerTimeout() = runTest(
        expected = { it is CancellationException }
    ) {
        withTimeoutOrNull(1000) {
            withTimeout(10) {
                while (true) {
                    yield()
                }
            }
            expectUnreached() // will timeout
        }
        expectUnreached() // will timeout
    }

    @Test
    fun testNestedTimeout() = runTest(expected = { it is TimeoutCancellationException }) {
        withTimeoutOrNull(Long.MAX_VALUE) {
            // Exception from this withTimeout is not suppressed by withTimeoutOrNull
            withTimeout(10) {
                delay(Long.MAX_VALUE)
                1
            }
        }

        expectUnreached()
    }

    @Test
    fun testOuterTimeout() = runTest {
        var counter = 0
        val result = withTimeoutOrNull(250) {
            while (true) {
                val inner = withTimeoutOrNull(100) {
                    while (true) {
                        yield()
                    }
                }
                assertEquals(null, inner)
                counter++
            }
        }
        assertEquals(null, result)
        check(counter in 1..2) {"Executed: $counter times"}
    }

    @Test
    fun testBadClass() = runTest {
        val bad = BadClass()
        val result = withTimeoutOrNull(100) {
            bad
        }
        assertSame(bad, result)
    }

    class BadClass {
        override fun equals(other: Any?): Boolean = error("Should not be called")
        override fun hashCode(): Int = error("Should not be called")
        override fun toString(): String = error("Should not be called")
    }

    @Test
    fun testNullOnTimeout() = runTest {
        expect(1)
        val result = withTimeoutOrNull(100) {
            expect(2)
            delay(1000)
            expectUnreached()
            "OK"
        }
        assertEquals(null, result)
        finish(3)
    }

    @Test
    fun testSuppressExceptionWithResult() = runTest {
        expect(1)
        val result = withTimeoutOrNull(100) {
            expect(2)
            try {
                delay(1000)
            } catch (e: CancellationException) {
                expect(3)
            }
            "OK"
        }
        assertEquals(null, result)
        finish(4)
    }

    @Test
    fun testSuppressExceptionWithAnotherException() = runTest {
        expect(1)
        try {
            withTimeoutOrNull(100) {
                expect(2)
                try {
                    delay(1000)
                } catch (e: CancellationException) {
                    expect(3)
                    throw TestException()
                }
                expectUnreached()
                "OK"
            }
            expectUnreached()
        } catch (e: TestException) {
            // catches TestException
            finish(4)

        }
    }

    @Test
    fun testNegativeTimeout() = runTest {
        expect(1)
        var result = withTimeoutOrNull(-1) {
            expectUnreached()
        }
        assertNull(result)
        result = withTimeoutOrNull(0) {
            expectUnreached()
        }
        assertNull(result)
        finish(2)
    }

    @Test
    fun testExceptionFromWithinTimeout() = runTest {
        expect(1)
        try {
            expect(2)
            withTimeoutOrNull(1000) {
                expect(3)
                throw TestException()
            }
            expectUnreached()
        } catch (e: TestException) {
            finish(4)
        }
    }
}
