package test;

import static org.junit.Assert.*;

import java.math.BigDecimal;
import java.util.Arrays;

import ntp.NTP;

import org.junit.Test;

import database.DBSet;
import qora.BlockGenerator;
import qora.account.Account;
import qora.account.PrivateKeyAccount;
import qora.block.Block;
import qora.block.BlockFactory;
import qora.block.GenesisBlock;
import qora.crypto.Crypto;
import qora.transaction.GenesisTransaction;
import qora.transaction.PaymentTransaction;
import qora.transaction.Transaction;

public class BlockTests
{
	@Test
	public void validateSignatureGenesisBlock()
	{
		Block genesisBlock = new GenesisBlock();
		
		//CHECK IF SIGNATURE VALID
		assertEquals(true, genesisBlock.isSignatureValid());
	}
	
	@Test
	public void validateGenesisBlock()
	{
		//CREATE EMPTY DATABASE
		DBSet databaseSet = DBSet.createEmptyDatabaseSet();
		
		//CREATE GENESIS BLOCK
		Block genesisBlock = new GenesisBlock();
		
		//CHECK IF VALID
		assertEquals(true, genesisBlock.isValid(databaseSet));
		
		//ADD INVALID GENESIS TRANSACTION
		Transaction transaction = new GenesisTransaction(new Account("XUi2oga2pnGNcZ9es6pBqxydtRZKWdkL2g"), BigDecimal.valueOf(-1000).setScale(8), NTP.getTime());
		genesisBlock.addTransaction(transaction);
		
		//CHECK IF INVALID
		assertEquals(false, genesisBlock.isValid(databaseSet));
		
		//CREATE NEW BLOCK
		genesisBlock = new GenesisBlock();
		
		//CHECK IF VALID
		assertEquals(true, genesisBlock.isValid(databaseSet));
		
		//PROCESS
		genesisBlock.process(databaseSet);
		
		//CHECK IF INVALID
		assertEquals(false, genesisBlock.isValid(databaseSet));
	}
	
	@Test
	public void parseGenesisBlock()
	{
		//CREATE VALID BLOCK
		Block genesisBlock = new GenesisBlock();
				
		//CONVERT TO BYTES
		byte[] rawBlock = genesisBlock.toBytes();
				
		try 
		{	
			//PARSE FROM BYTES
			Block parsedBlock = BlockFactory.getInstance().parse(rawBlock);
					
			//CHECK INSTANCE
			assertEquals(true, parsedBlock instanceof GenesisBlock);
					
			//CHECK SIGNATURE
			assertEquals(true, Arrays.equals(genesisBlock.getSignature(), parsedBlock.getSignature()));
					
			//CHECK GENERATOR
			assertEquals(genesisBlock.getGenerator().getAddress(), parsedBlock.getGenerator().getAddress());	
					
			//CHECK BASE TARGET
			assertEquals(genesisBlock.getGeneratingBalance(), parsedBlock.getGeneratingBalance());	
			
			//CHECK FEE
			assertEquals(genesisBlock.getTotalFee(), parsedBlock.getTotalFee());	
					
			//CHECK REFERENCE
			assertEquals(true, Arrays.equals(genesisBlock.getReference(), parsedBlock.getReference()));	
					
			//CHECK TIMESTAMP
			assertEquals(genesisBlock.getTimestamp(), parsedBlock.getTimestamp());

			//CHECK TRANSACTION COUNT
			assertEquals(genesisBlock.getTransactionCount(), parsedBlock.getTransactionCount());
		}
		catch (Exception e) 
		{
			fail("Exception while parsing transaction.");
		}
				
		//PARSE TRANSACTION FROM WRONG BYTES
		rawBlock = new byte[50];
		
		try 
		{	
			//PARSE FROM BYTES
			BlockFactory.getInstance().parse(rawBlock);
					
			//FAIL
			fail("this should throw an exception");
		}
		catch (Exception e) 
		{
			//EXCEPTION IS THROWN OKE
		}	
	}
	
	@Test
	public void validateSignatureBlock()
	{
		//CREATE EMPTY MEMORY DATABASE
		DBSet databaseSet = DBSet.createEmptyDatabaseSet();
				
		//PROCESS GENESISBLOCK
		GenesisBlock genesisBlock = new GenesisBlock();
		genesisBlock.process(databaseSet);
				
		//CREATE KNOWN ACCOUNT
		byte[] seed = Crypto.getInstance().digest("test".getBytes());
		byte[] privateKey = Crypto.getInstance().createKeyPair(seed).getA();
		PrivateKeyAccount generator = new PrivateKeyAccount(privateKey);
						
		//PROCESS GENESIS TRANSACTION TO MAKE SURE GENERATOR HAS FUNDS
		Transaction transaction = new GenesisTransaction(generator, BigDecimal.valueOf(1000).setScale(8), NTP.getTime());
		transaction.process(databaseSet);
				
		//GENERATE NEXT BLOCK
		BlockGenerator blockGenerator = new BlockGenerator();
		Block newBlock = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);
		
