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

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;

/**
 * Test class for {@code EventCountCircuitBreaker}.
 */
public class EventCountCircuitBreakerTest {
    /** Constant for the opening threshold. */
    private static final int OPENING_THRESHOLD = 10;

    /** Constant for the closing threshold. */
    private static final int CLOSING_THRESHOLD = 5;

    /** Constant for the factor for converting nanoseconds. */
    private static final long NANO_FACTOR = 1000L * 1000L * 1000L;

    /**
     * Tests that time units are correctly taken into account by constructors.
     */
    @Test
    public void testIntervalCalculation() {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 2, TimeUnit.MILLISECONDS);
        assertEquals(NANO_FACTOR, breaker.getOpeningInterval(), "Wrong opening interval");
        assertEquals(2 * NANO_FACTOR / 1000, breaker.getClosingInterval(), "Wrong closing interval");
    }

    /**
     * Tests that the closing interval is the same as the opening interval if it is not
     * specified.
     */
    @Test
    public void testDefaultClosingInterval() {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS, CLOSING_THRESHOLD);
        assertEquals(NANO_FACTOR, breaker.getClosingInterval(), "Wrong closing interval");
    }

    /**
     * Tests that the closing threshold is the same as the opening threshold if not
     * specified otherwise.
     */
    @Test
    public void testDefaultClosingThreshold() {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS);
        assertEquals(NANO_FACTOR, breaker.getClosingInterval(), "Wrong closing interval");
        assertEquals(OPENING_THRESHOLD, breaker.getClosingThreshold(), "Wrong closing threshold");
    }

    /**
     * Tests that a circuit breaker is closed after its creation.
     */
    @Test
    public void testInitiallyClosed() {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS);
        assertFalse(breaker.isOpen(), "Open");
        assertTrue(breaker.isClosed(), "Not closed");
    }

    /**
     * Tests whether the current time is correctly determined.
     */
    @Test
    public void testNow() {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS);
        final long now = breaker.now();
        final long delta = Math.abs(System.nanoTime() - now);
        assertTrue(delta < 100000, String.format("Delta %d ns to current time too large", delta));
    }

    /**
     * Tests that the circuit breaker stays closed if the number of received events stays
     * below the threshold.
     */
    @Test
    public void testNotOpeningUnderThreshold() {
        long startTime = 1000;
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        for (int i = 0; i < OPENING_THRESHOLD - 1; i++) {
            assertTrue(breaker.at(startTime).incrementAndCheckState(), "In open state");
            startTime++;
        }
        assertTrue(breaker.isClosed(), "Not closed");
    }

    /**
     * Tests that the circuit breaker stays closed if there are a number of received
     * events, but not in a single check interval.
     */
    @Test
    public void testNotOpeningCheckIntervalExceeded() {
        long startTime = 0L;
        final long timeIncrement = 3 * NANO_FACTOR / (2 * OPENING_THRESHOLD);
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        for (int i = 0; i < 5 * OPENING_THRESHOLD; i++) {
            assertTrue(breaker.at(startTime).incrementAndCheckState(), "In open state");
            startTime += timeIncrement;
        }
        assertTrue(breaker.isClosed(), "Not closed");
    }

    /**
     * Tests that the circuit breaker opens if all conditions are met.
     */
    @Test
    public void testOpeningWhenThresholdReached() {
        long startTime = 0;
        final long timeIncrement = NANO_FACTOR / OPENING_THRESHOLD - 1;
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        boolean open = false;
        for (int i = 0; i < OPENING_THRESHOLD + 1; i++) {
            open = !breaker.at(startTime).incrementAndCheckState();
            startTime += timeIncrement;
        }
        assertTrue(open, "Not open");
        assertFalse(breaker.isClosed(), "Closed");
    }

    /**
     * Tests that the circuit breaker opens if all conditions are met when using
     * {@link EventCountCircuitBreaker#incrementAndCheckState(Integer increment)}.
     */
    @Test
    public void testOpeningWhenThresholdReachedThroughBatch() {
        final long timeIncrement = NANO_FACTOR / OPENING_THRESHOLD - 1;
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 1,
            TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        long startTime = timeIncrement * (OPENING_THRESHOLD + 1);
        boolean open = !breaker.at(startTime).incrementAndCheckState(OPENING_THRESHOLD + 1);
        assertTrue(open, "Not open");
        assertFalse(breaker.isClosed(), "Closed");
    }

    /**
     * Tests that an open circuit breaker does not close itself when the number of events
     * received is over the threshold.
     */
    @Test
    public void testNotClosingOverThreshold() {
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD,
                10, TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        long startTime = 0;
        breaker.open();
        for (int i = 0; i <= CLOSING_THRESHOLD; i++) {
            assertFalse(breaker.at(startTime).incrementAndCheckState(), "Not open");
            startTime += 1000;
        }
        assertFalse(breaker.at(startTime + NANO_FACTOR).incrementAndCheckState(), "Closed in new interval");
        assertTrue(breaker.isOpen(), "Not open at end");
    }

    /**
     * Tests that the circuit breaker closes automatically if the number of events
     * received goes under the closing threshold.
     */
    @Test
    public void testClosingWhenThresholdReached() {
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD,
                10, TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        breaker.open();
        breaker.at(1000).incrementAndCheckState();
        assertFalse(breaker.at(2000).checkState(), "Already closed");
        assertFalse(breaker.at(NANO_FACTOR).checkState(), "Closed at interval end");
        assertTrue(breaker.at(NANO_FACTOR + 1).checkState(), "Not closed after interval end");
        assertTrue(breaker.isClosed(), "Not closed at end");
    }

    /**
     * Tests whether an explicit open operation fully initializes the internal check data
     * object. Otherwise, the circuit breaker may close itself directly afterwards.
     */
    @Test
    public void testOpenStartsNewCheckInterval() {
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        breaker.at(NANO_FACTOR - 1000).open();
        assertTrue(breaker.isOpen(), "Not open");
        assertFalse(breaker.at(NANO_FACTOR + 100).checkState(), "Already closed");
    }

    /**
     * Tests whether a new check interval is started if the circuit breaker has a
     * transition to open state.
     */
    @Test
    public void testAutomaticOpenStartsNewCheckInterval() {
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        long time = 10 * NANO_FACTOR;
        for (int i = 0; i <= OPENING_THRESHOLD; i++) {
            breaker.at(time++).incrementAndCheckState();
        }
        assertTrue(breaker.isOpen(), "Not open");
        time += NANO_FACTOR - 1000;
        assertFalse(breaker.at(time).incrementAndCheckState(), "Already closed");
        time += 1001;
        assertTrue(breaker.at(time).checkState(), "Not closed in time interval");
    }

    /**
     * Tests whether the circuit breaker can be closed explicitly.
     */
    @Test
    public void testClose() {
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        long time = 0;
        for (int i = 0; i <= OPENING_THRESHOLD; i++, time += 1000) {
            breaker.at(time).incrementAndCheckState();
        }
        assertTrue(breaker.isOpen(), "Not open");
        breaker.close();
        assertTrue(breaker.isClosed(), "Not closed");
        assertTrue(breaker.at(time + 1000).incrementAndCheckState(), "Open again");
    }

    /**
     * Tests whether events are generated when the state is changed.
     */
    @Test
    public void testChangeEvents() {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS);
        final ChangeListener listener = new ChangeListener(breaker);
        breaker.addChangeListener(listener);
        breaker.open();
        breaker.close();
        listener.verify(Boolean.TRUE, Boolean.FALSE);
    }

    /**
     * Tests whether a change listener can be removed.
     */
    @Test
    public void testRemoveChangeListener() {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS);
        final ChangeListener listener = new ChangeListener(breaker);
        breaker.addChangeListener(listener);
        breaker.open();
        breaker.removeChangeListener(listener);
        breaker.close();
        listener.verify(Boolean.TRUE);
    }

    /**
     * Tests that a state transition triggered by multiple threads is handled correctly.
     * Only the first transition should cause an event to be sent.
     */
    @Test
    public void testStateTransitionGuarded() throws InterruptedException {
        final EventCountCircuitBreaker breaker = new EventCountCircuitBreaker(OPENING_THRESHOLD, 1,
                TimeUnit.SECONDS);
        final ChangeListener listener = new ChangeListener(breaker);
        breaker.addChangeListener(listener);

        final int threadCount = 128;
        final CountDownLatch latch = new CountDownLatch(1);
        final Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (final InterruptedException iex) {
                        // ignore
                    }
                    breaker.open();
                }
            };
            threads[i].start();
        }
        latch.countDown();
        for (final Thread thread : threads) {
            thread.join();
        }
        listener.verify(Boolean.TRUE);
    }

    /**
     * Tests that automatic state transitions generate change events as well.
     */
    @Test
    public void testChangeEventsGeneratedByAutomaticTransitions() {
        final EventCountCircuitBreakerTestImpl breaker = new EventCountCircuitBreakerTestImpl(OPENING_THRESHOLD, 2,
                TimeUnit.SECONDS, CLOSING_THRESHOLD, 1, TimeUnit.SECONDS);
        final ChangeListener listener = new ChangeListener(breaker);
        breaker.addChangeListener(listener);
        long time = 0;
        for (int i = 0; i <= OPENING_THRESHOLD; i++, time += 1000) {
            breaker.at(time).incrementAndCheckState();
        }
        breaker.at(NANO_FACTOR + 1).checkState();
        breaker.at(3 * NANO_FACTOR).checkState();
        listener.verify(Boolean.TRUE, Boolean.FALSE);
    }

    /**
     * A test implementation of {@code EventCountCircuitBreaker} which supports mocking the timer.
     * This is useful for the creation of deterministic tests for switching the circuit
     * breaker's state.
     */
    private static class EventCountCircuitBreakerTestImpl extends EventCountCircuitBreaker {
        /** The current time in nanoseconds. */
        private long currentTime;

        EventCountCircuitBreakerTestImpl(final int openingThreshold, final long openingInterval,
                                                final TimeUnit openingUnit, final int closingThreshold, final long closingInterval,
                                                final TimeUnit closingUnit) {
            super(openingThreshold, openingInterval, openingUnit, closingThreshold,
                    closingInterval, closingUnit);
        }

        /**
         * Sets the current time to be used by this test object for the next operation.
         *
         * @param time the time to set
         * @return a reference to this object
         */
        public EventCountCircuitBreakerTestImpl at(final long time) {
            currentTime = time;
            return this;
        }

        /**
         * {@inheritDoc} This implementation returns the value passed to the {@code at()}
         * method.
         */
        @Override
        long now() {
            return currentTime;
        }
    }

    /**
     * A test change listener for checking whether correct change events are generated.
     */
    private static class ChangeListener implements PropertyChangeListener {
        /** The expected event source. */
        private final Object expectedSource;

        /** A list with the updated values extracted from received change events. */
        private final List<Boolean> changedValues;

        /**
         * Creates a new instance of {@code ChangeListener} and sets the expected event
         * source.
         *
         * @param source the expected event source
         */
        ChangeListener(final Object source) {
            expectedSource = source;
            changedValues = new ArrayList<>();
        }

        @Override
        public void propertyChange(final PropertyChangeEvent evt) {
            assertEquals(expectedSource, evt.getSource(), "Wrong event source");
            assertEquals("open", evt.getPropertyName(), "Wrong property name");
            final Boolean newValue = (Boolean) evt.getNewValue();
            final Boolean oldValue = (Boolean) evt.getOldValue();
            assertNotEquals(newValue, oldValue, "Old and new value are equal");
            changedValues.add(newValue);
        }

        /**
         * Verifies that change events for the expected values have been received.
         *
         * @param values the expected values
         */
        public void verify(final Boolean... values) {
            assertArrayEquals(values,
                    changedValues.toArray(new Boolean[changedValues.size()]));
        }
    }
}
