/*
 * 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
 *
 *      https://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.commons.lang3.concurrent;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.concurrent.ExecutorService;

import org.apache.commons.lang3.function.FailableConsumer;
import org.apache.commons.lang3.function.FailableSupplier;
import org.junit.jupiter.api.Test;

class BackgroundInitializerSupplierTest extends BackgroundInitializerTest {

    /**
     * A concrete implementation of BackgroundInitializer. It is designed as a wrapper so the test can
     * use the same builder pattern that real code will.
     */
    protected static final class SupplierBackgroundInitializerTestImpl extends AbstractBackgroundInitializerTestImpl {

        SupplierBackgroundInitializerTestImpl() {
            setSupplierAndCloser((final CloseableCounter cc) -> cc.close());
        }

        SupplierBackgroundInitializerTestImpl(final ExecutorService exec) {
            super(exec);
            setSupplierAndCloser((final CloseableCounter cc) -> cc.close());
        }

        SupplierBackgroundInitializerTestImpl(final FailableConsumer<?, ?> consumer) {
            setSupplierAndCloser(consumer);
        }

        private void setSupplierAndCloser(final FailableConsumer<?, ?> consumer) {
            try {
                // Use reflection here because the constructors we need are private
                final FailableSupplier<?, ?> supplier = this::initializeInternal;
                final Field initializer = AbstractConcurrentInitializer.class.getDeclaredField("initializer");
                initializer.setAccessible(true);
                initializer.set(this, supplier);

                final Field closer = AbstractConcurrentInitializer.class.getDeclaredField("closer");
                closer.setAccessible(true);
                closer.set(this, consumer);
            } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
                fail();
            }
        }
    }

    @Override
    protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl() {
        return new SupplierBackgroundInitializerTestImpl();
    }

    @Override
    protected SupplierBackgroundInitializerTestImpl getBackgroundInitializerTestImpl(final ExecutorService exec) {
        return new SupplierBackgroundInitializerTestImpl(exec);
    }

    /**
     * Tests that close() method closes the wrapped object
     *
     * @throws Exception
     */
    @Test
    void testClose() throws Exception {
        final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
        assertFalse(init.getCloseableCounter().isClosed(), "closed without close() call");
        init.close();
        assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded before start()");
        init.start();
        init.get(); //ensure the Future has completed.
        assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded after start() but before close()");
        init.close();
        assertTrue(init.getCloseableCounter().isClosed(), "closed() did not succeed");
    }

    /**
     * Tests that close() wraps a checked exception in a ConcurrentException
     *
     * @throws Exception
     */
    @Test
    void testCloseWithCheckedException() throws Exception {

        final IOException ioException = new IOException();
        final FailableConsumer<?, ?> IOExceptionConsumer = (final CloseableCounter cc) -> {
            throw ioException;
        };

        final AbstractBackgroundInitializerTestImpl init = new SupplierBackgroundInitializerTestImpl(IOExceptionConsumer);
        init.start();
        init.get(); //ensure the Future has completed.
        try {
            init.close();
            fail();
        } catch (final Exception e) {
            assertInstanceOf(ConcurrentException.class, e);
            assertSame(ioException, e.getCause());
        }
    }

    /**
     * Tests that close() throws a runtime exception
     *
     * @throws Exception
     */
    @Test
    void testCloseWithRuntimeException() throws Exception {

        final NullPointerException npe = new NullPointerException();
        final FailableConsumer<?, ?> NullPointerExceptionConsumer = (final CloseableCounter cc) -> {
            throw npe;
        };

        final AbstractBackgroundInitializerTestImpl init = new SupplierBackgroundInitializerTestImpl(NullPointerExceptionConsumer);
        init.start();
        init.get(); //ensure the Future has completed.
        try {
            init.close();
            fail();
        } catch (final Exception e) {
            assertSame(npe, e);
        }
    }
}
