/* 
 * This file is part of Artemis
 *
 * Copyright (C) 2017  Genome Research Limited
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */
package uk.ac.sanger.artemis.sequence;

import java.net.URL;

import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;

import uk.ac.sanger.artemis.Entry;
import uk.ac.sanger.artemis.Feature;
import uk.ac.sanger.artemis.Options;
import uk.ac.sanger.artemis.io.DocumentEntryFactory;
import uk.ac.sanger.artemis.io.EntryInformation;
import uk.ac.sanger.artemis.io.Range;
import uk.ac.sanger.artemis.util.Document;
import uk.ac.sanger.artemis.util.DocumentFactory;
import uk.ac.sanger.artemis.util.ReadOnlyException;
import uk.ac.sanger.artemis.util.StringVector;
import uk.ac.sanger.artemis.util.OutOfRangeException;

/**
 * Unit test for the Bases class.
 * 
 * @author kp11
 *
 */
public class BasesTest 
{

	private static final String TEST_SEQUENCE 				= "[encoded data, 33593 characters]";
	private static final String TEST_SEQUENCE_REVERSE_COMP 	= "[encoded data, 33593 characters]";
	private static final String REVERSE_COMP_SECTION_RES_STRING = "[encoded data, 33593 characters]";
	private static final int SEQLEN = TEST_SEQUENCE.length();
	private static final int [] STOP_CODON_IDXS = {139, 208, 274, 835, 1225, 1351, 1501, 1525, 1660, 1750, 1801};
	
	private Bases bases;
	private Entry entry;
	
	/*
	 * Helper method for loading sequence data from file.
	 */
	private Bases loadBases() 
	{
		
		URL url = BasesTest.class.getResource("/etc/af063097.embl");

		final EntryInformation artemisEntryInformation = Options.getArtemisEntryInformation();
		final Document entryDocument = DocumentFactory.makeDocument(url.getFile());

		try {
			final uk.ac.sanger.artemis.io.Entry emblEntry = DocumentEntryFactory
					.makeDocumentEntry(artemisEntryInformation, entryDocument, null);
			entry = new Entry(emblEntry);
		      
			return entry.getBases();
		} 
		catch (Exception e) {
			org.junit.Assert.fail(e.getMessage());
		}
		
		return null;
	}
	
	@Before
	public void setup() 
	{
		
		bases = loadBases();
	}

	@After
	public void cleanup() 
	{
		
		bases = null;
		entry = null;
	}
	
	@Test
	public void testBasesCreation() throws Exception 
	{
		
		assertNotNull("Forward strand is not null check", bases.getForwardStrand());
		assertEquals("Forward strand has correct direction identifier", Strand.FORWARD, bases.getForwardStrand().getDirection());
		
		assertNotNull("Reverse strand is not null check", bases.getReverseStrand());
		assertEquals("Reverse strand has correct direction identifier", Strand.REVERSE, bases.getReverseStrand().getDirection());
		
		assertEquals("Check sequence length", SEQLEN, bases.getLength());
		assertEquals("Check sequence letters", TEST_SEQUENCE, bases.toString());
	}
	
	@Test
	public void testReverseComplement() throws Exception 
	{
		
		try 
		{
			bases.reverseComplement();
		} 
		catch (ReadOnlyException e) 
		{
			org.junit.Assert.fail("Should not be read only " + e.getMessage());
		}
		
		assertEquals("Check sequence letters after reverse compliment", TEST_SEQUENCE_REVERSE_COMP, bases.toString());
		
		assertEquals("Check forward strand after reverse compliment", TEST_SEQUENCE_REVERSE_COMP, bases.getForwardStrand().getStrandBases().toString());
		assertEquals("Check reverse strand after reverse compliment", TEST_SEQUENCE, bases.getReverseStrand().getStrandBases().toString());
	}
	
	@Test
	public void testGetComplementPosition () throws Exception 
	{
		
		assertEquals(SEQLEN-4+1, bases.getComplementPosition(4));
		assertEquals(SEQLEN-0+1, bases.getComplementPosition(0));
		assertEquals(SEQLEN-SEQLEN+1, bases.getComplementPosition(SEQLEN));
		
		// NB: This method doesn't semm to have any error checking.
	}
	
