/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.
 */

import org.junit.jupiter.api.Test

import static groovy.test.GroovyAssert.assertScript

final class AsyncAwaitSpecTest {

    // === Getting started ===

    @Test
    void testBasicAsyncAwait() {
        assertScript '''
        // tag::basic_async_await[]
        def task = async { 21 * 2 }
        assert await(task) == 42
        // end::basic_async_await[]
        '''
    }

    @Test
    void testDrawCard() {
        assertScript '''
        // tag::draw_card[]
        def deck = ['2♠', '3♥', 'K♦', 'A♣']
        def card = async { deck.shuffled()[0] }
        println "You drew: ${await card}"
        // end::draw_card[]
        '''
    }

    @Test
    void testExceptionHandling() {
        assertScript '''
        // tag::exception_handling[]
        def drawFromEmpty = async {
            throw new IllegalStateException('deck is empty')
        }
        try {
            await drawFromEmpty
        } catch (IllegalStateException e) {
            // Original exception — no CompletionException wrapper
            assert e.message == 'deck is empty'
        }
        // end::exception_handling[]
        '''
    }

    @Test
    void testCfInterop() {
        assertScript '''
        import java.util.concurrent.CompletableFuture

        // tag::cf_interop[]
        // await works with CompletableFuture from Java libraries
        def future = CompletableFuture.supplyAsync { 'A♠' }
        assert await(future) == 'A♠'
        // end::cf_interop[]
        '''
    }

    // === Parallel tasks and combinators ===

    @Test
    void testDealHands() {
        assertScript '''
        import groovy.concurrent.Awaitable
        import java.util.concurrent.atomic.AtomicInteger

        // tag::deal_hands[]
        // Deal cards to three players concurrently
        def deck = [*1..9,'J','Q','K','A'].collectMany { rank ->
            ['♠','♥','♦','♣'].collect { suit -> "$rank$suit" }
        }.shuffled()
        def index = new AtomicInteger()
        def draw5 = { -> def i = index.getAndAdd(5); deck[i..i+4] }

        def alice = async { draw5() }
        def bob   = async { draw5() }
        def carol = async { draw5() }

        def (a, b, c) = await Awaitable.all(alice, bob, carol)
        assert a.size() == 5 && b.size() == 5 && c.size() == 5
        // end::deal_hands[]
        '''
    }

    @Test
    void testMultiArgAwait() {
        assertScript '''
        // tag::multi_arg_await[]
        def a = async { 1 }
        def b = async { 2 }
        def c = async { 3 }

        // Parenthesized multi-arg await (sugar for Awaitable.all):
        def results = await(a, b, c)
        assert results == [1, 2, 3]
        // end::multi_arg_await[]
        '''
    }

    @Test
    void testFastestServer() {
        assertScript '''
        import groovy.concurrent.Awaitable

        // tag::fastest_server[]
        // Race two servers — use whichever responds first
        def primary  = async { Thread.sleep(200); 'primary-response' }
        def fallback = async { 'fallback-response' }

        def response = await Awaitable.any(primary, fallback)
        assert response == 'fallback-response'
        // end::fastest_server[]
        '''
    }

    @Test
    void testFirstSuccess() {
        assertScript '''
        import groovy.concurrent.Awaitable

        // tag::first_success[]
        // Try multiple sources — use the first that succeeds
        def failing    = async { throw new RuntimeException('server down') }
        def succeeding = async { 'card-data-from-cache' }

        def result = await Awaitable.first(failing, succeeding)
        assert result == 'card-data-from-cache'
        // end::first_success[]
        '''
    }

    @Test
    void testAllSettled() {
        assertScript '''
        import groovy.concurrent.Awaitable

        // tag::all_settled[]
        def save1 = async { 42 }
        def save2 = async { throw new RuntimeException('db error') }

        def results = await Awaitable.allSettled(save1, save2)
        assert results[0].success && results[0].value == 42
        assert !results[1].success && results[1].error.message == 'db error'
        // end::all_settled[]
        '''
    }

    // === Generators and for await ===

    @Test
    void testDeckGenerator() {
        assertScript '''
        // tag::deck_generator[]
        def dealCards = async {
            def suits = ['♠', '♥', '♦', '♣']
            def ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
            for (suit in suits) {
                for (rank in ranks) {
                    yield return "$rank$suit"
                }
            }
        }
        def cards = dealCards.collect()
        assert cards.size() == 52
        assert cards.first() == 'A♠'
        assert cards.last() == 'K♣'
        // end::deck_generator[]
        '''
    }

    @Test
    void testForAwaitGenerator() {
        assertScript '''
        // tag::for_await_generator[]
        def topCards = async {
            for (card in ['A♠', 'K♥', 'Q♦']) {
                yield return card
            }
        }
        def hand = []
        for await (card in topCards) {
            hand << card
        }
        assert hand == ['A♠', 'K♥', 'Q♦']
        // end::for_await_generator[]
        '''
    }