		//ADD TRANSACTION SIGNATURE
		byte[] transactionsSignature = Crypto.getInstance().sign(generator, newBlock.getGeneratorSignature());
		newBlock.setTransactionsSignature(transactionsSignature);
		
		//CHECK IF SIGNATURE VALID
		assertEquals(true, newBlock.isSignatureValid());
		
		//INVALID TRANSACTION SIGNATURE
		transactionsSignature = new byte[64];
		newBlock.setTransactionsSignature(transactionsSignature);
		
		//CHECK IF SIGNATURE INVALID
		assertEquals(false, newBlock.isSignatureValid());
		
		//INVALID GENERATOR SIGNATURE
		newBlock = BlockFactory.getInstance().create(newBlock.getVersion(), newBlock.getReference(), newBlock.getTimestamp(), newBlock.getGeneratingBalance(), generator, new byte[32]);
		transactionsSignature = Crypto.getInstance().sign(generator, newBlock.getGeneratorSignature());
		newBlock.setTransactionsSignature(transactionsSignature);
		
		///CHECK IF SIGNATURE INVALID
		assertEquals(false, newBlock.isSignatureValid());
		
		//VALID TRANSACTION SIGNATURE
		newBlock = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);	
		
		//ADD TRANSACTION
		Account recipient = new Account("XUi2oga2pnGNcZ9es6pBqxydtRZKWdkL2g");
		long timestamp = newBlock.getTimestamp();
		byte[] signature = PaymentTransaction.generateSignature(databaseSet, generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment = new PaymentTransaction(generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(databaseSet), signature);
		newBlock.addTransaction(payment);
		
		//ADD TRANSACTION SIGNATURE
		transactionsSignature = blockGenerator.calculateTransactionsSignature(newBlock, generator);
		newBlock.setTransactionsSignature(transactionsSignature);
		
		//CHECK VALID TRANSACTION SIGNATURE
		assertEquals(true, newBlock.isSignatureValid());	
		
		//INVALID TRANSACTION SIGNATURE
		newBlock = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);	
		
		//ADD TRANSACTION
		payment = new PaymentTransaction(generator, recipient, BigDecimal.valueOf(200).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(databaseSet), signature);
		newBlock.addTransaction(payment);
				
		//ADD TRANSACTION SIGNATURE
		transactionsSignature = blockGenerator.calculateTransactionsSignature(newBlock, generator);
		newBlock.setTransactionsSignature(transactionsSignature);
		
		//CHECK INVALID TRANSACTION SIGNATURE
		assertEquals(false, newBlock.isSignatureValid());	
	}
	
	@Test
	public void validateBlock()
	{
		//CREATE EMPTY MEMORY DATABASE
		DBSet databaseSet = DBSet.createEmptyDatabaseSet();
						
		//PROCESS GENESISBLOCK
		GenesisBlock genesisBlock = new GenesisBlock();
		genesisBlock.process(databaseSet);
						
		//CREATE KNOWN ACCOUNT
		byte[] seed = Crypto.getInstance().digest("test".getBytes());
		byte[] privateKey = Crypto.getInstance().createKeyPair(seed).getA();
		PrivateKeyAccount generator = new PrivateKeyAccount(privateKey);
								
		//PROCESS GENESIS TRANSACTION TO MAKE SURE GENERATOR HAS FUNDS
		Transaction transaction = new GenesisTransaction(generator, BigDecimal.valueOf(1000).setScale(8), NTP.getTime());
		transaction.process(databaseSet);
						
		//GENERATE NEXT BLOCK
		BlockGenerator blockGenerator = new BlockGenerator();
		Block newBlock = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);
				
		//ADD TRANSACTION SIGNATURE
		byte[] transactionsSignature = Crypto.getInstance().sign(generator, newBlock.getGeneratorSignature());
		newBlock.setTransactionsSignature(transactionsSignature);
		
		//CHECK IF VALID
		assertEquals(true, newBlock.isValid(databaseSet));
		
		//CHANGE REFERENCE
		Block invalidBlock = BlockFactory.getInstance().create(newBlock.getVersion(), new byte[128], newBlock.getTimestamp(), newBlock.getGeneratingBalance(), newBlock.getGenerator(), newBlock.getGeneratorSignature());
		
		//CHECK IF INVALID
		assertEquals(false, invalidBlock.isValid(databaseSet));
		
		//CHANGE TIMESTAMP
		invalidBlock = BlockFactory.getInstance().create(newBlock.getVersion(), newBlock.getReference(), 1L, newBlock.getGeneratingBalance(), newBlock.getGenerator(), newBlock.getGeneratorSignature());
		
		//CHECK IF INVALID
		assertEquals(false, invalidBlock.isValid(databaseSet));
		
		//CHANGE BASETARGET
		invalidBlock = BlockFactory.getInstance().create(newBlock.getVersion(), newBlock.getReference(), newBlock.getTimestamp(), 1L, newBlock.getGenerator(), newBlock.getGeneratorSignature());
				
		//CHECK IF INVALID
		assertEquals(false, invalidBlock.isValid(databaseSet));
		
		//ADD INVALID TRANSACTION
		invalidBlock = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);
		Account recipient = new Account("XUi2oga2pnGNcZ9es6pBqxydtRZKWdkL2g");
		long timestamp = newBlock.getTimestamp();
		byte[] signature = PaymentTransaction.generateSignature(databaseSet, generator, recipient, BigDecimal.valueOf(-100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment = new PaymentTransaction(generator, recipient, BigDecimal.valueOf(-100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(databaseSet), signature);
		invalidBlock.addTransaction(payment);		
		
		//CHECK IF INVALID
		assertEquals(false, invalidBlock.isValid(databaseSet));
		
		//ADD GENESIS TRANSACTION
		invalidBlock = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);	
		transaction = new GenesisTransaction(generator, BigDecimal.valueOf(1000).setScale(8), newBlock.getTimestamp());
		invalidBlock.addTransaction(transaction);	
		
		//CHECK IF INVALID
		assertEquals(false, invalidBlock.isValid(databaseSet));
	}
	
	@Test
	public void parseBlock()
	{
		//CREATE EMPTY MEMORY DATABASE
		DBSet databaseSet = DBSet.createEmptyDatabaseSet();
								
		//PROCESS GENESISBLOCK
		GenesisBlock genesisBlock = new GenesisBlock();
		genesisBlock.process(databaseSet);
								
		//CREATE KNOWN ACCOUNT
		byte[] seed = Crypto.getInstance().digest("test".getBytes());
		byte[] privateKey = Crypto.getInstance().createKeyPair(seed).getA();
		PrivateKeyAccount generator = new PrivateKeyAccount(privateKey);
										
		//PROCESS GENESIS TRANSACTION TO MAKE SURE GENERATOR HAS FUNDS
		Transaction transaction = new GenesisTransaction(generator, BigDecimal.valueOf(1000).setScale(8), NTP.getTime());
		transaction.process(databaseSet);
								
		//GENERATE NEXT BLOCK
		BlockGenerator blockGenerator = new BlockGenerator();
		Block block = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);
						
		//FORK
		DBSet fork = databaseSet.fork();
				
		//GENERATE PAYMENT 1
		Account recipient = new Account("XUi2oga2pnGNcZ9es6pBqxydtRZKWdkL2g");
		long timestamp = block.getTimestamp();
		byte[] signature = PaymentTransaction.generateSignature(databaseSet, generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment1 = new PaymentTransaction(generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(databaseSet), signature);
		payment1.process(fork);
		block.addTransaction(payment1);	
				
		//GENERATE PAYMENT 2
		Account recipient2 = new Account("XLPYYfxKEiDcybCkFA7jXcxSdePMMoyZLt");
		signature = PaymentTransaction.generateSignature(fork, generator, recipient2, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment2 = new PaymentTransaction(generator, recipient2, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(fork), signature);
		block.addTransaction(payment2);	
						
		//ADD TRANSACTION SIGNATURE
		byte[] transactionsSignature = Crypto.getInstance().sign(generator, block.getGeneratorSignature());
		block.setTransactionsSignature(transactionsSignature);
				
		//CONVERT TO BYTES
		byte[] rawBlock = block.toBytes();
				
		try 
		{	
			//PARSE FROM BYTES
			Block parsedBlock = BlockFactory.getInstance().parse(rawBlock);
					
			//CHECK INSTANCE
			assertEquals(false, parsedBlock instanceof GenesisBlock);
					
			//CHECK SIGNATURE
			assertEquals(true, Arrays.equals(block.getSignature(), parsedBlock.getSignature()));
					
			//CHECK GENERATOR
			assertEquals(block.getGenerator().getAddress(), parsedBlock.getGenerator().getAddress());	
					
			//CHECK BASE TARGET
			assertEquals(block.getGeneratingBalance(), parsedBlock.getGeneratingBalance());	
			
			//CHECK FEE
			assertEquals(block.getTotalFee(), parsedBlock.getTotalFee());	
					
			//CHECK REFERENCE
			assertEquals(true, Arrays.equals(block.getReference(), parsedBlock.getReference()));	
					
			//CHECK TIMESTAMP
			assertEquals(block.getTimestamp(), parsedBlock.getTimestamp());		
			
			//CHECK TRANSACTIONS COUNT
			assertEquals(block.getTransactionCount(), parsedBlock.getTransactionCount());		
		}
		catch (Exception e) 
		{
			fail("Exception while parsing transaction.");
		}
				
		//PARSE TRANSACTION FROM WRONG BYTES
		rawBlock = new byte[50];
		
		try 
		{	
			//PARSE FROM BYTES
			BlockFactory.getInstance().parse(rawBlock);
					
			//FAIL
			fail("this should throw an exception");
		}
		catch (Exception e) 
		{
			//EXCEPTION IS THROWN OKE
		}			
	}
	
	@Test
	public void processBlock()
	{
		//CREATE EMPTY MEMORY DATABASE
		DBSet databaseSet = DBSet.createEmptyDatabaseSet();
										
		//PROCESS GENESISBLOCK
		GenesisBlock genesisBlock = new GenesisBlock();
		genesisBlock.process(databaseSet);
										
		//CREATE KNOWN ACCOUNT
		byte[] seed = Crypto.getInstance().digest("test".getBytes());
		byte[] privateKey = Crypto.getInstance().createKeyPair(seed).getA();
		PrivateKeyAccount generator = new PrivateKeyAccount(privateKey);
												
		//PROCESS GENESIS TRANSACTION TO MAKE SURE GENERATOR HAS FUNDS
		Transaction transaction = new GenesisTransaction(generator, BigDecimal.valueOf(1000).setScale(8), NTP.getTime());
		transaction.process(databaseSet);
								
		//GENERATE NEXT BLOCK
		BlockGenerator blockGenerator = new BlockGenerator();
		Block block = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);
		
		//FORK
		DBSet fork = databaseSet.fork();
		
		//GENERATE PAYMENT 1
		Account recipient = new Account("XUi2oga2pnGNcZ9es6pBqxydtRZKWdkL2g");
		long timestamp = block.getTimestamp();
		byte[] signature = PaymentTransaction.generateSignature(databaseSet, generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment1 = new PaymentTransaction(generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(databaseSet), signature);
		payment1.process(fork);
		block.addTransaction(payment1);	
		
		//GENERATE PAYMENT 2
		Account recipient2 = new Account("XLPYYfxKEiDcybCkFA7jXcxSdePMMoyZLt");
		signature = PaymentTransaction.generateSignature(fork, generator, recipient2, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment2 = new PaymentTransaction(generator, recipient2, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(fork), signature);
		block.addTransaction(payment2);	
		
		//ADD TRANSACTION SIGNATURE
		byte[] transactionsSignature = blockGenerator.calculateTransactionsSignature(block, generator);
		block.setTransactionsSignature(transactionsSignature);
		
		//CHECK VALID
		assertEquals(true, block.isSignatureValid());
		assertEquals(true, block.isValid(databaseSet));
		
		//PROCESS BLOCK
		block.process(databaseSet);
		
		//CHECK BALANCE GENERATOR
		assertEquals(true, generator.getConfirmedBalance(databaseSet).compareTo(BigDecimal.valueOf(800)) == 0);
		
		//CHECK LAST REFERENCE GENERATOR
		assertEquals(true, Arrays.equals(generator.getLastReference(databaseSet), payment2.getSignature()));
		
		//CHECK BALANCE RECIPIENT
		assertEquals(true, recipient.getConfirmedBalance(databaseSet).compareTo(BigDecimal.valueOf(1100)) == 0);
		
		//CHECK LAST REFERENCE RECIPIENT
		assertEquals(false, Arrays.equals(recipient.getLastReference(databaseSet), payment1.getSignature()));
		
		//CHECK BALANCE RECIPIENT2
		assertEquals(true, recipient2.getConfirmedBalance(databaseSet).compareTo(BigDecimal.valueOf(100)) == 0);
				
		//CHECK LAST REFERENCE RECIPIENT
		assertEquals(true, Arrays.equals(recipient2.getLastReference(databaseSet), payment2.getSignature()));
		
		//CHECK TOTAL FEE
		assertEquals(true, block.getTotalFee().compareTo(BigDecimal.valueOf(2)) == 0);
		
		//CHECK TOTAL TRANSACTIONS
		assertEquals(2, block.getTransactionCount());
		
		//CHECK LAST BLOCK
		assertEquals(true, Arrays.equals(block.getSignature(), databaseSet.getBlockMap().getLastBlock().getSignature()));
	}
	
	@Test
	public void orphanBlock()
	{
		//CREATE EMPTY MEMORY DATABASE
		DBSet databaseSet = DBSet.createEmptyDatabaseSet();
										
		//PROCESS GENESISBLOCK
		GenesisBlock genesisBlock = new GenesisBlock();
		genesisBlock.process(databaseSet);
										
		//CREATE KNOWN ACCOUNT
		byte[] seed = Crypto.getInstance().digest("test".getBytes());
		byte[] privateKey = Crypto.getInstance().createKeyPair(seed).getA();
		PrivateKeyAccount generator = new PrivateKeyAccount(privateKey);
												
		//PROCESS GENESIS TRANSACTION TO MAKE SURE GENERATOR HAS FUNDS
		Transaction transaction = new GenesisTransaction(generator, BigDecimal.valueOf(1000).setScale(8), NTP.getTime());
		transaction.process(databaseSet);
								
		//GENERATE NEXT BLOCK
		BlockGenerator blockGenerator = new BlockGenerator();
		Block block = blockGenerator.generateNextBlock(databaseSet, generator, genesisBlock);
		
		//FORK
		DBSet fork = databaseSet.fork();
		
		//GENERATE PAYMENT 1
		Account recipient = new Account("XUi2oga2pnGNcZ9es6pBqxydtRZKWdkL2g");
		long timestamp = block.getTimestamp();
		byte[] signature = PaymentTransaction.generateSignature(databaseSet, generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment1 = new PaymentTransaction(generator, recipient, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(databaseSet), signature);
		payment1.process(fork);
		block.addTransaction(payment1);	
		
		//GENERATE PAYMENT 2
		Account recipient2 = new Account("XLPYYfxKEiDcybCkFA7jXcxSdePMMoyZLt");
		signature = PaymentTransaction.generateSignature(fork, generator, recipient2, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp);
		Transaction payment2 = new PaymentTransaction(generator, recipient2, BigDecimal.valueOf(100).setScale(8), BigDecimal.valueOf(1).setScale(8), timestamp, generator.getLastReference(fork), signature);
		block.addTransaction(payment2);	
		
		//ADD TRANSACTION SIGNATURE
		byte[] transactionsSignature = blockGenerator.calculateTransactionsSignature(block, generator);
		block.setTransactionsSignature(transactionsSignature);
		
		//CHECK VALID
		assertEquals(true, block.isSignatureValid());
		assertEquals(true, block.isValid(databaseSet));
		
		//PROCESS BLOCK
		block.process(databaseSet);
		
		//ORPHAN BLOCK
		block.orphan(databaseSet);
		
		//CHECK BALANCE GENERATOR
		assertEquals(true, generator.getConfirmedBalance(databaseSet).compareTo(BigDecimal.valueOf(1000)) == 0);
		
		//CHECK LAST REFERENCE GENERATOR
		assertEquals(true, Arrays.equals(generator.getLastReference(databaseSet), transaction.getSignature()));
		
		//CHECK BALANCE RECIPIENT
		assertEquals(true, recipient.getConfirmedBalance(databaseSet).compareTo(BigDecimal.valueOf(1000)) == 0);
		
		//CHECK LAST REFERENCE RECIPIENT
		assertEquals(false, Arrays.equals(recipient.getLastReference(databaseSet), payment1.getSignature()));
		
		//CHECK BALANCE RECIPIENT2
		assertEquals(true, recipient2.getConfirmedBalance(databaseSet).compareTo(BigDecimal.valueOf(0)) == 0);
				
		//CHECK LAST REFERENCE RECIPIENT
		assertEquals(true, Arrays.equals(recipient2.getLastReference(databaseSet), new byte[0]));
		
		//CHECK LAST BLOCK
		assertEquals(true, Arrays.equals(genesisBlock.getSignature(), databaseSet.getBlockMap().getLastBlock().getSignature()));
	}
}