	@Test
	public void testGetRawPosition() throws Exception{
		
		assertEquals(4, bases.getRawPosition(4, Strand.FORWARD));
		assertEquals(0, bases.getRawPosition(0, Strand.FORWARD));
		assertEquals(SEQLEN-1, bases.getRawPosition(SEQLEN-1, Strand.FORWARD));
		
		assertEquals(SEQLEN-4+1, bases.getRawPosition(4, Strand.REVERSE));
		assertEquals(SEQLEN-0+1, bases.getRawPosition(0, Strand.REVERSE));
		assertEquals(SEQLEN-SEQLEN+1, bases.getRawPosition(SEQLEN, Strand.REVERSE));

		// NB: This method doesn't semm to have any error checking.
	}
	
	@Test
	public void testGetTranslation() throws Exception 
	{
		
		Range range1 = new Range(5, 20);
		AminoAcidSequence seq1 = bases.getTranslation(range1, Strand.FORWARD, true);
		
		assertNotNull(seq1);
		
		// Amino acid letters for forward strand triplets starting at 5...
		assertEquals("Amino acid translation", "rrgkh", seq1.toString());
		
		
		Range range2 = new Range(1, 10);
		AminoAcidSequence seq2 = bases.getTranslation(range2, Strand.REVERSE, true);
		
		assertNotNull(seq2);
		
		// Amino acid letters for reverse strand triplets starting at 1...
		assertEquals("Amino acid translation", "vls", seq2.toString());
		
	}
	
	@Test
	public void testGetSpacedTranslation() throws Exception 
	{
		
		Range range1 = new Range(5, 20);
		AminoAcidSequence seq1 = bases.getSpacedTranslation(range1, Strand.FORWARD, true);
		
		assertNotNull("Amino acid sequence not null", seq1);
		
		// Amino acid letters for forward strand triplets starting at 5...
		assertEquals("Amino acid spaced translation", "r  r  g  k  h  ", seq1.toString());
		
		
		Range range2 = new Range(1, 10);
		AminoAcidSequence seq2 = bases.getSpacedTranslation(range2, Strand.REVERSE, true);
		
		assertNotNull("Amino acid sequence not null",seq2);
		
		// Amino acid letters for reverse strand triplets starting at 1...
		assertEquals("Amino acid spaced translation", "v  l  s  ", seq2.toString());
		
	}
	
	@Test
	public void testGetMatchingCodons() throws Exception 
	{
		
		String [] queryCodons1 = {"gag","ggg"};
		StringVector queryCodonsVec = new StringVector(queryCodons1);
		Range range = new Range(1, 18);
		int [] matchingCodons = bases.getMatchingCodons (range, Strand.FORWARD, queryCodonsVec);
		
		assertNotNull("Matching codon array not null", matchingCodons);
		assertEquals("Matching codon array length", matchingCodons.length, 20);
		assertEquals("Check first match", 4, matchingCodons[0]);
		assertEquals("Check second match", 10, matchingCodons[1]);
		assertEquals("Check no more matches", 0, matchingCodons[2]);
		
		
		String [] queryCodons2 = {"tcc"};
		queryCodonsVec = new StringVector(queryCodons2);
		range = new Range(1, 18);
		matchingCodons = bases.getMatchingCodons (range, Strand.REVERSE, queryCodonsVec);
		
		assertNotNull("Matching codon array not null", matchingCodons);
		assertEquals("Matching codon array length", matchingCodons.length, 20);
		assertEquals("Check first match", 7, matchingCodons[0]);
		assertEquals("Check no more matches", 0, matchingCodons[2]);
		
		
		String [] queryCodons3 = {"zzz"}; // Won't match
		queryCodonsVec = new StringVector(queryCodons3);
		range = new Range(1, 18);
		matchingCodons = bases.getMatchingCodons (range, Strand.FORWARD, queryCodonsVec);
		assertNotNull("Matching codon array not null", matchingCodons);
		assertEquals("Matching codon array length", matchingCodons.length, 20);
		assertEquals("Check no matches at all", 0, matchingCodons[0]);
	}
	
	@Test
	public void testGetStopCodons() throws Exception 
	{
		
		Range range = new Range(1, 2000);
		int [] stopCodons = bases.getStopCodons(range, Strand.FORWARD);
		
		assertNotNull("Expecting some stop codons - should not be a null array", stopCodons);
		assertTrue("Expecting some stop codons - array length should be greater than zero", (stopCodons.length > 0));
		
		for (int i = 0; i < STOP_CODON_IDXS.length; i++) 
		{
			assertEquals("Check each stop codon index", STOP_CODON_IDXS[i], stopCodons[i]);
		}
	}
	
