package org.logstash.util;

import org.junit.Test;

import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertThrows;

public class SetOnceReferenceTest {
    @Test
    public void testFromUnset() {
        checkUnsetReference(SetOnceReference.unset());
    }

    @Test
    public void testFromOfWithValue() {
        final Sentinel sentinel = new Sentinel();
        checkSetReferenceIsImmutable(SetOnceReference.of(sentinel), sentinel);
    }

    @Test
    public void testFromOfWithNull() {
        assertThrows(NullPointerException.class, () -> SetOnceReference.of(null));
    }

    @Test
    public void testFromOfNullableWithNull() {
        checkUnsetReference(SetOnceReference.ofNullable(null));
    }

    @Test
    public void testFromOfNullableWithValue() {
        final Sentinel sentinel = new Sentinel();
        checkSetReferenceIsImmutable(SetOnceReference.ofNullable(sentinel), sentinel);
    }

    @Test
    public void testUnsetOfferValue() {
        final Sentinel offeredSentinel = new Sentinel();
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();
        assertThat(reference.offer(offeredSentinel), is(true));

        checkSetReferenceIsImmutable(reference, offeredSentinel);
    }

    @Test
    public void testUnsetOfferSupplier() {
        final Sentinel offeredSentinel = new Sentinel();
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();
        assertThat(reference.offer(() -> offeredSentinel), is(true));

        checkSetReferenceIsImmutable(reference, offeredSentinel);
    }

    @Test
    public void testUnsetOfferNullValue() {
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();
        assertThrows(NullPointerException.class, () -> reference.offer((Sentinel) null));
    }

    @Test
    public void testUnsetOfferSupplierThatReturnsNullValue() {
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();
        assertThat(reference.offer(() -> null), is(false));

        checkUnsetReference(reference);
    }

    @Test
    public void testUnsetIfSetOrElseSupply() {
        final Sentinel suppliedSentinel = new Sentinel();
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        final MutableReference<Boolean> supplierCalled = new MutableReference<>(false);
        final MutableReference<Boolean> consumerCalled = new MutableReference<>(false);

        final boolean returnValue = reference.ifSetOrElseSupply(
                (v) -> consumerCalled.setValue(true),
                () -> { supplierCalled.setValue(true); return suppliedSentinel; }
        );

        assertThat(returnValue, is(true));
        assertThat(supplierCalled.getValue(), is(true));
        assertThat(consumerCalled.getValue(), is(false));

        checkSetReferenceIsImmutable(reference, suppliedSentinel);
    }

    @Test
    public void testUnsetIfSetOrElseSupplyNullValue() {
        final Sentinel nullSentinel = null;
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        final MutableReference<Boolean> consumerCalled = new MutableReference<>(false);

        //noinspection ConstantConditions
        assertThrows(NullPointerException.class, () ->
            reference.ifSetOrElseSupply(
                    (v) -> consumerCalled.setValue(true),
                    () -> nullSentinel
            )
        );

        assertThat(consumerCalled.getValue(), is(false));

        checkUnsetReference(reference);
    }

    @Test
    public void testUnsetOfferAndGetWithValue() {
        final Sentinel suppliedSentinel = new Sentinel();
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        assertThat(reference.offerAndGet(suppliedSentinel), is(sameInstance(suppliedSentinel)));

        checkSetReferenceIsImmutable(reference, suppliedSentinel);
    }
    @Test
    public void testUnsetOfferAndGetWithNullValue() {
        final Sentinel nullSentinel = null;
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        //noinspection ConstantConditions
        assertThrows(NullPointerException.class, () -> reference.offerAndGet(nullSentinel));

        checkUnsetReference(reference);
    }

    @Test
    public void testUnsetOfferAndGetWithSupplier() {
        final Sentinel suppliedSentinel = new Sentinel();
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        assertThat(reference.offerAndGet(() -> suppliedSentinel), is(sameInstance(suppliedSentinel)));

        checkSetReferenceIsImmutable(reference, suppliedSentinel);
    }
    @Test
    public void testUnsetOfferAndGetWithNullReturningSupplier() {
        final Sentinel nullSentinel = null;
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        //noinspection ConstantConditions
        assertThrows(NullPointerException.class, () -> reference.offerAndGet(() -> nullSentinel));

        checkUnsetReference(reference);
    }

    @Test
    public void testUnsetOfferAndGetOptionalWithValue() {
        final Sentinel suppliedSentinel = new Sentinel();
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        assertThat(reference.offerAndGetOptional(() -> suppliedSentinel), is(equalTo(Optional.of(suppliedSentinel))));

        checkSetReferenceIsImmutable(reference, suppliedSentinel);
    }


