/*
 * Copyright 2019 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed 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 io.smallrye.metrics;

import static org.junit.Assert.assertEquals;

import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;

import org.eclipse.microprofile.metrics.Counter;
import org.eclipse.microprofile.metrics.MetricFilter;
import org.eclipse.microprofile.metrics.MetricID;
import org.eclipse.microprofile.metrics.MetricRegistry;
import org.eclipse.microprofile.metrics.Tag;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;

import io.smallrye.metrics.app.CounterImpl;

public class MetricRegistryThreadSafetyTest {

    private final MetricRegistry registry = MetricRegistries.get(MetricRegistry.Type.APPLICATION);

    @After
    public void cleanup() {
        registry.removeMatching(MetricFilter.ALL);
    }

    @Test
    public void tryRegisterSameMetricMultipleTimesInParallel() throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            cleanup();
            final AtomicReference<Counter> actuallyRegisteredCounter = new AtomicReference<>();
            final CountDownLatch latch = new CountDownLatch(100);
            final ExecutorService executor = Executors.newFixedThreadPool(100);
            final CompletableFuture[] futures = IntStream.range(0, 100)
                    .mapToObj(j -> CompletableFuture.runAsync(
                            () -> {
                                latch.countDown();
                                try {
                                    latch.await();
                                } catch (InterruptedException e) {
                                }
                                CounterImpl counter = registry.register("mycounter", new CounterImpl());
                                actuallyRegisteredCounter.set(counter);
                            }, executor))
                    .toArray(CompletableFuture[]::new);
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);

            assertEquals("exactly one attempt should go through",
                    1, Arrays.stream(futures).filter(f -> {
                        try {
                            f.get();
                            return true;
                        } catch (Exception e) {
                            return false;
                        }
                    }).count());

            assertEquals("99 attempts should fail with IllegalStateException",
                    99, Arrays.stream(futures).filter(f -> {
                        try {
                            f.get();
                            return false;
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            return false;
                        } catch (ExecutionException e) {
                            return e.getCause() instanceof IllegalStateException;
                        }
                    }).count());

            // verify that the one counter instance that was successfully registered is now indeed in the registry
            assertEquals(actuallyRegisteredCounter.get(), registry.getCounters().get(new MetricID("mycounter")));
        }
    }

    /**
     * One set of threads is registering new metrics and removing them after a short while,
     * at the same time, another set of threads is retrieving a list of metrics conforming to a filter.
     * None of the threads should be getting any exceptions.
     */
    @Test
    public void tryRegisteringRemovingAndReadingAtTheSameTime() throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            cleanup();
            final ExecutorService executor = Executors.newFixedThreadPool(10);
            final CompletableFuture[] futures = IntStream.range(0, 200)
                    .parallel()
                    .mapToObj(j -> CompletableFuture.runAsync(
                            () -> {
                                try {
                                    if (j % 2 == 0) {
                                        MetricID metricID = new MetricID("mycounter", new Tag("number", String.valueOf(j)));
                                        registry.counter("mycounter", new Tag("number", String.valueOf(j)));
                                        registry.remove(metricID);
                                    } else {
                                        registry.getCounters(MetricFilter.ALL);
                                    }
                                } catch (Throwable t) {
                                    t.printStackTrace();
                                    throw t;
                                }
                            }, executor))
                    .toArray(CompletableFuture[]::new);
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);

            assertEquals("All threads should finish without exceptions",
                    0, Arrays.stream(futures).filter(CompletableFuture::isCompletedExceptionally).count());
        }
    }

    /**
     * Test concurrent calls of MetricRegistryImpl.register() and MetricRegistryImpl.getMetadata()
     * at the same time.
     */
    @Test
    public void registerAndGetMetadata() throws InterruptedException, ExecutionException, TimeoutException {
        final MetricsRegistryImpl registry = new MetricsRegistryImpl();
        final AtomicReference<Throwable> throwableEncounteredDuringTest = new AtomicReference<>();
        ExecutorService executor = Executors.newFixedThreadPool(50);
        try {
            // to store CompletableFutures for all the operations the test will perform
            CompletableFuture<Void>[] futures = new CompletableFuture[2000];
            for (int i = 0; i < 1000; i++) {
                final int finalI = i;
                futures[2 * i] = CompletableFuture.runAsync(() -> {
                    try {
                        registry.counter("metric" + finalI);
                    } catch (Throwable t) {
                        throwableEncounteredDuringTest.set(t);
                    }

                }, executor);
                futures[2 * i + 1] = CompletableFuture.runAsync(() -> {
                    try {
                        registry.getMetadata();
                    } catch (Throwable t) {
                        throwableEncounteredDuringTest.set(t);
                    }
                }, executor);
            }

            // wait until all tasks finish
            CompletableFuture.allOf(futures).get(30, TimeUnit.SECONDS);

            // assert that no task received an error and that the registry contains all required data
            Assert.assertNull(throwableEncounteredDuringTest.get());
            Assert.assertEquals(1000, registry.getMetadata().size());
            Assert.assertEquals(1000, registry.getCounters().size());
        } finally {
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);
        }
    }

}