	@Test
	public void testGetBaseAt() throws Exception 
	{
		
		assertEquals('g', bases.getBaseAt(1));
		assertEquals('g', bases.getBaseAt(2));
		assertEquals('c', bases.getBaseAt(3));
		assertEquals('g', bases.getBaseAt(4));
		
		// Check end
		assertEquals('a', bases.getBaseAt(SEQLEN-1));
		assertEquals('c', bases.getBaseAt(SEQLEN));
		
		// Check bounds
		try 
		{
			bases.getBaseAt(SEQLEN+1);
			org.junit.Assert.fail("Expected an OutOfRangeException to be thrown");
		} 
		catch(OutOfRangeException e) 
		{
			// Expected
		}
		
		try {
			bases.getBaseAt(0);
			org.junit.Assert.fail("Expected an OutOfRangeException to be thrown");
		} 
		catch(OutOfRangeException e) 
		{
			// Expected
		}
	}
	
	@Test
	public void testGetSubSequence() throws Exception 
	{
		
		Range range1 = new Range(20, 30);
		String seq1 = bases.getSubSequence (range1, Strand.FORWARD);
		
		assertNotNull("Sub-sequence not null", seq1);
		assertEquals("First sub-sequence", "tgcgcgctgac", seq1.toString());
		
		
		Range range2 = new Range(1, 10);
		String seq2 = bases.getSubSequence (range2, Strand.REVERSE);
		
		assertNotNull("Sub-sequence not null", seq2);
		assertEquals("Second sub-sequence", "gtgctttccc", seq2.toString());
		
		
		Range range3 = new Range(SEQLEN-5, SEQLEN);
		String seq3 = bases.getSubSequence (range3, Strand.FORWARD);
		
		assertNotNull("Sub-sequence not null", seq3);
		assertEquals("Third sub-sequence", "aagcac", seq3.toString());
		
		// TODO Out of range value gives back a huge array of @ characters!!
		//Range range4 = new Range(200000, 200002);
		//String seq4 = bases.getSubSequence (range4, Strand.FORWARD);
		
	}
	
	@Test
	public void testGetSubSequenceC() throws Exception 
	{
		
		Range range1 = new Range(20, 30);
		char [] seq1 = bases.getSubSequenceC(range1, Strand.FORWARD);
		
		assertNotNull("Sub-sequence not null", seq1);
		assertEquals("First sub-sequence", "tgcgcgctgac", String.valueOf(seq1));
		
		
		Range range2 = new Range(1, 10);
		char [] seq2 = bases.getSubSequenceC(range2, Strand.REVERSE);
		
		assertNotNull("Sub-sequence not null", seq2);
		assertEquals("Second sub-sequence", "gtgctttccc", String.valueOf(seq2));
		
		
		Range range3 = new Range(SEQLEN-5, SEQLEN);
		char [] seq3 = bases.getSubSequenceC(range3, Strand.FORWARD);
		
		assertNotNull("Sub-sequence not null", seq3);
		assertEquals("Third sub-sequence", "aagcac", String.valueOf(seq3));
		
		// TODO Out of range value gives back a huge array of @ characters!!
		//Range range4 = new Range(200000, 200002);
		//char [] seq4 = bases.getSubSequenceC(range4, Strand.FORWARD);
		
	}
	
	@Test
	public void testTruncate() throws Exception 
	{

		Range range1 = new Range(20, 30);
		Bases truncatedBases = bases.truncate(range1);
		
		assertNotNull("Truncated bases object is not null", truncatedBases);
		assertEquals("Check bases truncation", "tgcgcgctgac", truncatedBases.toString());
		
		
		Range range2 = new Range(SEQLEN-5, SEQLEN);
		truncatedBases = bases.truncate(range2);
		
		assertNotNull("Truncated bases object is not null", truncatedBases);
		assertEquals("Check bases truncation", "aagcac", truncatedBases.toString());
		
		
		Range range3 = new Range(1, 20);
		truncatedBases = bases.truncate(range3);
		
		assertNotNull("Truncated bases object is not null", truncatedBases);
		assertEquals("Check bases truncation", "ggcgaggcggggaaagcact", truncatedBases.toString());
	}
	
