/*
 *  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.
 */
package org.apache.causeway.regressiontests.core.wrapperfactory.integtests;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

import jakarta.inject.Inject;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import static org.assertj.core.api.Assertions.assertThat;

import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.bookmark.BookmarkService;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
import org.apache.causeway.applib.services.wrapper.control.AsyncControl;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.testdomain.wrapperfactory.Counter;
import org.apache.causeway.testdomain.wrapperfactory.Counter_bumpUsingMixin;

import lombok.SneakyThrows;

class WrapperFactory_async_IntegTest extends CoreWrapperFactory_IntegTestAbstract {

    @Inject WrapperFactory wrapperFactory;
    @Inject TransactionService transactionService;
    @Inject BookmarkService bookmarkService;

    Bookmark bookmark;

    void setup_counter() {

        runWithNewTransaction(() -> {
            counterRepository.persist(newCounter("fred"));
            List<Counter> counters = counterRepository.find();
            assertThat(counters).hasSize(1);

            bookmark = bookmarkService.bookmarkForElseFail(counters.get(0));
        });

        // given
        assertThat(bookmark).isNotNull();

        var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
        assertThat(counter.getNum()).isNull();
    }


    @SneakyThrows
    @ParameterizedTest(name = "executorService[{index}]: {0}")
    @MethodSource("executorServices")
    void async_using_default_executor_service(final String displayName, final ExecutorService executorService) {

        setup_counter();

        final AtomicReference<AsyncProxy<Counter>> asyncProxyUnderTest1 = new AtomicReference<>();
        final AtomicReference<AsyncProxy<Counter_bumpUsingMixin>> asyncProxyUnderTest2 = new AtomicReference<>();

        // when - executing regular action
        runWithNewTransaction(() -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();

            var asyncControl = AsyncControl.defaults()
                    .with(executorService);

            // store the async proxy for later use below
            asyncProxyUnderTest1.set(wrapperFactory.asyncWrap(counter, asyncControl));
        });

        // execute async and wait till done
        {
            asyncProxyUnderTest1.get()
                .applyAsync(Counter::increment)
                // let's wait max 5 sec to allow executor to complete before continuing
                .tryGet(5, TimeUnit.SECONDS);
        }

        // then
        runWithNewTransaction(() -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
            assertThat(counter.getNum()).isEqualTo(1L);
        });

        // when - executing mixed-in action
        runWithNewTransaction(() -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
            assertThat(counter.getNum()).isEqualTo(1L);

            var asyncControl = AsyncControl.defaults()
                    .with(executorService);

            // store the async proxy for later use below
            asyncProxyUnderTest2.set(wrapperFactory.asyncWrapMixin(Counter_bumpUsingMixin.class, counter, asyncControl));
        });

        // execute async and wait till done
        {
            // returns the detached counter entity, so we can immediately check whether the action was executed
            var counter = asyncProxyUnderTest2.get()
                    .applyAsync(Counter_bumpUsingMixin::act)
                    // let's wait max 5 sec to allow executor to complete before continuing
                    .tryGet(5, TimeUnit.SECONDS)
                    .valueAsNonNullElseFail();
            assertThat(counter.getNum()).isEqualTo(2L);
        }

        // then
        runWithNewTransaction(() -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
            assertThat(counter).isNotNull();
            assertThat(counter.getNum()).isEqualTo(2L);
        });
    }

    // -- HELPER

    private static Stream<Arguments> executorServices() {
        return Stream.of(
              Arguments.of("Executors.newSingleThreadExecutor()", Executors.newSingleThreadExecutor()),
              Arguments.of("ForkJoinPool.commonPool()", ForkJoinPool.commonPool()),
              Arguments.of("Executors.newFixedThreadPool(4)", Executors.newFixedThreadPool(4))
        );
    }

}
