/**
 * Copyright (c) 2012-2017, jcabi.com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met: 1) Redistributions of source code must retain the above
 * copyright notice, this list of conditions and the following
 * disclaimer. 2) Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided
 * with the distribution. 3) Neither the name of the jcabi.com nor
 * the names of its contributors may be used to endorse or promote
 * products derived from this software without specific prior written
 * permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jcabi.aspects;

import java.security.SecureRandom;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Test;

/**
 * Test case for {@link Cacheable} annotation and its implementation.
 * @author Yegor Bugayenko (yegor@tpc2.com)
 * @version $Id: fdcd961b2402e7b90400934292a5a56df825203b $
 * @checkstyle ClassDataAbstractionCoupling (500 lines)
 */
@SuppressWarnings({ "PMD.TooManyMethods", "PMD.DoNotUseThreads" })
public final class CacheableTest {

    /**
     * Random.
     * @checkstyle ConstantUsageCheck (3 lines)
     */
    private static final Random RANDOM = new SecureRandom();

    /**
     * Cacheable can cache calls.
     * @throws Exception If something goes wrong
     */
    @Test
    public void cachesSimpleCall() throws Exception {
        final CacheableTest.Foo foo = new CacheableTest.Foo(1L);
        final String first = foo.get().toString();
        MatcherAssert.assertThat(first, Matchers.equalTo(foo.get().toString()));
        foo.flush();
        MatcherAssert.assertThat(
            foo.get().toString(),
            Matchers.not(Matchers.equalTo(first))
        );
    }

    /**
     * Cacheable can Asynchronous update.
     * @throws Exception If something goes wrong
     */
    @Test
    public void asyncUpdateCacheSimpleCall() throws Exception {
        final CacheableTest.Foo foo = new CacheableTest.Foo(1L);
        final String first = foo.asyncGet().toString();
        MatcherAssert.assertThat(
            first,
            Matchers.equalTo(foo.asyncGet().toString())
        );
        TimeUnit.SECONDS.sleep(2);
        MatcherAssert.assertThat(
            first,
            Matchers.equalTo(foo.asyncGet().toString())
        );
        TimeUnit.SECONDS.sleep(2);
        MatcherAssert.assertThat(
            first,
            Matchers.not(Matchers.equalTo(foo.asyncGet().toString()))
        );
    }

    /**
     * Cacheable can cache static calls.
     * @throws Exception If something goes wrong
     */
    @Test
    public void cachesSimpleStaticCall() throws Exception {
        final String first = CacheableTest.Foo.staticGet();
        MatcherAssert.assertThat(
            first,
            Matchers.equalTo(CacheableTest.Foo.staticGet())
        );
        CacheableTest.Foo.staticFlush();
        MatcherAssert.assertThat(
            CacheableTest.Foo.staticGet(),
            Matchers.not(Matchers.equalTo(first))
        );
    }

    /**
     * Cacheable can clean cache after timeout.
     * @throws Exception If something goes wrong
     */
    @Test
    public void cleansCacheWhenExpired() throws Exception {
        final CacheableTest.Foo foo = new CacheableTest.Foo(1L);
        final String first = foo.get().toString();
        TimeUnit.SECONDS.sleep((long) Tv.FIVE);
        MatcherAssert.assertThat(
            foo.get().toString(),
            Matchers.not(Matchers.equalTo(first))
        );
    }

    /**
     * Cacheable can cache just once.
     * @throws Exception If something goes wrong
     */
    @Test
    public void cachesJustOnceInParallelThreads() throws Exception {
        final CacheableTest.Foo foo = new CacheableTest.Foo(1L);
        final Thread never = new Thread(
            new Runnable() {
                @Override
                public void run() {
                    foo.never();
                }
            }
        );
        never.start();
        final Set<String> values = new ConcurrentSkipListSet<String>();
        final int threads = Runtime.getRuntime().availableProcessors() << 1;
        final CountDownLatch start = new CountDownLatch(1);
        final CountDownLatch done = new CountDownLatch(threads);
        final Callable<Boolean> task = new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                start.await(1L, TimeUnit.SECONDS);
                values.add(foo.get().toString());
                done.countDown();
                return true;
            }
        };
        final ExecutorService executor = Executors.newFixedThreadPool(threads);
        try {
            for (int pos = 0; pos < threads; ++pos) {
                executor.submit(task);
            }
            start.countDown();
            done.await((long) Tv.THIRTY, TimeUnit.SECONDS);
            MatcherAssert.assertThat(values.size(), Matchers.equalTo(1));
            never.interrupt();
        } finally {
            executor.shutdown();
        }
    }

    /**
     * Cacheable can flush with a static trigger.
     * @throws Exception If something goes wrong
     */
    @Test
    public void flushesWithStaticTrigger() throws Exception {
        final CacheableTest.Bar bar = new CacheableTest.Bar();
        MatcherAssert.assertThat(
            bar.get(),
            Matchers.not(Matchers.equalTo(bar.get()))
        );
    }

    /**
     * Dummy class, for tests above.
     */
    private static final class Foo {
        /**
         * Encapsulated long.
         */
        private final transient long number;
        /**
         * Public ctor.
         * @param num Number to encapsulate
         */
        Foo(final long num) {
            this.number = num;
        }
        @Override
        public int hashCode() {
            return this.get().hashCode();
        }
        @Override
        public boolean equals(final Object obj) {
            return obj == this;
        }
        @Override
        @Cacheable(forever = true)
        @Loggable(Loggable.DEBUG)
        public String toString() {
            return Long.toString(this.number);
        }
        /**
         * Download some text.
         * @return Downloaded text
         */
        @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS)
        @Loggable(Loggable.DEBUG)
        public CacheableTest.Foo get() {
            return new CacheableTest.Foo(CacheableTest.RANDOM.nextLong());
        }

        /**
         * Download some text.
         * @return Downloaded text
         */
        @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS, asyncUpdate = true)
        @Loggable(Loggable.DEBUG)
        public CacheableTest.Foo asyncGet() {
            return new CacheableTest.Foo(CacheableTest.RANDOM.nextLong());
        }
        /**
         * Sleep forever, to abuse caching system.
         * @return The same object
         */
        @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS)
        @Loggable(Loggable.DEBUG)
        public CacheableTest.Foo never() {
            try {
                TimeUnit.HOURS.sleep(1L);
            } catch (final InterruptedException ex) {
                throw new IllegalStateException(ex);
            }
            return this;
        }
        /**
         * Flush it.
         */
        @Cacheable.FlushBefore
        public void flush() {
            // nothing to do
        }
        /**
         * Download some text.
         * @return Downloaded text
         */
        @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS)
        public static String staticGet() {
            return Long.toString(CacheableTest.RANDOM.nextLong());
        }
        /**
         * Flush it.
         */
        @Cacheable.FlushBefore
        public static void staticFlush() {
            // nothing to do
        }
    }

    /**
     * Dummy class, for tests above.
     */
    public static final class Bar {
        /**
         * Get some number.
         * @return The number
         */
        @Cacheable(before = Bar.class)
        public long get() {
            return CacheableTest.RANDOM.nextLong();
        }
        /**
         * Flush before?
         * @return TRUE if flush is required
         */
        public static boolean flushBefore() {
            return true;
        }
    }

}
