package aalgTest;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import javax.xml.bind.DatatypeConverter;

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

import aalg.crypto.IHashFunction;
import aalg.crypto.Sha512;
import aalg.crypto.Tiger;

public class HashTest {
	
	private static final class TestData {
		public final String input;
		public final String hash;
		
		public TestData (final String input, final String hash) {
			this.input = input;
			this.hash = hash;
		}
	}
	
	private static final class TestCase {
		public final IHashFunction hashFunction;
		public final List<TestData> dataList;
		
		public TestCase (final IHashFunction hashFunction, final List<TestData> dataList) {
			this.hashFunction = hashFunction;
			this.dataList = dataList;
		}
	}
	
	private static final TestCase[] TEST_CASE_ARRAY = {
		new TestCase(
			new Sha512(),
			Arrays.asList(
				new TestData(
					"abc",
					"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
				),
				new TestData(
					"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu",
					"8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909"
				) 
			)
		)
		,
		
		new TestCase(
				new Tiger(),
				Arrays.asList(
						new TestData(
								"",
								"3293ac630c13f0245f92bbb1766e16167a4e58492dde73f3"
							),	
						new TestData(
								"The quick brown fox jumps over the lazy dog",
								"6d12a41e72e644f017b6f0e2f7b44c6285f06dd5d2c5b075"
							),	
						new TestData(
								"Test vector from febooti.com",
								"382599758b759db703d4940c08c3393182adad7e9a7e590f"
								) 
							)
						)	
		};
	
	@Test
	public void correctnessTest() {
		for (TestCase testCase: TEST_CASE_ARRAY) {
			final IHashFunction hashFuntion = testCase.hashFunction;
			
			for (TestData data: testCase.dataList) {
				final byte[] message = data.input.getBytes(Charset.forName("UTF8"));
				final byte[] hash = hashFuntion.calculateHash(message);
				
				final String hashStr = DatatypeConverter.printHexBinary(hash);
				Assert.assertEquals(data.hash.toLowerCase(), hashStr.toLowerCase());
			}
		}
	}
	
	@Test
	public void hashLengthTest() {
		final byte[] message = {};
		
		for (TestCase testCase: TEST_CASE_ARRAY) {
			final IHashFunction hashFuntion = testCase.hashFunction;
			
			final int calculated = hashFuntion.calculateHash(message).length;
			final int expected = hashFuntion.getOutputByteCount();
			
			Assert.assertEquals(expected, calculated);
		}
	}
	
	/**
	 * Verifies that calculating hash at once gives same result as calculating it in more parts.
	 */
	@Test
	public void partialUpdateTest() {
		final int MAX_LENGTH = 1024;
		final int MESSAGE_TEST_COUNT = 20;
		final int DIVISION_TEST_COUNT = 20;
		final Random rnd = new Random(1234);
		
		for (TestCase testCase: TEST_CASE_ARRAY) {
			final IHashFunction hashFuntion = testCase.hashFunction;
			
			for (int i = 0; i < MESSAGE_TEST_COUNT; i++) {
				final int length = rnd.nextInt(MAX_LENGTH);
				final byte[] message = createRandomMessage (rnd, length);
				final byte[] expectedHash = hashFuntion.calculateHash(message);
				
				for (int j = 0; j < DIVISION_TEST_COUNT; j++) {
					final byte[] calculatedHash = calculateHashByParts (message, hashFuntion, rnd);
					
					Assert.assertArrayEquals(expectedHash, calculatedHash);
				}
			}
		}
	}

	private byte[] calculateHashByParts(final byte[] message, final IHashFunction hashFuntion, final Random rnd) {
		hashFuntion.initialize();
		
		int offset = 0;
		
		while (offset < message.length) {
			final int size = rnd.nextInt(message.length - offset) + 1;
			hashFuntion.update(message, offset, size);
			offset += size;
		}
		
		return hashFuntion.finish();
	}

	private byte[] createRandomMessage(final Random rnd, final int length) {
		final byte[] message = new byte[length];
		
		for (int i = 0; i < length; i++) {
			message[i] = (byte) rnd.nextInt();
		}
		
		return message;
	}

}