    @Test
    public void testUnsetOfferAndGetOptionalWithNullValue() {
        final Sentinel nullSentinel = null;
        final SetOnceReference<Sentinel> reference = SetOnceReference.unset();

        //noinspection ConstantConditions
        assertThat(reference.offerAndGetOptional(() -> nullSentinel), is(equalTo(Optional.empty())));

        checkUnsetReference(reference);
    }

    void checkUnsetReference(final SetOnceReference<Sentinel> unsetReference) {
        assertThat(unsetReference.isSet(), is(false));
        assertThrows(NoSuchElementException.class, unsetReference::get);
        assertThat(unsetReference.orElse(null), is(nullValue()));
        assertThat(unsetReference.asOptional(), is(equalTo(Optional.empty())));

        // double-check that none of the above mutated our reference
        assertThat(unsetReference.isSet(), is(false));
    }

    void checkExpectedValue(final SetOnceReference<Sentinel> immutable, final Sentinel expectedValue) {
        Objects.requireNonNull(expectedValue);

        assertThat(immutable.isSet(), is(true));
        assertThat(immutable.get(), is(sameInstance(expectedValue)));
        assertThat(immutable.orElse(new Sentinel()), is(sameInstance(expectedValue)));
        assertThat(immutable.asOptional(), is(equalTo(Optional.of(expectedValue))));
    }

    void checkSetReferenceIsImmutable(final SetOnceReference<Sentinel> immutable, final Sentinel expectedValue) {
        Objects.requireNonNull(expectedValue);

        checkExpectedValue(immutable, expectedValue); // sanity check

        checkImmutableOffer(immutable, expectedValue);
        checkImmutableIfSetOrElseSupply(immutable, expectedValue);
        checkImmutableOfferAndGetValue(immutable, expectedValue);
        checkImmutableOfferAndGetSupplier(immutable, expectedValue);

        checkExpectedValue(immutable, expectedValue);
    }

    void checkImmutableOffer(final SetOnceReference<Sentinel> immutable, final Sentinel expectedValue) {
        assertThat(immutable.offer(new Sentinel()), is(false));

        checkExpectedValue(immutable, expectedValue);
    }

    void checkImmutableOfferAndGetValue(final SetOnceReference<Sentinel> immutable, final Sentinel expectedValue) {
        assertThat(immutable.offerAndGet(new Sentinel()), is(sameInstance(expectedValue)));

        checkExpectedValue(immutable, expectedValue);
    }

    void checkImmutableOfferAndGetSupplier(final SetOnceReference<Sentinel> immutable, final Sentinel expectedValue) {
        final MutableReference<Boolean> supplierCalled = new MutableReference<>(false);
        final Supplier<Sentinel> sentinelSupplier = () -> {
            supplierCalled.setValue(true);
            return new Sentinel();
        };
        assertThat(immutable.offerAndGet(sentinelSupplier), is(sameInstance(expectedValue)));
        assertThat(supplierCalled.getValue(), is(false));

        checkExpectedValue(immutable, expectedValue);
    }

    void checkImmutableIfSetOrElseSupply(final SetOnceReference<Sentinel> immutable, final Sentinel expectedValue) {
        final MutableReference<Boolean> supplierCalled = new MutableReference<>(false);
        final MutableReference<Boolean> consumerCalled = new MutableReference<>(false);
        final MutableReference<Sentinel> consumed = new MutableReference<>(null);

        final boolean returnValue = immutable.ifSetOrElseSupply(
                (v) -> { consumerCalled.setValue(true); consumed.setValue(v); },
                () -> { supplierCalled.setValue(true); return new Sentinel(); }
        );

        assertThat(returnValue, is(false));
        assertThat(supplierCalled.getValue(), is(false));
        assertThat(consumerCalled.getValue(), is(true));
        assertThat(consumed.getValue(), is(sameInstance(expectedValue)));

        checkExpectedValue(immutable, expectedValue);
    }

    private static class MutableReference<T> {
        T value;

        public MutableReference(T value) {
            this.value = value;
        }

        public T getValue() {
            return value;
        }

        public void setValue(T value) {
            this.value = value;
        }
    }

    private static class Sentinel {
        private static final AtomicLong ID_GENERATOR = new AtomicLong();
        private final long id;
        private Sentinel(){
            this.id = ID_GENERATOR.incrementAndGet();
        }

        @Override
        public String toString() {
            return "Sentinel{" +
                    "id=" + id +
                    '}';
        }
    }
}