/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package eu.dnetlib.miscutils.collections;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;

import org.junit.Test;

/**
 * Tests for BloomFilter.java
 *
 * @author Magnus Skjegstad <magnus@skjegstad.com>
 */
public class BloomFilterTest {
    static Random r = new Random();

    @Test
    public void testConstructorCNK() throws Exception {
        System.out.println("BloomFilter(c,n,k)");

        for (int i = 0; i < 10000; i++) {
            double c = r.nextInt(20) + 1;
            int n = r.nextInt(10000) + 1;
            int k = r.nextInt(20) + 1;
            BloomFilter<?> bf = new BloomFilter<Object>(c, n, k);
            assertEquals(bf.getK(), k);
            assertEquals(bf.getExpectedBitsPerElement(), c, 0);
            assertEquals(bf.getExpectedNumberOfElements(), n);
            assertEquals(bf.size(), c*n, 0);
        }
    }


    /**
     * Test of createHash method, of class BloomFilter.
     * @throws Exception
     */
    @Test
    public void testCreateHash_String() throws Exception {
        System.out.println("createHash");
        String val = UUID.randomUUID().toString();
        int result1 = BloomFilter.createHash(val);
        int result2 = BloomFilter.createHash(val);
        assertEquals(result2, result1);
        int result3 = BloomFilter.createHash(UUID.randomUUID().toString());
        assertNotSame(result3, result2);

        int result4 = BloomFilter.createHash(val.getBytes("UTF-8"));
        assertEquals(result4, result1);
    }

    /**
     * Test of createHash method, of class BloomFilter.
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testCreateHash_byteArr() throws UnsupportedEncodingException {
        System.out.println("createHash");
        String val = UUID.randomUUID().toString();
        byte[] data = val.getBytes("UTF-8");
        int result1 = BloomFilter.createHash(data);
        int result2 = BloomFilter.createHash(val);
        assertEquals(result1, result2);
    }

    /**
     * Test of createHash method, of class BloomFilter.
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testCreateHashes_byteArr() throws UnsupportedEncodingException {
        System.out.println("createHashes");
        String val = UUID.randomUUID().toString();
        byte[] data = val.getBytes("UTF-8");
        int[] result1 = BloomFilter.createHashes(data, 10);
        int[] result2 = BloomFilter.createHashes(data, 10);
        assertEquals(result1.length, 10);
        assertEquals(result2.length, 10);
        assertArrayEquals(result1, result2);
        int[] result3 = BloomFilter.createHashes(data, 5);
        assertEquals(result3.length, 5);
        for (int i = 0; i < result3.length; i++)
            assertEquals(result3[i], result1[i]);

    }

    /**
     * Test of equals method, of class BloomFilter.
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testEquals() throws UnsupportedEncodingException {
        System.out.println("equals");
        BloomFilter<String> instance1 = new BloomFilter<String>(1000, 100);
        BloomFilter<String> instance2 = new BloomFilter<String>(1000, 100);

        for (int i = 0; i < 100; i++) {
            String val = UUID.randomUUID().toString();
            instance1.add(val);
            instance2.add(val);
        }

        assert(instance1.equals(instance2));
        assert(instance2.equals(instance1));

        instance1.add("Another entry"); // make instance1 and instance2 different before clearing

        instance1.clear();
        instance2.clear();

        assert(instance1.equals(instance2));
        assert(instance2.equals(instance1));

        for (int i = 0; i < 100; i++) {
            String val = UUID.randomUUID().toString();     
            instance1.add(val);
            instance2.add(val);
        }

        assertTrue(instance1.equals(instance2));
        assertTrue(instance2.equals(instance1));
    }

    /**
     * Test of hashCode method, of class BloomFilter.
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testHashCode() throws UnsupportedEncodingException {
        System.out.println("hashCode");

        BloomFilter<String> instance1 = new BloomFilter<String>(1000, 100);
        BloomFilter<String> instance2 = new BloomFilter<String>(1000, 100);

        assertTrue(instance1.hashCode() == instance2.hashCode());

        for (int i = 0; i < 100; i++) {
            String val = UUID.randomUUID().toString();
            instance1.add(val);
            instance2.add(val);
        }

        assertTrue(instance1.hashCode() == instance2.hashCode());

        instance1.clear();
        instance2.clear();

        assertTrue(instance1.hashCode() == instance2.hashCode());

        instance1 = new BloomFilter<String>(100, 10);
        instance2 = new BloomFilter<String>(100, 9);
        assertFalse(instance1.hashCode() == instance2.hashCode());

        instance1 = new BloomFilter<String>(100, 10);
        instance2 = new BloomFilter<String>(99, 9);
        assertFalse(instance1.hashCode() == instance2.hashCode());

        instance1 = new BloomFilter<String>(100, 10);
        instance2 = new BloomFilter<String>(50, 10);
        assertFalse(instance1.hashCode() == instance2.hashCode());
    }

    /**
     * Test of expectedFalsePositiveProbability method, of class BloomFilter.
     */
    @Test
    public void testExpectedFalsePositiveProbability() {
        // These probabilities are taken from the bloom filter probability table at
        // http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
        System.out.println("expectedFalsePositiveProbability");
        BloomFilter<?> instance = new BloomFilter<Object>(1000, 100);
        double expResult = 0.00819; // m/n=10, k=7
        double result = instance.expectedFalsePositiveProbability();
        assertEquals(instance.getK(), 7);
        assertEquals(expResult, result, 0.000009);

        instance = new BloomFilter<Object>(100, 10);
        expResult = 0.00819; // m/n=10, k=7
        result = instance.expectedFalsePositiveProbability();
        assertEquals(instance.getK(), 7);
        assertEquals(expResult, result, 0.000009);

        instance = new BloomFilter<Object>(20, 10);
        expResult = 0.393; // m/n=2, k=1
        result = instance.expectedFalsePositiveProbability();
        assertEquals(1, instance.getK());
        assertEquals(expResult, result, 0.0005);

        instance = new BloomFilter<Object>(110, 10);
        expResult = 0.00509; // m/n=11, k=8
        result = instance.expectedFalsePositiveProbability();
        assertEquals(8, instance.getK());
        assertEquals(expResult, result, 0.00001);
    }