	@Test
	public void testReverseComplementUsingFeature() throws Exception 
	{
		
		final Feature feature = entry.getFeature(101); // Pick a specific feature - a CDS at 24036..24254
		
		// DEBUG:
		//FeatureVector features = entry.getAllFeatures();
		//for (int i = 0; i < features.size()-1; i++) {
		//	System.err.println("Next feature: " + i + ": " + features.elementAt(i).toString());
		//}
		
		try 
		{
			bases.reverseComplement(feature);
			assertEquals(REVERSE_COMP_SECTION_RES_STRING, bases.toString());
		} 
		catch (ReadOnlyException e) 
		{
			org.junit.Assert.fail("Should not have thrown a ReadOnlyException");
		}
	
	}
	
	@Test
	public void testContigRearrange()  throws Exception 
	{
	
		// TODO This test needs completing
		
		final Feature feature = entry.getFeature(3); // Pick a specific feature - a CDS at tt[aatgc]
		/*try {
           bases.contigRearrange(feature, 1);
           assertEquals("", bases.toString());
		} catch (ReadOnlyException e) {
			org.junit.Assert.fail("Should not have thrown a ReadOnlyException");
		}*/
	}
	
	@Test
	public void testDeleteRange() throws Exception 
	{
		
		Range range1 = new Range(20, 30);
		String deletedBases = bases.deleteRange(range1);
		
		assertEquals("Check returned deleted bases string", "tgcgcgctgac", deletedBases);
		assertEquals("Check bases have been deleted from sequence by comparing before and after lengths", SEQLEN-11, bases.getLength());
	}
	
	@Test
	public void testAddBases() throws Exception 
	{
		
		// Forward
		
		int len = bases.getLength();
		String basesToAdd = "aggca";
		
		bases.addBases(20, Strand.FORWARD, basesToAdd);
		
		assertEquals("Check bases have been added to sequence by comparing before and after lengths", len+5, bases.getLength());
		
		Range range1 = new Range(20, 24);
		String subseq = String.valueOf(bases.getSubSequenceC(range1, Strand.FORWARD));
		assertEquals("Check bases have been added by pulling the sub sequence ", basesToAdd, subseq);
		
		
		// Reverse - i.e reverse compliment
		
		len = bases.getLength();
		basesToAdd = "tcccgaa";
		
		bases.addBases(40, Strand.REVERSE, basesToAdd);
		
		assertEquals("Check bases have been added to sequence by comparing before and after lengths", len+7, bases.getLength());
		
		Range range2 = new Range(41, 47); // 41 as we used reverse to add
		subseq = String.valueOf(bases.getSubSequenceC(range2, Strand.FORWARD));
		assertEquals("Check bases have been added by pulling the sub sequence", "ttcggga", subseq); // reverse compliment	
		
	}
	
	@Test
	public void testGetAverageGCPercent() throws Exception 
	{
	
		float gcPercent = bases.getAverageGCPercent();
	    assertEquals("Check GC Percentage", 50.168194f, gcPercent, 0.000001f);
	}
	
	@Test
	public void testGetAverageAGPercent() throws Exception 
	{
	
		float agPercent = bases.getAverageAGPercent();
	    assertEquals("Check AG Percentage", 51.403564f, agPercent, 0.000001f);
	}
	
	@Test
	public void testGetACount() throws Exception 
	{
	
		int num = bases.getACount();
	    assertEquals("Check num of As", 8398, num);
	}
	
	@Test
	public void testGetTCount() throws Exception 
	{
	
		int num = bases.getTCount();
	    assertEquals("Check num of Ts", 8342, num);
	}
	
	@Test
	public void testGetCCount() throws Exception 
	{
	
		int num = bases.getCCount();
	    assertEquals("Check num of Cs", 7983, num);
	}
	
	@Test
	public void testGetGCount() throws Exception 
	{
	
		int num = bases.getGCount();
	    assertEquals("Check num of Gs", 8870, num);
	}
	
	@Test
	public void testStaticReverseComplement() throws Exception 
	{
		
		String reverseComplement = Bases.reverseComplement("gaggcggggaaagcactgcgcgctgacggtggtgctgattgtattttttcagcgtctcagcgcgtcgtgacggcacttagtctgcccgttgaggcgttgtgtgtctgcggggtgttttgtgcggtggtgagcgtgtgagggggg");
		assertEquals("Check static string reverse complement method", "cccccctcacacgctcaccaccgcacaaaacaccccgcagacacacaacgcctcaacgggcagactaagtgccgtcacgacgcgctgagacgctgaaaaaatacaatcagcaccaccgtcagcgcgcagtgctttccccgcctc", reverseComplement);
		
	}
	
