package com.conversantmedia.util.concurrent;

/*
 * #%L
 * Conversant Disruptor
 * ~~
 * Conversantmedia.com © 2016, Conversant, Inc. Conversant® is a trademark of Conversant, Inc.
 * ~~
 * 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.
 * #L%
 */

import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author John Cairns <jcairns@dotomi.com>
 *         Date: 4//25/12
 *         Time: 3:27 PM
 */
public class PushPullBlockingQueueTest {

    final static boolean ALLOW_LONG_RUN = false;

    @Test
    public void testPushPullBlockingQueueTestC1() {
        final int cap = 10;
        BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);
        while(dbq.offer(Integer.valueOf(0)));
        Assert.assertEquals(16, dbq.size());
    }

    @Test
    public void testPushPullBlockingQueueTestC2() {

        final int cap = 50;

        Set<Integer> x = new HashSet<Integer>(cap);
        for(int i=0; i<2*cap; i++) {
            x.add(Integer.valueOf(i));
        }

        BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap, x);
        // next power of two
        Assert.assertEquals(64, dbq.size());
    }

    @Test
    public void testOffer() {

        final int cap = 16;
        BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);
        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        Assert.assertFalse(dbq.offer(Integer.valueOf(cap)));

        for(int i=0; i<cap; i++) {
            Assert.assertEquals(Integer.valueOf(i), dbq.poll());
        }

    }



    @Test
    public void remove() {

        final int cap = 10;
        BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);
        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        Integer i = dbq.peek();
        Integer x = dbq.remove();

        Assert.assertEquals(i, x);
        Assert.assertEquals(i, Integer.valueOf(0));
        Assert.assertFalse(i.equals(dbq.peek()));
    }

    @Test
    public void testPoll() {
        final int cap = 10;
        BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        Assert.assertNull(dbq.poll());

        dbq.offer(Integer.valueOf(1));
        dbq.offer(Integer.valueOf(2));
        Assert.assertEquals(dbq.poll(), Integer.valueOf(1));
        Assert.assertEquals(dbq.poll(), Integer.valueOf(2));

        Assert.assertNull(dbq.poll());
    }

    @Test
    public void testElement() {
        final int cap = 10;
        BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        try {
            dbq.element();
            Assert.fail();
        } catch(NoSuchElementException ex) {
            // expected
        }
    }


    @Test
    public void testPeek() {
        final int cap = 10;
        BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        try {

            Assert.assertNull(dbq.peek());

        } catch(NoSuchElementException nsex) {
            Assert.fail();
        }

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
            Assert.assertEquals(Integer.valueOf(0), dbq.peek());
        }

        for(int i=0; i<cap; i++) {
            Assert.assertEquals(Integer.valueOf(i), dbq.peek());
            dbq.poll(); // count up values checking peeks
        }
    }


    @Test
    public void testPut() throws InterruptedException {

        final int cap = 10;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        new Thread(){
            @Override
            public void run() {
                try {
                    sleep(1000);
                    // after a second remove one
                    dbq.poll();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();


        // in main thread add one
        // this operation must wait for thread
        dbq.put(Integer.valueOf(cap));

        boolean hasValCap = false;
        while(!dbq.isEmpty()) {
            if(dbq.poll().equals(Integer.valueOf(cap)))
                hasValCap = true;
        }
        Assert.assertTrue(hasValCap);

    }

    @Ignore // this test flickers in @ParallelRunner
    public void testTimeOffer() throws InterruptedException {

        final int cap = 16;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        new Thread(){
            @Override
            public void run() {
                try {
                    sleep(1000);
                    // after a second remove one
                    dbq.poll();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();



        // expect to fail for only 50 ms
        Assert.assertFalse(dbq.offer(Integer.valueOf(cap), 50, TimeUnit.MILLISECONDS));

        Assert.assertTrue(dbq.offer(Integer.valueOf(cap), 1550, TimeUnit.MILLISECONDS));

        boolean hasValCap = false;
        while(!dbq.isEmpty()) {
            if(dbq.poll().equals(Integer.valueOf(cap)))
                hasValCap = true;
        }
        Assert.assertTrue(hasValCap);
    }

    @Test
    public void testTake() throws InterruptedException {

        final int cap = 10;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        new Thread(){
            @Override
            public void run() {
                try {
                    sleep(1000);
                    // after a second remove one
                    dbq.offer(Integer.valueOf(cap));
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        // wait for value to be added
        Assert.assertEquals(Integer.valueOf(cap), dbq.take());

    }

    @Test
    public void testTimePoll() throws InterruptedException {
        final int cap = 10;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        new Thread(){
            @Override
            public void run() {
                try {
                    sleep(500);
                    // after a second remove one
                    dbq.offer(Integer.valueOf(cap));
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        // wait for value to be added
        Assert.assertNull(dbq.poll(50, TimeUnit.MICROSECONDS));
        Assert.assertEquals(Integer.valueOf(cap),dbq.poll(50, TimeUnit.SECONDS) );
    }

    @Test
    public void testRemainingCapacity() {
        final int cap = 128;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            Assert.assertEquals(cap-i, dbq.remainingCapacity());
            dbq.offer(Integer.valueOf(i));
        }

    }

    @Test
    public void testDrainToC() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        final List<Integer> c= new LinkedList();
        Assert.assertEquals(dbq.drainTo(c), cap);
        int i=0;
        for(final Integer a : c) {
            Assert.assertEquals(a, Integer.valueOf(i++));
        }


    }

    @Test
    public void drainToToCMax() {

        final int cap = 100;
        final int max = 75;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        final List<Integer> c= new LinkedList();
        Assert.assertEquals(dbq.drainTo(c, max), max);
        Assert.assertEquals(c.size(), max);
        int i=0;
        for(final Integer a : c) {
            Assert.assertEquals(a, Integer.valueOf(i++));
        }
    }

    @Test
    public void testSize() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        Assert.assertEquals(0, dbq.size());

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
            Assert.assertEquals(i+1, dbq.size());
        }

        Assert.assertEquals(cap, dbq.size());

        for(int i=0; i<cap; i++) {
            Assert.assertEquals(dbq.size(), cap-i);
            dbq.poll();
        }

        Assert.assertEquals(0, dbq.size());

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
            Assert.assertEquals(i+1, dbq.size());
        }

        Assert.assertEquals(cap, dbq.size());

        for(int i=0; i<cap; i++) {
            Assert.assertEquals(dbq.size(), cap-i);
            dbq.poll();
        }

    }

    @Test
    public void testIsEmpty() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        Assert.assertTrue(dbq.isEmpty());

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
            Assert.assertFalse(dbq.isEmpty());
        }

        for(int i=0; i<cap; i++) {
            Assert.assertFalse(dbq.isEmpty());
            dbq.poll();
        }

        Assert.assertTrue(dbq.isEmpty());

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
            Assert.assertFalse(dbq.isEmpty());
        }

        for(int i=0; i<cap; i++) {
            Assert.assertFalse(dbq.isEmpty());
            dbq.poll();
        }

        Assert.assertTrue(dbq.isEmpty());
    }

    @Test
    public void testContains() {

        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            Assert.assertFalse(dbq.contains(Integer.valueOf(i)));
        }


        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        for(int i=0; i<cap; i++) {
            Assert.assertTrue(dbq.contains(Integer.valueOf(i)));
        }


        for(int i=cap; i<2*cap; i++) {
            Assert.assertFalse(dbq.contains(Integer.valueOf(i)));
        }
    }

    @Test
    public void testToArray() {

        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {

            dbq.offer(Integer.valueOf(i));
        }

        Object[] objArray = dbq.toArray();
        for(int i=0; i<cap; i++) {
            Assert.assertEquals(objArray[i], Integer.valueOf(i));
        }

    }

    @Test
    public void testAdd() {
        final int cap = 16;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {

            dbq.add(Integer.valueOf(i));
        }

        try {
            dbq.add(Integer.valueOf(cap));
            Assert.fail();
        } catch(IllegalStateException ex) {
            // expected;
        }
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testRemoveObj() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {

            dbq.offer(Integer.valueOf(i));
        }

        for(int i=0; i<cap; i+=2) {
            dbq.remove(Integer.valueOf(i));
        }

        Assert.assertEquals(dbq.size(), cap/2);

        for(int i=1; i<cap; i+=2) {
            Assert.assertEquals(Integer.valueOf(i), dbq.poll());
        }
    }


    @Test(expected = UnsupportedOperationException.class)
    public void testRemoveObjDups() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            // all just zeros and ones
            dbq.offer(Integer.valueOf(i&1));
        }

        // nothing removed
        dbq.remove(Integer.valueOf(777));

        Assert.assertEquals(dbq.size(), cap);

        dbq.remove(Integer.valueOf(1));

        Assert.assertEquals(dbq.size(), cap/2);

        for(int i=1; i<cap; i+=2) {
            Assert.assertEquals(Integer.valueOf(0), dbq.poll());
        }
    }


    @Test
    public void testContainsAll() {

        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {

            dbq.offer(Integer.valueOf(i));
        }

        Set<Integer> si = new HashSet(10);
        for(int i=0; i<cap/10; i++) {
            si.add(Integer.valueOf(i));
        }
        Assert.assertTrue(dbq.containsAll(si));

        si.add(Integer.valueOf(-1));
        Assert.assertFalse(dbq.containsAll(si));
        si.remove(-1);
        dbq.clear();
        Assert.assertFalse(dbq.containsAll(si));
    }

    @Test
    public void testAddAll() {

        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        Set<Integer> si = new HashSet(cap);
        for(int i=0; i<cap/10; i++) {
            si.add(Integer.valueOf(i));
        }
        dbq.addAll(si);
        Assert.assertTrue(dbq.containsAll(si));

        Set<Integer> ni = new HashSet(cap);
        for(int i=0; i<cap/10; i++) {
            ni.add(Integer.valueOf(-i));
        }
        dbq.addAll(ni);
        Assert.assertTrue(dbq.containsAll(si));
        Assert.assertTrue(dbq.containsAll(ni));

        for(int i=2*cap/10; i<2*cap; i++) {
            si.add(Integer.valueOf(i));
        }
        dbq.addAll(si);
        Assert.assertEquals(dbq.size(), 128);
    }

    @Test
    public void testAddAllReturn() {

        final int cap = 8;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<>(cap);

        final Set<Integer> set = new HashSet();

        for(int i=0; i<8; i++) {
            set.add(i);
        }

        Assert.assertTrue(dbq.addAll(set));

        Integer iVal = dbq.poll();
        while(iVal != null) {
            Assert.assertTrue(set.contains(iVal));
            iVal = dbq.poll();
        }

        for(int i=0; i<20; i++) {
            set.add(i);
        }

        // at least one will fail
        Assert.assertTrue(dbq.addAll(set));
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testRemoveIsNotSupported() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        dbq.remove(0);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testRemoveAllIsNotSupported() {

        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        dbq.removeAll(Collections.emptySet());
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testRetainAllIsNotSupported() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        dbq.retainAll(Collections.emptySet());
    }


    @Test(timeout=30000)
    public void testOverflowingOffers() throws InterruptedException {
        // there is a problem in the native implementation
        // of null overwriting the added value on the last bucket
        // when the queue was just full
        final int NRUN = 5_000;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(5000);
        final Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0; i<NRUN; i++) {
                    while(!dbq.offer(i)) {
                        Thread.yield(); // fast
                    }
                }
            }
        });

        t.start();

        int i;
        for(i=0; i<NRUN; i++) {
            Integer j=null;
            while(j==null) {
                try {
                    Thread.sleep(1); // slower
                } catch(InterruptedException e) {
                    // ignore!
                }
                j = dbq.poll();
            }
        }

        t.join();

        Assert.assertEquals(NRUN, i);
    }

    @Test
    public void testClear() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        Set<Integer> si = new HashSet(cap);
        for(int i=0; i<cap/10; i++) {
            si.add(Integer.valueOf(i));
        }

        Assert.assertTrue(dbq.containsAll(si));
        dbq.clear();
        Assert.assertFalse(dbq.containsAll(si));
        Assert.assertEquals(0, dbq.size());
        Assert.assertTrue(dbq.isEmpty());
        Assert.assertNull(dbq.poll());
    }

    @Test
    public void testIterator() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        int i=0;
        for(final Integer c : dbq) {
            Assert.assertEquals(Integer.valueOf(i++), c);
        }
    }

    @Test
    public void testTypeToArray() {
        final int cap = 100;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

        for(int i=0; i<cap; i++) {
            dbq.offer(Integer.valueOf(i));
        }

        Integer[] t = new Integer[cap];
        dbq.toArray(t);
        for(int i=0; i<cap; i++) {
            Assert.assertEquals(Integer.valueOf(i), t[i]);
        }
    }


    @Test
    public void textIntMaxValue() {

        // the blocking queue depends on sequence numbers that are integers
        // be sure the blocking queue operates normally over
        // a range spanning integer values

        if(ALLOW_LONG_RUN) {
            final int cap = 3;
            final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(cap);

            long  nIter = 0;

            for(int i=0; i<Integer.MAX_VALUE; i++) {

                for(int a=0; a<cap; a++) {
                    Assert.assertEquals(dbq.size(), a);
                    dbq.offer(Integer.valueOf(a));
                    nIter++;
                }

                for(int a=0; a<cap; a++) {
                    Assert.assertEquals(dbq.size(), cap-a);
                    Assert.assertEquals("At i="+i, dbq.poll(),Integer.valueOf(a));
                }

                if(nIter % Integer.MAX_VALUE == 0) System.out.println(nIter+"times MAX_VALUE");

            }
        } else {
            System.out.println("max value test not executed");
        }
    }

    @Test
    public void testSequentialFeed() throws InterruptedException {

        final int feedCount = 2*8192;
        final BlockingQueue<Integer> dbq = new PushPullBlockingQueue<Integer>(128);
        final AtomicInteger nFed = new AtomicInteger(0);
        final AtomicInteger nRead = new AtomicInteger(0);


        final int nFeeders = 1;

        final Thread[] f=new Thread[nFeeders];
        for(int i=0; i<nFeeders; i++) {
            f[i] = new Thread(){
                @Override
                public void run() {
                    try {
                        for(int i = 0; i<feedCount/nFeeders; i++) {
                            while(!dbq.offer(i, 50L, TimeUnit.MICROSECONDS)) yield();
                            nFed.incrementAndGet();
                        }
                    } catch(InterruptedException ex) {

                    }
                }
            };
            f[i].start();
        }

        final int nReaders = 1;
        Thread[] t = new Thread[nReaders];
        for(int i=0; i<nReaders; i++) {
            t[i]=new Thread(){
                @Override
                public void run() {
                    try {
                        while(nRead.get()<feedCount) {
                            Integer r;
                            do {
                                r = dbq.poll(50, TimeUnit.MILLISECONDS);
                                if(r == null) yield();
                            } while((r == null) && (nRead.get()<feedCount));
                            if(r != null) {
                                // we can't control which thread will return
                                // first, but the expected still must be within
                                // the number of threads range
                                Assert.assertTrue(r.intValue()<=nRead.get()+nReaders+1);
                                nRead.incrementAndGet();
                            }
                        }
                    } catch(InterruptedException ex) {

                    }
                }
            };
            t[i].start();
        }
        for(int i=0;i<nFeeders;i++) {
            f[i].join();
        }
        for(int i=0;i<nReaders;i++) {
            t[i].join();
        }

        Assert.assertEquals(nFed.get(), nRead.get());


    }
}
