/*
 * 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.jackrabbit.oak.plugins.document;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.apache.jackrabbit.guava.common.collect.Lists;
import org.apache.jackrabbit.guava.common.collect.Queues;
import org.apache.jackrabbit.guava.common.collect.Sets;
import org.apache.jackrabbit.guava.common.util.concurrent.Uninterruptibles;

import org.junit.Test;

/**
 * Tests the revision class
 */
public class RevisionTest {
    
    @Test
    public void invalid() {
        // revisions need to start with "br" or "r"
        for(String s : "1234,b,bb,".split(",")) {
            try {
                Revision.fromString(s);
                fail("Expected: Invalid revision id exception for " + s);
            } catch (Exception expected) {
                // expected
            }
        }
    }
    
    @Test
    public void edgeCases() {
        assertEquals("br0-0-0", new Revision(0, 0, 0, true).toString());
        Random rand = new Random(0);
        for (int i = 0; i < 1000; i++) {
            Revision r = new Revision(rand.nextLong(), rand.nextInt(),
                    rand.nextInt(), rand.nextBoolean());
            assertEquals(r.toString(), Revision.fromString(r.toString()).toString());
        }
        for (int i = 0; i < 1000; i++) {
            Revision r = new Revision(rand.nextInt(10), rand.nextInt(10),
                    rand.nextInt(10), rand.nextBoolean());
            assertEquals(r.toString(), Revision.fromString(r.toString()).toString());
        }
    }

    @Test
    public void fromStringToString() {
        for (int i = 0; i < 10000; i++) {
            Revision r = Revision.newRevision(i);
            // System.out.println(r);
            String rs = r.toString();
            Revision r2 = Revision.fromString(rs);
            if(!rs.equals(r2.toString())) {
                r2 = Revision.fromString(rs);
                assertEquals(rs, r2.toString());
            }
            assertEquals(rs, r2.toString());
            assertEquals(r.hashCode(), r2.hashCode());
            assertTrue(r.equals(r2));
        }
    }

    @Test
    public void difference() throws InterruptedException {
        long t0 = Revision.getCurrentTimestamp();
        Revision r0 = Revision.newRevision(0);
        Revision r1 = Revision.newRevision(0);
        long t1 = Revision.getCurrentTimestamp();
        // the difference must not be more than t1 - t0
        assertTrue(Revision.getTimestampDifference(r1, r0) <= (t1 - t0));
        // busy wait until we have a timestamp different from t1
        long t2;
        do {
            t2 = Revision.getCurrentTimestamp();
        } while (t1 == t2);

        Revision r2 = Revision.newRevision(0);
        assertTrue(Revision.getTimestampDifference(r2, r1) > 0);
    }

    @Test
    public void equalsHashCode() {
        Revision a = Revision.newRevision(0);
        Revision b = Revision.newRevision(0);
        assertTrue(a.equals(a));
        assertFalse(a.equals(b));
        assertFalse(b.equals(a));
        assertFalse(a.hashCode() == b.hashCode());
        Revision a1 = Revision.fromString(a.toString());
        assertTrue(a.equals(a1));
        assertTrue(a1.equals(a));
        Revision a2 = new Revision(a.getTimestamp(), a.getCounter(), a.getClusterId());
        assertTrue(a.equals(a2));
        assertTrue(a2.equals(a));
        assertEquals(a.hashCode(), a1.hashCode());
        assertEquals(a.hashCode(), a2.hashCode());
        Revision x1 = new Revision(a.getTimestamp() + 1, a.getCounter(), a.getClusterId());
        assertFalse(a.equals(x1));
        assertFalse(x1.equals(a));
        assertFalse(a.hashCode() == x1.hashCode());
        Revision x2 = new Revision(a.getTimestamp(), a.getCounter() + 1, a.getClusterId());
        assertFalse(a.equals(x2));
        assertFalse(x2.equals(a));
        assertFalse(a.hashCode() == x2.hashCode());
        Revision x3 = new Revision(a.getTimestamp(), a.getCounter(), a.getClusterId() + 1);
        assertFalse(a.equals(x3));
        assertFalse(x3.equals(a));
        assertFalse(a.hashCode() == x3.hashCode());
    }