    @Test
    void testForAwaitPlainCollection() {
        assertScript '''
        // tag::for_await_collection[]
        def results = []
        for await (card in ['A♠', 'K♥', 'Q♦']) {
            results << card
        }
        assert results == ['A♠', 'K♥', 'Q♦']
        // end::for_await_collection[]
        '''
    }

    @Test
    void testGeneratorRegularFor() {
        assertScript '''
        // tag::generator_regular_for[]
        def scores = async {
            for (s in [100, 250, 75]) { yield return s }
        }
        // Generators return Iterable — regular for and collect work
        assert scores.collect { it * 2 } == [200, 500, 150]
        // end::generator_regular_for[]
        '''
    }

    // === Channels ===

    @Test
    void testChannel() {
        assertScript '''
        import groovy.concurrent.AsyncChannel

        // tag::channel[]
        def cardStream = AsyncChannel.create(3)

        // Dealer — sends cards concurrently
        async {
            for (card in ['A♠', 'K♥', 'Q♦', 'J♣']) {
                await cardStream.send(card)
            }
            cardStream.close()
        }

        // Player — receives cards as they arrive
        def hand = []
        for await (card in cardStream) {
            hand << card
        }
        assert hand == ['A♠', 'K♥', 'Q♦', 'J♣']
        // end::channel[]
        '''
    }

    // === Defer ===

    @Test
    void testDeferBasic() {
        assertScript '''
        // tag::defer_basic[]
        def log = []
        def task = async {
            log << 'open connection'
            defer { log << 'close connection' }
            log << 'open transaction'
            defer { log << 'close transaction' }
            log << 'save game state'
            'saved'
        }
        assert await(task) == 'saved'
        // Deferred actions run in LIFO order — last registered, first to run
        assert log == ['open connection', 'open transaction', 'save game state',
                       'close transaction', 'close connection']
        // end::defer_basic[]
        '''
    }

    @Test
    void testDeferOnException() {
        assertScript '''
        // tag::defer_exception[]
        def cleaned = false
        def task = async {
            defer { cleaned = true }
            throw new RuntimeException('save failed')
        }
        try {
            await task
        } catch (RuntimeException e) {
            assert e.message == 'save failed'
        }
        // Deferred actions run even when an exception occurs
        assert cleaned
        // end::defer_exception[]
        '''
    }

    // === Structured concurrency ===

    @Test
    void testTournamentScope() {
        assertScript '''
        import groovy.concurrent.AsyncScope

        // tag::structured_concurrency[]
        // Run a tournament round — all tables play concurrently
        def results = AsyncScope.withScope { scope ->
            def table1 = scope.async { [winner: 'Alice',  score: 320] }
            def table2 = scope.async { [winner: 'Bob',    score: 280] }
            def table3 = scope.async { [winner: 'Carol',  score: 410] }
            [await(table1), await(table2), await(table3)]
        }
        // All tables guaranteed complete when withScope returns
        assert results.size() == 3
        assert results.max { it.score }.winner == 'Carol'
        // end::structured_concurrency[]
        '''
    }

    @Test
    void testScopeFailFast() {
        assertScript '''
        import groovy.concurrent.AsyncScope

        // tag::scope_fail_fast[]
        try {
            AsyncScope.withScope { scope ->
                scope.async { Thread.sleep(5000); 'still playing' }
                scope.async { throw new RuntimeException('player disconnected') }
            }
        } catch (RuntimeException e) {
            // First failure cancels all siblings and propagates
            assert e.message == 'player disconnected'
        }
        // end::scope_fail_fast[]
        '''
    }

    @Test
    void testScopeWaitsForAll() {
        assertScript '''
        import groovy.concurrent.AsyncScope
        import java.util.concurrent.atomic.AtomicInteger

        // tag::scope_waits[]
        def completed = new AtomicInteger(0)
        AsyncScope.withScope { scope ->
            3.times { scope.async { Thread.sleep(50); completed.incrementAndGet() } }
        }
        // All children have completed — even without explicit await
        assert completed.get() == 3
        // end::scope_waits[]
        '''
    }

    // === Timeouts and delays ===

    @Test
    void testTimeout() {
        assertScript '''
        import groovy.concurrent.Awaitable
        import java.util.concurrent.TimeoutException

        // tag::timeout[]
        def slowPlayer = async { Thread.sleep(5000); 'finally played' }
        try {
            await Awaitable.orTimeoutMillis(slowPlayer, 100)
        } catch (TimeoutException e) {
            // Player took too long — turn forfeited
            assert true
        }
        // end::timeout[]
        '''
    }

