package io.smallrye.mutiny.operators;

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

import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.testng.annotations.Test;

import io.smallrye.mutiny.CompositeException;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.subscription.Cancellable;

public class UniOnFailureInvokeTest {

    private static final IOException BOOM = new IOException("boom");
    private final Uni<Integer> failure = Uni.createFrom().failure(BOOM);

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testThatConsumeMustNotBeNull() {
        Uni.createFrom().item(1).onFailure().invoke(null);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testThatFunctionMustNotBeNull() {
        Uni.createFrom().item(1).onFailure().invokeUni(null);
    }

    @Test
    public void testInvokeOnFailure() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        int res = failure
                .onFailure().invoke(container::set)
                .onFailure().recoverWithItem(1)
                .await().indefinitely();

        assertThat(res).isEqualTo(1);
        assertThat(container).hasValue(BOOM);
    }

    @Test
    public void testInvokeNotCalledOnItem() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        int res = Uni.createFrom().item(3)
                .onFailure().invoke(container::set)
                .onFailure().recoverWithItem(1)
                .await().indefinitely();

        assertThat(res).isEqualTo(3);
        assertThat(container).hasValue(null);
    }

    @Test
    public void testInvokeUniOnFailure() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        AtomicInteger called = new AtomicInteger(-1);
        int res = failure
                .onFailure().invokeUni(t -> {
                    container.set(t);
                    return Uni.createFrom().item(22).onItem().invoke(called::set);
                })
                .onFailure().recoverWithItem(1)
                .await().indefinitely();

        assertThat(res).isEqualTo(1);
        assertThat(container).hasValue(BOOM);
        assertThat(called).hasValue(22);
    }

    @Test
    public void testInvokeUniNotCalledOnItem() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        AtomicInteger called = new AtomicInteger(-1);
        int res = Uni.createFrom().item(3)
                .onFailure().invokeUni(t -> {
                    container.set(t);
                    return Uni.createFrom().item(22).onItem().invoke(called::set);
                })
                .onFailure().recoverWithItem(1)
                .await().indefinitely();

        assertThat(res).isEqualTo(3);
        assertThat(container).hasValue(null);
        assertThat(called).hasValue(-1);
    }

    @Test
    public void testInvokeNotCalledOnNullItem() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        int res = Uni.createFrom().nullItem()
                .onFailure().invoke(container::set)
                .onFailure().recoverWithItem(1)
                .onItem().apply(x -> 5)
                .await().indefinitely();

        assertThat(res).isEqualTo(5);
        assertThat(container).hasValue(null);
    }

    @Test
    public void testInvokeUniNotCalledOnNullItem() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        AtomicInteger called = new AtomicInteger(-1);
        int res = Uni.createFrom().nullItem()
                .onFailure().invokeUni(t -> {
                    container.set(t);
                    return Uni.createFrom().item(22).onItem().invoke(called::set);
                })
                .onFailure().recoverWithItem(1)
                .onItem().apply(x -> 4)
                .await().indefinitely();

        assertThat(res).isEqualTo(4);
        assertThat(container).hasValue(null);
        assertThat(called).hasValue(-1);
    }

    @Test
    public void testInvokeOnFailureWithExceptionThrownByCallback() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        assertThatThrownBy(() -> failure
                .onFailure().invoke(t -> {
                    container.set(t);
                    throw new IllegalStateException("bad");
                })
                .await().indefinitely()).isInstanceOf(CompositeException.class).satisfies(t -> {
                    CompositeException ex = (CompositeException) t;
                    assertThat(ex.getCauses()).hasSize(2).contains(BOOM);
                });
    }

    @Test
    public void testInvokeUniOnFailureWithExceptionThrownByCallback() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        assertThatThrownBy(() -> failure
                .onFailure().invokeUni(t -> {
                    container.set(t);
                    throw new IllegalStateException("bad");
                })
                .await().indefinitely()).isInstanceOf(CompositeException.class).satisfies(t -> {
                    CompositeException ex = (CompositeException) t;
                    assertThat(ex.getCauses()).hasSize(2).contains(BOOM);
                });
    }

    @Test
    public void testInvokeUniOnFailureWithCallbackReturningNull() {
        AtomicReference<Throwable> container = new AtomicReference<>();
        assertThatThrownBy(() -> failure
                .onFailure().invokeUni(t -> {
                    container.set(t);
                    return null;
                })
                .await().indefinitely()).isInstanceOf(CompositeException.class).satisfies(t -> {
                    CompositeException ex = (CompositeException) t;
                    assertThat(ex.getCauses()).hasSize(2).contains(BOOM);
                });
    }

    @Test
    public void testCancellationBeforeActionCompletes() {
        AtomicBoolean terminated = new AtomicBoolean();
        Uni<Object> uni = Uni.createFrom().emitter(e -> e.onTermination(() -> terminated.set(true)));

        AtomicInteger result = new AtomicInteger();
        AtomicReference<Throwable> failed = new AtomicReference<>();
        Cancellable cancellable = failure.onFailure().invokeUni(i -> uni).subscribe()
                .with(result::set, failed::set);

        cancellable.cancel();
        assertThat(result).hasValue(0);
        assertThat(failed).hasValue(null);
        //noinspection ConstantConditions
        assertThat(terminated).isTrue();
    }
}