/*
 *  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.extensions.commandlog.applib.integtest;

import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.inject.Inject;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.quartz.JobExecutionContext;

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

import org.springframework.transaction.annotation.Propagation;

import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.bookmark.BookmarkService;
import org.apache.causeway.applib.services.iactnlayer.InteractionService;
import org.apache.causeway.applib.services.wrapper.WrapperFactory;
import org.apache.causeway.applib.services.wrapper.WrapperFactory.AsyncProxy;
import org.apache.causeway.applib.services.xactn.TransactionService;
import org.apache.causeway.core.config.environment.CausewaySystemEnvironment;
import org.apache.causeway.extensions.commandlog.applib.dom.BackgroundService;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntry;
import org.apache.causeway.extensions.commandlog.applib.dom.CommandLogEntryRepository;
import org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn;
import org.apache.causeway.extensions.commandlog.applib.dom.ReplayState;
import org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter;
import org.apache.causeway.extensions.commandlog.applib.integtest.model.CounterRepository;
import org.apache.causeway.extensions.commandlog.applib.integtest.model.Counter_bumpUsingMixin;
import org.apache.causeway.extensions.commandlog.applib.job.RunBackgroundCommandsJob;
import org.apache.causeway.testing.integtestsupport.applib.CausewayIntegrationTestAbstract;

import lombok.SneakyThrows;

public abstract class BackgroundService_IntegTestAbstract extends CausewayIntegrationTestAbstract {

    @Inject InteractionService interactionService;
    @Inject BackgroundService backgroundService;
    @Inject WrapperFactory wrapperFactory;
    @Inject CommandLogEntryRepository commandLogEntryRepository;
    @Inject TransactionService transactionService;
    @Inject RunBackgroundCommandsJob runBackgroundCommandsJob;
    @Inject BookmarkService bookmarkService;
    @Inject CounterRepository<? extends Counter> counterRepository;

    JobExecutionContext mockQuartzJobExecutionContext = Mockito.mock(JobExecutionContext.class);

    Bookmark bookmark;

    protected abstract <T extends Counter> T newCounter(String name);

    private static boolean prototypingOrig;

    @BeforeAll
    static void setup_environment() {
        prototypingOrig = CausewaySystemEnvironment.deploymentTypeFromEnvironment().isPrototyping();
        CausewaySystemEnvironment.setPrototyping(true);
    }

    @AfterAll
    static void reset_environment() {
        CausewaySystemEnvironment.setPrototyping(prototypingOrig);
    }

    @BeforeEach
    void setup_counter() {
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            counterRepository.removeAll();

            counterRepository.persist(newCounter("fred"));
            List<? extends Counter> counters = counterRepository.find();
            assertThat(counters).hasSize(1);

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

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

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

    @Test
    void async_using_default_executor_service() {

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

        // when
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();

            asyncProxyUnderTest1.set(wrapperFactory.asyncWrap(counter));

        }).ifFailureFail();

        // execute async and wait till done
        {
            asyncProxyUnderTest1.get()
                .applyAsync(Counter::bumpUsingDeclaredAction)
                .tryGet(5, TimeUnit.SECONDS); // wait till done
        }

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

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

        }).ifFailureFail();

        // 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
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
            assertThat(counter.getNum()).isEqualTo(2L);
        }).ifFailureFail();

    }

    @SneakyThrows
    @Test
    void using_background_service() {

        final AtomicReference<AsyncProxy<Counter>> asyncProxyUnderTest = new AtomicReference<>();

        // given
        removeAllCommandLogEntriesAndCounters();

        // when
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
            assertThat(counter.getNum()).isNull();

            // when
            asyncProxyUnderTest.set(backgroundService.execute(counter));

        }).ifFailureFail();

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

        // then no change to the counter
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
            assertThat(counter.getNum()).isNull();   // still null
        }).ifFailureFail();

        // but then instead a background command is persisted
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            var all = commandLogEntryRepository.findAll();
            assertThat(all).hasSize(1);
            CommandLogEntry commandLogEntry = all.get(0);

            assertThat(commandLogEntry)
                    .satisfies(x -> assertThat(x.getTarget()).isEqualTo(bookmark))
                    .satisfies(x -> assertThat(x.getLogicalMemberIdentifier()).isEqualTo("commandlog.test.Counter#bumpUsingDeclaredAction"))
                    .satisfies(x -> assertThat(x.getTimestamp()).isNotNull())
                    .satisfies(x -> assertThat(x.getExecuteIn()).isEqualTo(ExecuteIn.BACKGROUND))
                    .satisfies(x -> assertThat(x.getParentInteractionId()).isNotNull())
                    .satisfies(x -> assertThat(x.getCommandDto()).isNotNull())
                    .satisfies(x -> assertThat(x.getStartedAt()).isNull())
                    .satisfies(x -> assertThat(x.getCompletedAt()).isNull())
                    .satisfies(x -> assertThat(x.getResult()).isNull())
                    .satisfies(x -> assertThat(x.getException()).isNullOrEmpty())
                    .satisfies(x -> assertThat(x.getResultSummary()).isNullOrEmpty())
                    .satisfies(x -> assertThat(x.getReplayState()).isEqualTo(ReplayState.UNDEFINED))
                    .satisfies(x -> assertThat(x.getReplayStateFailureReason()).isNull());
        }).ifFailureFail();

        // when (simulate quartz running in the background)
        runBackgroundCommandsJob.execute(mockQuartzJobExecutionContext);
        interactionService.nextInteraction();

        // then bumped
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            var counter = bookmarkService.lookup(bookmark, Counter.class).orElseThrow();
            assertThat(counter.getNum()).isEqualTo(1L);
        }).ifFailureFail();

        // and marked as started and completed
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            var after = commandLogEntryRepository.findAll();
            assertThat(after).hasSize(1);
            CommandLogEntry commandLogEntryAfter = after.get(0);

            assertThat(commandLogEntryAfter)
                    .satisfies(x -> assertThat(x.getStartedAt()).isNotNull()) // changed
                    .satisfies(x -> assertThat(x.getCompletedAt()).isNotNull()) // changed
                    .satisfies(x -> assertThat(x.getResult()).isNotNull()) // changed
                    .satisfies(x -> assertThat(x.getResultSummary()).isNotNull()) // changed
                    ;
        }).ifFailureFail();

    }

    private void removeAllCommandLogEntriesAndCounters() {
        transactionService.runTransactional(Propagation.REQUIRES_NEW, () -> {
            commandLogEntryRepository.removeAll();
            assertThat(commandLogEntryRepository.findAll()).isEmpty();
        }).ifFailureFail();
    }

}