    @Test
    public void compare() throws InterruptedException {
        Revision last = Revision.newRevision(0);
        try {
            last.compareRevisionTime(null);
            fail();
        } catch (NullPointerException e) {
            // expected
        }
        for (int i = 0; i < 1000; i++) {
            Revision r = Revision.newRevision(0);
            assertTrue(r.compareRevisionTime(r) == 0);
            assertTrue(r.compareRevisionTime(last) > 0);
            assertTrue(last.compareRevisionTime(r) < 0);
            last = r;
            if (i % 100 == 0) {
                // ensure the timestamp part changes as well
                Thread.sleep(1);
            }
        }
    }

    @Test
    public void uniqueRevision2() throws Exception {
        List<Thread> threads = new ArrayList<Thread>();
        final AtomicBoolean stop = new AtomicBoolean();
        final Set<Revision> set = Collections
                .synchronizedSet(new HashSet<Revision>());
        final Revision[] duplicate = new Revision[1];
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    Revision[] last = new Revision[1024];
                    while (!stop.get()) {
                        for (Revision r : last) {
                            set.remove(r);
                        }
                        for (int i = 0; i < last.length; i++) {
                            last[i] = Revision.newRevision(1);
                        }
                        for (Revision r : last) {
                            if (!set.add(r)) {
                                duplicate[0] = r;
                            }
                        }
                    }
                }
            });
            thread.start();
            threads.add(thread);
        }
        Thread.sleep(200);
        stop.set(true);
        for (Thread t : threads) {
            t.join();
        }
        assertNull("Duplicate revision", duplicate[0]);
    }

    @Test
    public void uniqueRevision() throws Exception {
        //Revision.setClock(new Clock.Virtual());
        final BlockingQueue<Revision> revisionQueue = Queues.newLinkedBlockingQueue();
        int noOfThreads = 60;
        final int noOfLoops = 1000;
        List<Thread> workers = new ArrayList<Thread>();
        final AtomicBoolean stop = new AtomicBoolean();
        final CountDownLatch startLatch = new CountDownLatch(1);
        final CountDownLatch stopLatch = new CountDownLatch(noOfThreads);
        for (int i = 0; i < noOfThreads; i++) {
            workers.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    Uninterruptibles.awaitUninterruptibly(startLatch);
                    for (int j = 0; j < noOfLoops && !stop.get(); j++) {
                        revisionQueue.add(Revision.newRevision(1));
                    }
                    stopLatch.countDown();
                }
            }));
        }

        final List<Revision> duplicates = Lists.newArrayList();
        final Set<Revision> seenRevs = Sets.newHashSet();
        workers.add(new Thread(new Runnable() {
            @Override
            public void run() {
                startLatch.countDown();

                while (!stop.get()) {
                    List<Revision> revs = Lists.newArrayList();
                    Queues.drainUninterruptibly(revisionQueue, revs, 5, 100, TimeUnit.MILLISECONDS);
                    record(revs);
                }

                List<Revision> revs = Lists.newArrayList();
                revisionQueue.drainTo(revs);
                record(revs);
            }

            private void record(List<Revision> revs) {
                for (Revision rev : revs) {
                    if (!seenRevs.add(rev)) {
                        duplicates.add(rev);
                    }
                }

                if (!duplicates.isEmpty()) {
                    stop.set(true);
                }
            }
        }));

        for (Thread t : workers) {
            t.start();
        }

        stopLatch.await();
        stop.set(true);

        for (Thread t : workers) {
            t.join();
        }
        assertTrue(String.format("Duplicate rev seen %s %n Seen %s", duplicates, seenRevs), duplicates.isEmpty());
    }
}
