package com.github.kilianB.dataStrorage.tree;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.PriorityQueue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import com.github.kilianB.TestResources;
import com.github.kilianB.datastructures.tree.Result;
import com.github.kilianB.datastructures.tree.binaryTree.BinaryTree;
import com.github.kilianB.hash.Hash;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class BinaryTreeTest {

	private BinaryTree binTree;

	@BeforeEach
	public void createTree() {
		binTree = new BinaryTree(true);
	}

	@Test
	public void searchExactItem() {
		Hash hash = TestResources.createHash("101010100011", 0);

		binTree.addHash(hash, 1);
		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(hash, 100);

		Result r = results.peek();
		assertEquals(1, r.value);
		assertEquals(0, r.distance);
	}

	@Test
	public void searchExactItemZero() {
		// Doesn't fail
		String bits = "00001010";
		Hash hash = TestResources.createHash(bits, 0);
		binTree.addHash(hash, 0);
		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(hash, 0);
		assertEquals(1, results.size());
	}

	@Test
	public void searchDistantItemZero() {
		// Doesn't fail
		String bits = "00001010";
		String bits1 = "10001010";
		Hash hash = TestResources.createHash(bits, 0);
		Hash hash1 = TestResources.createHash(bits1, 0);

		binTree.addHash(hash, 0);
		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(hash1, 1);
		assertEquals(1, results.size());
	}

	@Test
	public void searchDistantItem() {
		Hash hash = TestResources.createHash("101010100011", 0);
		Hash needle = TestResources.createHash("101010101111", 0);

		binTree.addHash(hash, 1);
		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(needle, 100);

		Result r = results.peek();
		assertEquals(1, r.value);
		assertEquals(2, r.distance);
	}

	@Test
	public void searchDistantItemFail() {
		Hash hash = TestResources.createHash("101010100011", 0);
		Hash needle = TestResources.createHash("101010101111", 0);

		binTree.addHash(hash, 1);
		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(needle, 1);

		assertEquals(0, results.size());
	}

	@Test
	public void searchDistanceExact() {
		Hash hash = TestResources.createHash("101010100011", 0);
		Hash needle = TestResources.createHash("101010101111", 0);

		binTree.addHash(hash, 1);
		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(needle, 2);

		assertEquals(1, results.size());
	}

	@Test
	public void searchItemMultipleValues() {
		Hash hash = TestResources.createHash("101010100011", 0);

		binTree.addHash(hash, 1);
		binTree.addHash(hash, 2);
		binTree.addHash(hash, 3);

		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(hash, 0);

		assertEquals(3, results.size());
	}

	@Test
	public void searchItemMultipleValuesExact() {
		Hash hash = TestResources.createHash("101010100011", 0);
		Hash hash1 = TestResources.createHash("101010100010", 0);

		binTree.addHash(hash, 1);
		binTree.addHash(hash, 2);
		binTree.addHash(hash, 3);
		binTree.addHash(hash1, 3);
		binTree.addHash(hash1, 3);
		binTree.addHash(hash1, 3);

		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(hash, 0);

		assertEquals(3, results.size());
	}

	@Test
	public void searchItemMultipleValues2() {
		Hash hash = TestResources.createHash("101010100011", 0);
		Hash hash1 = TestResources.createHash("101010100010", 0);

		binTree.addHash(hash, 1);
		binTree.addHash(hash, 2);
		binTree.addHash(hash, 3);
		binTree.addHash(hash1, 3);
		binTree.addHash(hash1, 3);
		binTree.addHash(hash1, 3);

		PriorityQueue<Result> results = binTree.getElementsWithinHammingDistance(hash, 2);

		assertEquals(6, results.size());
	}

	@Test
	public void ensureHashConsistencyAdd() {

		Hash hash = TestResources.createHash("101010100011", 250);
		Hash hash1 = TestResources.createHash("101010100010", 251);

		binTree.addHash(hash, 0);
		assertThrows(IllegalStateException.class, () -> {
			binTree.addHash(hash1, 0);
		});
	}

	@Test
	public void ensureHashConsistencySearch() {
		Hash hash = TestResources.createHash("101010100011", 250);
		Hash hash1 = TestResources.createHash("101010100010", 251);

		binTree.addHash(hash, 0);
		assertThrows(IllegalStateException.class, () -> {
			binTree.getElementsWithinHammingDistance(hash1, 10);
		});
	}

	@Test
	public void addedHashCount() {
		assertEquals(0, binTree.getHashCount());

		Hash hash = TestResources.createHash("101010100011", 250);
		Hash hash1 = TestResources.createHash("101010100010", 250);

		binTree.addHash(hash, 0);
		assertEquals(1, binTree.getHashCount());
		binTree.addHash(hash1, 0);
		assertEquals(2, binTree.getHashCount());

		// Adding the same hash will increase the hash count
		binTree.addHash(hash, 0);
		assertEquals(3, binTree.getHashCount());
	}

	@Nested
	class NearestNeightbour {

		@Test
		public void searchItemExact() {
			Hash hash = TestResources.createHash("101010100011", 0);
			binTree.addHash(hash, 1);
			List<Result> r = binTree.getNearestNeighbour(hash);
			assertEquals(1, r.get(0).value);
			assertEquals(0, r.get(0).distance);
		}

		@Test
		public void searchDistantItem() {
			Hash hash = TestResources.createHash("101010100011", 0);
			Hash needle = TestResources.createHash("101010101111", 0);

			binTree.addHash(hash, 1);

			List<Result> results = binTree.getNearestNeighbour(needle);

			assertEquals(1, results.size());

			Result r = results.get(0);
			assertEquals(1, r.value);
			assertEquals(2, r.distance);
		}

		@Test
		public void nearestItem() {
			Hash needle = TestResources.createHash("00001", 0);

			binTree.addHash(TestResources.createHash("10000", 0), 0);
			binTree.addHash(TestResources.createHash("11111", 0), 1);

			List<Result> results = binTree.getNearestNeighbour(needle);

			Result r = results.get(0);
			assertEquals(0, r.value);
			assertEquals(2, r.distance);
		}

		@Test
		public void multipleValues() {
			Hash needle = TestResources.createHash("00001", 0);

			binTree.addHash(TestResources.createHash("10000", 0), 0);
			binTree.addHash(TestResources.createHash("10000", 0), 2);
			binTree.addHash(TestResources.createHash("11111", 0), 1);

			List<Result> results = binTree.getNearestNeighbour(needle);

			assertEquals(2, results.size());

			Result r = results.get(0);
			Result r2 = results.get(0);

			assertTrue(((int) r.value == 0) || (int) r.value == 2);
			assertTrue(((int) r2.value == 0 || (int) r2.value == 2));
		}

		
		@Test
		public void equidistant() {
			Hash needle = TestResources.createHash("00001", 0);

			binTree.addHash(TestResources.createHash("00010", 0), 0);
			binTree.addHash(TestResources.createHash("00100", 0), 2);
			binTree.addHash(TestResources.createHash("11111", 0), 1);

			List<Result> results = binTree.getNearestNeighbour(needle);

			assertEquals(2, results.size());

			Result r = results.get(0);
			Result r2 = results.get(0);

			assertTrue(((int) r.value == 0) || (int) r.value == 2);
			assertTrue(((int) r2.value == 0 || (int) r2.value == 2));
		}
	}
}