    @Test
    void testTimeoutFallback() {
        assertScript '''
        import groovy.concurrent.Awaitable

        // tag::timeout_fallback[]
        def slowPlayer = async { Thread.sleep(5000); 'deliberate move' }
        def move = await Awaitable.completeOnTimeoutMillis(slowPlayer, 'auto-pass', 100)
        assert move == 'auto-pass'
        // end::timeout_fallback[]
        '''
    }

    @Test
    void testDelay() {
        assertScript '''
        import groovy.concurrent.Awaitable

        // tag::delay[]
        long start = System.currentTimeMillis()
        await Awaitable.delay(100)   // pause without blocking a thread
        assert System.currentTimeMillis() - start >= 90
        // end::delay[]
        '''
    }

    // === JDK Integration ===

    @Test
    void testJdkIntegration() {
        assertScript '''
        import com.sun.net.httpserver.HttpServer
        import groovy.http.HttpBuilder
        import java.nio.charset.StandardCharsets
        import java.nio.file.Files

        // setup: temp file containing a password
        def etcPassword = Files.createTempFile('passwords', '.txt')
        Files.writeString(etcPassword, 'admin:sEcrEt\\nroot:P4ssw0rd')

        // setup: fake bank HTTP server
        def server = HttpServer.create(new InetSocketAddress('127.0.0.1', 0), 0)
        server.createContext('/') { exchange ->
            def body = '<html>SUCCESS</html>'
            byte[] bytes = body.getBytes(StandardCharsets.UTF_8)
            exchange.responseHeaders.add('Content-Type', 'text/html')
            exchange.sendResponseHeaders(200, bytes.length)
            exchange.responseBody.withCloseable { it.write(bytes) }
        }
        server.start()
        def bankUrl = "http://127.0.0.1:${server.address.port}"

        // setup: portable echo command (Windows has no echo executable — it's a cmd built-in)
        def echoCmd = System.getProperty('os.name').toLowerCase().startsWith('windows')
            ? ['cmd', '/c', 'echo Hello from Groovy']
            : ['echo', 'Hello from Groovy']

        try {
        // tag::jdk_integration[]
        // Task 1: Run a process and await its completion
        def proc = async {
            def p = echoCmd.execute() // echo Hello from Groovy
            await p.onExit()
            p.text.trim()
        }

        // Task 2: Read a file asynchronously
        def fileContent = async {
            await etcPassword.textAsync
        }

        // Task 3: Fetch a web page using Groovy's HttpBuilder
        def webContent = async {
            def http = HttpBuilder.http { baseUri bankUrl }
            http.get('/withdraw/100').body
        }

        // All three tasks run concurrently — collect results
        var (echo, file, html) = await proc, fileContent, webContent
        assert echo == 'Hello from Groovy'
        assert file =~ /sEcrEt/
        assert html == '<html>SUCCESS</html>'
        // end::jdk_integration[]
        } finally {
            server.stop(0)
            Files.delete(etcPassword)
        }
        '''
    }

    // === Async closure factories ===

    @Test
    void testClosureFactory() {
        assertScript '''
        import groovy.concurrent.Awaitable

        // tag::closure_factory[]
        // A method that returns an async task — a simple factory
        def dealCard(List deck) {
            async { deck.shuffled()[0] }
        }

        def numPlayers = 3
        // each player gets a card from their own deck
        def cards = (1..numPlayers).collect { dealCard(['A♠', 'K♥', 'Q♦', 'J♣']) }

        def hands = await Awaitable.all(*cards)
        assert hands.size() == 3
        assert hands.every { it in ['A♠', 'K♥', 'Q♦', 'J♣'] }
        // end::closure_factory[]
        '''
    }

    @Test
    void testAwaitCompletableFutureNoAmbiguity() {
        // Verifies that await works directly with HttpClient.sendAsync()
        // which returns a CompletableFuture (implements both CompletionStage and Future)
        assertScript '''
        import com.sun.net.httpserver.HttpServer
        import java.nio.charset.StandardCharsets
        import java.net.http.HttpClient
        import java.net.http.HttpRequest
        import java.net.http.HttpResponse

        def server = HttpServer.create(new InetSocketAddress('127.0.0.1', 0), 0)
        server.createContext('/') { exchange ->
            byte[] bytes = 'OK'.getBytes(StandardCharsets.UTF_8)
            exchange.sendResponseHeaders(200, bytes.length)
            exchange.responseBody.withCloseable { it.write(bytes) }
        }
        server.start()
        def url = "http://127.0.0.1:${server.address.port}"

        try {
            def result = async {
                def client = HttpClient.newHttpClient()
                def req = HttpRequest.newBuilder().uri(URI.create(url)).build()
                def resp = await client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
                resp.body()
            }
            assert await(result) == 'OK'
        } finally {
            server.stop(0)
        }
        '''
    }
}