    /**
     * Test of clear method, of class BloomFilter.
     */
    @Test
    public void testClear() {
        System.out.println("clear");
        BloomFilter<?> instance = new BloomFilter<Object>(1000, 100);
        for (int i = 0; i < instance.size(); i++)
            instance.setBit(i, true);
        instance.clear();
        for (int i = 0; i < instance.size(); i++)
            assertSame(instance.getBit(i), false);
    }

    /**
     * Test of add method, of class BloomFilter.
     * @throws Exception
     */
    @Test
    public void testAdd() throws Exception {
        System.out.println("add");
        BloomFilter<String> instance = new BloomFilter<String>(1000, 100);

        for (int i = 0; i < 100; i++) {
            String val = UUID.randomUUID().toString();
            instance.add(val);
            assert(instance.contains(val));
        }
    }

    /**
     * Test of addAll method, of class BloomFilter.
     * @throws Exception
     */
    @Test
    public void testAddAll() throws Exception {
        System.out.println("addAll");
        List<String> v = new ArrayList<String>();
        BloomFilter<String> instance = new BloomFilter<String>(1000, 100);

        for (int i = 0; i < 100; i++)
            v.add(UUID.randomUUID().toString());

        instance.addAll(v);

        for (int i = 0; i < 100; i++)
            assert(instance.contains(v.get(i)));
    }

    /**
     * Test of contains method, of class BloomFilter.
     * @throws Exception
     */
    @Test
    public void testContains() throws Exception {
        System.out.println("contains");
        BloomFilter<String> instance = new BloomFilter<String>(10000, 10);

        for (int i = 0; i < 10; i++) {
            instance.add(Integer.toBinaryString(i));
            assert(instance.contains(Integer.toBinaryString(i)));
        }

        assertFalse(instance.contains(UUID.randomUUID().toString()));
    }

    /**
     * Test of containsAll method, of class BloomFilter.
     * @throws Exception
     */
    @Test
    public void testContainsAll() throws Exception {
        System.out.println("containsAll");
        List<String> v = new ArrayList<String>();
        BloomFilter<String> instance = new BloomFilter<String>(1000, 100);

        for (int i = 0; i < 100; i++) {
            v.add(UUID.randomUUID().toString());
            instance.add(v.get(i));
        }

        assert(instance.containsAll(v));
    }

    /**
     * Test of getBit method, of class BloomFilter.
     */
    @Test
    public void testGetBit() {
        System.out.println("getBit");
        BloomFilter<?> instance = new BloomFilter<Object>(1000, 100);
        Random r = new Random();

        for (int i = 0; i < 100; i++) {
            boolean b = r.nextBoolean();
            instance.setBit(i, b);
            assertSame(instance.getBit(i), b);
        }
    }

