/*
   Copyright 2016 Mark Gunlogson

   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 com.github.mgunlogson.cuckoofilter4j;

import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.Random;

import org.junit.Test;

import com.github.mgunlogson.cuckoofilter4j.BucketAndTag;
import com.github.mgunlogson.cuckoofilter4j.IndexTagCalc;
import com.github.mgunlogson.cuckoofilter4j.SerializableSaltedHasher;
import com.github.mgunlogson.cuckoofilter4j.Utils.Algorithm;
import com.google.common.hash.Funnels;
import com.google.common.testing.ClassSanityTester;
import com.google.common.testing.EqualsTester;
import com.google.common.testing.SerializableTester;

public class TestIndexTagCalc {

	@Test(expected = IllegalArgumentException.class)
	public void filterTooBig() {
		IndexTagCalc.create(Algorithm.Murmur3_32, Funnels.integerFunnel(), Long.MAX_VALUE, 1);
	}

	@Test(expected = IllegalArgumentException.class)
	public void filterTooBig2() {
		IndexTagCalc.create(Algorithm.Murmur3_32, Funnels.integerFunnel(), 30000, Integer.MAX_VALUE);
	}

	@Test(expected = IllegalArgumentException.class)
	public void testInvalidArgs() {
		IndexTagCalc.create(Funnels.integerFunnel(), 0, 1);
	}

	@Test(expected = IllegalArgumentException.class)
	public void testInvalidArgs2() {
		IndexTagCalc.create(Funnels.integerFunnel(), 1, 0);
	}

	@Test
	public void knownZeroTags32() {
		// manual instantiation to force salts to be static
		SerializableSaltedHasher<Integer> hasher = new SerializableSaltedHasher<>(0, 0, Funnels.integerFunnel(),
				Algorithm.Murmur3_32);
		IndexTagCalc<Integer> indexer = new IndexTagCalc<>(hasher, 128, 4);
		// find some 0 value tags
		int zeroTags = 0;
		int i = 0;
		ArrayList<Integer> zeroTagInputs = new ArrayList<>();
		while (zeroTags < 20) {
			if (indexer.getTagValue32(hasher.hashObj(i).asInt()) == 0) {
				zeroTagInputs.add(i);
				zeroTags++;
			}
			i++;
		}
		for (Integer tag : zeroTagInputs) {
			// none of the zero value tags should be zero from indexer if
			// rehashing is working properly
			assertFalse(indexer.generate(tag).tag == 0);
		}

	}

	@Test
	public void sanityTagIndexBitsUsed32() {
		// manual instantiation to force salts to be static
		SerializableSaltedHasher<Integer> hasher = new SerializableSaltedHasher<>(0, 0, Funnels.integerFunnel(),
				Algorithm.Murmur3_32);
		IndexTagCalc<Integer> indexer = new IndexTagCalc<>(hasher, 128, 4);
		long setBitsIndex = 0;
		long setBitsTag = 0;
		// should be enough to set all bits being used...
		for (int i = 0; i < 12345; i++) {
			BucketAndTag bt = indexer.generate(i);
			setBitsIndex |= bt.index;
			setBitsTag |= bt.tag;
		}
		// will be true if we're using the right number of bits for tag and
		// index for this calculator
		assertTrue(Long.bitCount(setBitsIndex) == 7);
		assertTrue(Long.bitCount(setBitsTag) == 4);
		// check where the set bits are
		long indexMask = 0b1111111;
		long tagMask = 0b0001111;
		assertTrue(indexMask == setBitsIndex);
		assertTrue(tagMask == setBitsTag);
	}

	@Test
	public void sanityTagIndexBitsUsed64() {
		// manual instantiation to force salts to be static
		SerializableSaltedHasher<Integer> hasher = new SerializableSaltedHasher<>(0, 0, Funnels.integerFunnel(),
				Algorithm.sipHash24);
		IndexTagCalc<Integer> indexer = new IndexTagCalc<>(hasher, (long) Math.pow(2, 31), 32);
		long setBitsIndex = 0;
		long setBitsTag = 0;
		// should be enough to set all bits being used...
		for (int i = 0; i < 1234567; i++) {
			BucketAndTag bt = indexer.generate(i);
			setBitsIndex |= bt.index;
			setBitsTag |= bt.tag;
		}
		// will be true if we're using the right number of bits for tag and
		// index for this calculator
		assertTrue(Long.bitCount(setBitsIndex) == 31);
		assertTrue(Long.bitCount(setBitsTag) == 32);
		// check where the set bits are
		long bitMask32 = -1L >>> 32;// (mask for lower 32 bits set)
		long bitMask31 = bitMask32 >>> 1;// (mask for lower 32 bits set)
		assertTrue(bitMask32 == setBitsTag);
		assertTrue(bitMask31 == setBitsIndex);
	}

	@Test
	public void sanityTagIndexBitsUsed128() {
		// manual instantiation to force salts to be static
		SerializableSaltedHasher<Integer> hasher = new SerializableSaltedHasher<>(0, 0, Funnels.integerFunnel(),
				Algorithm.sha256);
		IndexTagCalc<Integer> indexer = new IndexTagCalc<>(hasher, (long) Math.pow(2, 62), 64);
		long setBitsIndex = 0;
		long setBitsTag = 0;
		// should be enough to set all bits being used...
		for (int i = 0; i < 1234567; i++) {
			BucketAndTag bt = indexer.generate(i);
			setBitsIndex |= bt.index;
			setBitsTag |= bt.tag;
		}
		// will be true if we're using the right number of bits for tag and
		// index for this calculator
		assertTrue(Long.bitCount(setBitsIndex) == 64);
		assertTrue(Long.bitCount(setBitsTag) == 64);
		// check where the set bits are
		long bitMask = -1L;// (mask for all 64 bits set)
		assertTrue(bitMask == setBitsIndex);
		assertTrue(bitMask == setBitsTag);
	}

	@Test
	public void sanityTagIndexNotSame() {
		// manual instantiation to force salts to be static
		SerializableSaltedHasher<Integer> hasher = new SerializableSaltedHasher<>(0, 0, Funnels.integerFunnel(),
				Algorithm.sha256);
		IndexTagCalc<Integer> indexer = new IndexTagCalc<>(hasher, (long) Math.pow(2, 62), 64);
		// should be enough to set all bits being used...
		for (int i = 0; i < 1234567; i += 4) {
			BucketAndTag bt = indexer.generate(i);
			BucketAndTag bt2 = indexer.generate(i + 1);
			// we use two equalities to make collisions super-rare since we
			// otherwise only have 32 bits of hash to compare
			// we're checking for 2 collisions in 2 pairs of 32 bit hash. Should
			// be as hard as getting a single 64 bit collision aka... never
			// happen
			assertTrue(bt.index != bt.tag || bt2.index != bt2.tag);
		}
	}

	@Test
	public void testEquals() {
		new EqualsTester().addEqualityGroup(new IndexTagCalc<Integer>(getUnsaltedHasher(), 128, 4))
				.addEqualityGroup(new IndexTagCalc<Integer>(getUnsaltedHasher(), 256, 4))
				.addEqualityGroup(new IndexTagCalc<Integer>(getUnsaltedHasher(), 128, 6)).testEquals();
	}

	@Test
	public void testEqualsSame() {
		assertTrue(new IndexTagCalc<Integer>(getUnsaltedHasher(), 128, 4)
				.equals(new IndexTagCalc<Integer>(getUnsaltedHasher(), 128, 4)));
	}

	@Test
	public void testCopy() {
		IndexTagCalc<Integer> calc = IndexTagCalc.create(Funnels.integerFunnel(), 128, 4);
		IndexTagCalc<Integer> calcCopy = calc.copy();
		assertTrue(calcCopy.equals(calc));
		assertNotSame(calc, calcCopy);
	}

	@Test
	public void autoTestNulls() {
		// chose 8 for int so it passes bucket and tag size checks
		new ClassSanityTester().setDefault(SerializableSaltedHasher.class, getUnsaltedHasher())
				.setDefault(long.class, 8L).setDefault(int.class, 8).testNulls(IndexTagCalc.class);
	}

	@Test
	public void brokenAltIndex32() {
		Random rando = new Random();
		IndexTagCalc<Integer> calc = IndexTagCalc.create(Funnels.integerFunnel(), 2048, 14);
		for (int i = 0; i < 10000; i++) {
			BucketAndTag pos = calc.generate(rando.nextInt());
			long altIndex = calc.altIndex(pos.index, pos.tag);
			assertTrue(pos.index == calc.altIndex(altIndex, pos.tag));
		}
	}

	@Test
	public void brokenAltIndex64() {
		Random rando = new Random();
		IndexTagCalc<Integer> calc = IndexTagCalc.create(Funnels.integerFunnel(), (long) Math.pow(2, 32), 5);
		for (int i = 0; i < 10000; i++) {
			BucketAndTag pos = calc.generate(rando.nextInt());
			long altIndex = calc.altIndex(pos.index, pos.tag);
			assertTrue(pos.index == calc.altIndex(altIndex, pos.tag));
		}
	}

	@Test
	public void testSerialize() {
		SerializableTester.reserializeAndAssert(new IndexTagCalc<Integer>(getUnsaltedHasher(), 128, 4));
	}

	// using this because otherwise internal state of salts in hasher will
	// prevent equality comparisons
	private SerializableSaltedHasher<Integer> getUnsaltedHasher() {
		return new SerializableSaltedHasher<>(0, 0, Funnels.integerFunnel(), Algorithm.Murmur3_32);
	}

}