	@Test
	public void testStaticCharArrReverseComplement() throws Exception 
	{
		
		char[] reverseComplement = Bases.reverseComplement("gaggcggggaaagcactgcgcgctgacggtggtgctgattgtattttttcagcgtctcagcgcgtcgtgacggcacttagtctgcccgttgaggcgttgtgtgtctgcggggtgttttgtgcggtggtgagcgtgtgagggggg".toCharArray());
		assertEquals("Check static char array reverse complement method", "cccccctcacacgctcaccaccgcacaaaacaccccgcagacacacaacgcctcaacgggcagactaagtgccgtcacgacgcgctgagacgctgaaaaaatacaatcagcaccaccgtcagcgcgcagtgctttccccgcctc", String.valueOf(reverseComplement));
		
	}
	
	@Test
	public void testStaticComplement() throws Exception 
	{
		
		String sequenceString = Bases.complement ("gaggcggggaaagcactgcgcgctgacggt");
		assertEquals("Check static string complement method", "ctccgcccctttcgtgacgcgcgactgcca", sequenceString);
	}
	
	@Test
	public void testStaticCharArrComplement() throws Exception 
	{
		
		char[]  complement = Bases.complement ("gaggcggggaaagcactgcgcgctgacggt".toCharArray());
		assertEquals("Check static char array complement method", "ctccgcccctttcgtgacgcgcgactgcca", String.valueOf(complement));
	}
	
	@Test
	public void testComplement() throws Exception 
	{
		
		// Error case
		assertEquals('@', Bases.complement('%'));
		
		assertEquals('t', Bases.complement('a'));
		assertEquals('t', Bases.complement('A'));
		
		assertEquals('a', Bases.complement('t'));
		assertEquals('a', Bases.complement('T'));
		assertEquals('a', Bases.complement('U'));
		
		assertEquals('c', Bases.complement('g'));
		assertEquals('c', Bases.complement('G'));
		
		assertEquals('g', Bases.complement('c'));
		assertEquals('g', Bases.complement('C'));
		
		assertEquals('y', Bases.complement('r'));
		assertEquals('y', Bases.complement('R'));
		
		assertEquals('m', Bases.complement('k'));
		assertEquals('m', Bases.complement('K'));
		
		assertEquals('k', Bases.complement('m'));
		assertEquals('k', Bases.complement('M'));
		
		assertEquals('s', Bases.complement('s'));
		assertEquals('s', Bases.complement('S'));
		
		assertEquals('w', Bases.complement('w'));
		assertEquals('w', Bases.complement('W'));
		
		assertEquals('v', Bases.complement('b'));
		assertEquals('v', Bases.complement('B'));
		
		assertEquals('h', Bases.complement('d'));
		assertEquals('h', Bases.complement('D'));
		
		assertEquals('d', Bases.complement('h'));
		assertEquals('d', Bases.complement('H'));
		
		assertEquals('b', Bases.complement('v'));
		assertEquals('b', Bases.complement('V'));
		
		assertEquals('n', Bases.complement('n'));
		assertEquals('n', Bases.complement('N'));
		
		assertEquals('x', Bases.complement('x'));
		assertEquals('x', Bases.complement('X'));
	
	}
	
	@Test
	public void testIsLegalBase() throws Exception 
	{
		
		assertFalse(Bases.isLegalBase ('1'));
		assertFalse(Bases.isLegalBase ('$'));
		assertFalse(Bases.isLegalBase ('E'));
		assertFalse(Bases.isLegalBase ('l'));
		
		assertTrue(Bases.isLegalBase ('a'));
		assertTrue(Bases.isLegalBase ('A'));
		assertTrue(Bases.isLegalBase ('t'));
		assertTrue(Bases.isLegalBase ('T'));
		assertTrue(Bases.isLegalBase ('u'));
		assertTrue(Bases.isLegalBase ('U'));
		assertTrue(Bases.isLegalBase ('g'));
		assertTrue(Bases.isLegalBase ('G'));
		assertTrue(Bases.isLegalBase ('c'));
		assertTrue(Bases.isLegalBase ('C'));
	}
}