    /**
     * Test of setBit method, of class BloomFilter.
     */
    @Test
    public void testSetBit() {
        System.out.println("setBit");

        BloomFilter<?> instance = new BloomFilter<Object>(1000, 100);

        for (int i = 0; i < 100; i++) {
            instance.setBit(i, true);
            assertSame(instance.getBit(i), true);
        }

        for (int i = 0; i < 100; i++) {
            instance.setBit(i, false);
            assertSame(instance.getBit(i), false);
        }
    }

    /**
     * Test of size method, of class BloomFilter.
     */
    @Test
    public void testSize() {
        System.out.println("size");
        for (int i = 100; i < 1000; i++) {
            BloomFilter<?> instance = new BloomFilter<Object>(i, 10);
            assertEquals(instance.size(), i);
        }
    }

    /** Test error rate *
     * @throws UnsupportedEncodingException
     */
    @Test
    public void testFalsePositiveRate1() throws UnsupportedEncodingException {
        // Numbers are from // http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
        System.out.println("falsePositiveRate1");

        for (int j = 10; j < 21; j++) {
            System.out.print(j-9 + "/11");
            List<byte[]> v = new ArrayList<byte[]>();
            BloomFilter<Object> instance = new BloomFilter<Object>(100*j,100);

            for (int i = 0; i < 100; i++) {
                byte[] bytes = new byte[100];
                r.nextBytes(bytes);
                v.add(bytes);
            }
            instance.addAll(v);

            long f = 0;
            double tests = 300000;
            for (int i = 0; i < tests; i++) {
                byte[] bytes = new byte[100];
                r.nextBytes(bytes);
                if (instance.contains(bytes)) {
                    if (!v.contains(bytes)) {
                        f++;
                    }
                }
            }

            double ratio = f / tests;

            System.out.println(" - got " + ratio + ", math says " + instance.expectedFalsePositiveProbability());
            assertEquals(instance.expectedFalsePositiveProbability(), ratio, 0.01);
        }
    }

    /** Test for correct k **/
    @Test
    public void testGetK() {
        // Numbers are from http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
        System.out.println("testGetK");
        BloomFilter<?> instance = null;

        instance = new BloomFilter<Object>(2, 1);
        assertEquals(1, instance.getK());

        instance = new BloomFilter<Object>(3, 1);
        assertEquals(2, instance.getK());

        instance = new BloomFilter<Object>(4, 1);
        assertEquals(3, instance.getK());

        instance = new BloomFilter<Object>(5, 1);
        assertEquals(3, instance.getK());

        instance = new BloomFilter<Object>(6, 1);
        assertEquals(4, instance.getK());

        instance = new BloomFilter<Object>(7, 1);
        assertEquals(5, instance.getK());

        instance = new BloomFilter<Object>(8, 1);
        assertEquals(6, instance.getK());

        instance = new BloomFilter<Object>(9, 1);
        assertEquals(6, instance.getK());

        instance = new BloomFilter<Object>(10, 1);
        assertEquals(7, instance.getK());

        instance = new BloomFilter<Object>(11, 1);
        assertEquals(8, instance.getK());

        instance = new BloomFilter<Object>(12, 1);
        assertEquals(8, instance.getK());
    }
    
    /**
     * Test of contains method, of class BloomFilter.
     */
    @Test
    public void testContains_GenericType() {
        System.out.println("contains");
        int items = 100;
        BloomFilter<String> instance = new BloomFilter<String>(0.01, items);

        for (int i = 0; i < items; i++) {
            String s = UUID.randomUUID().toString();
            instance.add(s);
            assertTrue(instance.contains(s));
        }
    }

    /**
     * Test of contains method, of class BloomFilter.
     */
    @Test
    public void testContains_byteArr() {
        System.out.println("contains");

        int items = 100;
        BloomFilter<?> instance = new BloomFilter<Object>(0.01, items);

        for (int i = 0; i < items; i++) {
            byte[] bytes = new byte[500];
            r.nextBytes(bytes);
            instance.add(bytes);
            assertTrue(instance.contains(bytes));
        }
    }

    /**
     * Test of count method, of class BloomFilter.
     */
    @Test
    public void testCount() {
        System.out.println("count");
        int expResult = 100;
        BloomFilter<String> instance = new BloomFilter<String>(0.01, expResult);
        for (int i = 0; i < expResult; i++) {
            byte[] bytes = new byte[100];
            r.nextBytes(bytes);
            instance.add(bytes);
        }
        int result = instance.count();
        assertEquals(expResult, result);

        instance = new BloomFilter<String>(0.01, expResult);
        for (int i = 0; i < expResult; i++) {
            instance.add(UUID.randomUUID().toString());
        }
        result = instance.count();
        assertEquals(expResult, result);
    }


}