package bsearch.test;

//TODO: Improve test coverage!  (It's pretty weak right now... :-/)

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;

import junit.framework.Assert;

import org.junit.Test;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.nlogo.util.MersenneTwisterFast;
import org.xml.sax.SAXException;

import bsearch.algorithms.SearchMethod;
import bsearch.algorithms.SearchMethodLoader;
import bsearch.algorithms.SearchParameterException;
import bsearch.app.BehaviorSearch;
import bsearch.app.BehaviorSearchException;
import bsearch.app.SearchProtocol;
import bsearch.app.BehaviorSearch.RunOptions;
import bsearch.nlogolink.ModelRunner;
import bsearch.nlogolink.NetLogoLinkException;
import bsearch.representations.Chromosome;
import bsearch.representations.ChromosomeFactory;
import bsearch.representations.ChromosomeTypeLoader;
import bsearch.representations.GrayBinaryChromosome;
import bsearch.representations.StandardBinaryChromosome;
import bsearch.space.SearchSpace;
import bsearch.util.GeneralUtils;

public strictfp class TestBehaviorSearch
{
	public static final String PATH_TO_NETLOGO_MODELS = "/home/forrest/apps/netlogo/models/Sample Models/";
	
	// a main method to run it -- for convenience.
	public static void main( String... args )
	{
		org.junit.runner.JUnitCore.main
				( TestBehaviorSearch.class.getName() ) ;
	}
	

	@Test
	public void testModelRunner1() throws Exception
	{
		ModelRunner runner;
		LinkedHashMap<String,Object> params; 
    	runner = bsearch.nlogolink.ModelRunner.createModelRunnerForTesting(PATH_TO_NETLOGO_MODELS + "Earth Science/Fire.nlogo", true, 100);
    	runner.setSetupCommands( "setup" );
    	runner.setStepCommands( "go" );
    	runner.setStopConditionReporter( "burned-trees > 3000" );
    	runner.setMeasureIfReporter("ticks mod 10 = 0 or ticks = 37");	
    	runner.addResultReporter( "burned-trees" );

        params = new LinkedHashMap<String, Object>();
        params.put("density", 61.0);
        
        ModelRunner.RunSetup runSetup = new ModelRunner.RunSetup(0, params); 
    	
        List<Double> results = runner.doFullRun( runSetup ).getTimeSeriesForMeasure("burned-trees");
        Assert.assertEquals(5, results.size()); // 5 because we only get to tick 37, due to the stop condition.
        
        Assert.assertEquals(3002.0, results.get(results.size() - 1));
        Assert.assertEquals(37.0, runner.report( "ticks" ));
        runner.command("go");
        Assert.assertEquals(3068.0, runner.measureResults().get("burned-trees"));

        runner.dispose();
	}

	@Test
	public void testModelRunner2() throws Exception
    {
		ModelRunner runner;
		LinkedHashMap<String,Object> params; 
    	runner = bsearch.nlogolink.ModelRunner.createModelRunnerForTesting(PATH_TO_NETLOGO_MODELS + "Earth Science/Fire.nlogo", false, 100);
    	runner.setSetupCommands( "setup" );
    	runner.setStepCommands( "go" );
    	runner.setStopConditionReporter( "burned-trees > 3000" );
    	runner.setMeasureIfReporter("true");
    	runner.addResultReporter( "burned-trees" );

        params = new LinkedHashMap<String, Object>();
        params.put("density", 61.0);

        // now do it manually, without using doFullRun() 
        runner.setup(0, params);
        int i;
        for (i = 0; i < 100; i++)
        {
        	if (runner.go())
        		break;
        }
        Assert.assertEquals(3002.0, runner.measureResults().get("burned-trees"));
        Assert.assertEquals(37.0, runner.report("ticks"));
        runner.dispose();        
	}
	@Test
	public void testConstraintsTextGeneration() throws BehaviorSearchException, NetLogoLinkException
    {
		Assert.assertEquals(bsearch.nlogolink.Utils.getDefaultConstraintsText(PATH_TO_NETLOGO_MODELS + "/Social Science/Ethnocentrism.nlogo").trim(),
		"[\"mutation-rate\" [0 0.001 1]]\n[\"death-rate\" [0 0.05 1]]\n[\"immigrants-per-day\" [0 1 100]]\n[\"initial-ptr\" [0 0.01 1]]\n[\"cost-of-giving\" [0 0.01 1]]\n[\"gain-of-receiving\" [0 0.01 1]]\n[\"immigrant-chance-cooperate-with-same\" [0 0.01 1]]\n[\"immigrant-chance-cooperate-with-different\" [0 0.01 1]]");
    }
	

	@Test
	public void testSearchProtocol() throws IOException , SAXException
	{
		String FILENAME = GeneralUtils.attemptResolvePathFromBSearchRoot("test/TestProtocol.xml") ;
		SearchProtocol sp = SearchProtocol.loadFile( FILENAME ) ;
		java.io.StringWriter sw = new java.io.StringWriter() ;
		sp.save( new PrintWriter( sw ) ) ;
		// is the result the same as the original, ignoring all whitespace?
		String result = sw.getBuffer().toString().replaceAll( "\\s" , "" ) ;
		String original  = bsearch.util.GeneralUtils.stringContentsOfFile( new java.io.File(FILENAME) ).replaceAll( "\\s" , "" ) ;
		//System.out.println( result ) ;
		//System.out.println( original ) ;
		//System.out.println( result.equals( original ) ) ;
		Assert.assertEquals(result, original);
	}
	
	@Test
	public void testSearchMethodLoader() throws BehaviorSearchException
	{
		// just make sure there are no errors...
    	for (String s: SearchMethodLoader.getAllSearchMethodNames())
    	{
    		SearchMethod searcher = SearchMethodLoader.createFromName(s);
    		searcher.getSearchParams().toString();
    	}		
	}

	@Test
	public void testChromosomes() throws BehaviorSearchException
	{
		SearchSpace ss = new SearchSpace(Arrays.asList("[\"discrete1to4\" [1 1 4]]", 
														"[\"continuous0to1.5\" [0.0 \"C\" 1.5]]",
														"[\"categorical\" \"apple\" \"banana\" \"cherry\"]",
														"[\"const\" 25]",
														"[\"discretedecimal\" [-1 0.17 2]]"));
		MersenneTwisterFast rng = new MersenneTwisterFast();
		
    	for (String chromoType: ChromosomeTypeLoader.getAllChromosomeTypes())
    	{
    		ChromosomeFactory factory = ChromosomeTypeLoader.createFromName(chromoType);    		
    		Chromosome cs[] = new Chromosome[] {factory.createChromosome(ss, rng), factory.createChromosome(ss, rng)};
			for (int i = 0; i < 1000; i++)
			{
				cs[0] = cs[0].mutate(0.20, rng);
				cs[1] = cs[1].crossoverWith(cs[0], rng)[0];
				
				for (Chromosome c: cs )
				{
					LinkedHashMap<String, Object> params = c.getParamSettings(); 
					double val1 = (Double)params.get("discrete1to4");
					double val2 = (Double)params.get("continuous0to1.5");
					String val3 = (String) params.get("categorical");
					Object val4 = params.get("const");
					double val5 = (Double)params.get("discretedecimal");
					Assert.assertTrue(chromoType + " check val1 >= 1", val1 >= 1 );
					Assert.assertTrue(chromoType + " check val1 <= 4", val1 <= 4 );
					Assert.assertTrue(chromoType + " check val2 >= 0.0", val2 >= 0.0 );
					Assert.assertTrue(chromoType + " check val2 <= 1.5", val2 <= 1.5 );
					Assert.assertTrue(chromoType + " check val3 okay", val3.equals("apple") || val3.equals("banana") || val3.equals("cherry"));
					Assert.assertTrue(chromoType + " check val4 okay", val4.equals(new Double(25)));
					Assert.assertTrue(chromoType + " check val5 >= -1", val5 >= -1 );
					Assert.assertTrue(chromoType + " check val5 <= 2", val5 <= 2 );
				}
			}
			for (int i = 0; i < 10000; i++)
			{
				Chromosome c = factory.createChromosome(ss, rng);
				//System.out.println(chromoType + " i=" + i + " : " + c);
				Chromosome c2 = factory.createChromosome(ss, c.getParamSettings());
				Assert.assertEquals(chromoType + " check recreate Chromosomes from values:", c.getParamSettings(), c2.getParamSettings());
			}
    	}		
	}

	@Test
	public void testBinaryConversion()
	{
		int numBits = 8;
		boolean[] bits = new boolean[numBits + 12];
		
		SearchSpace ss = new SearchSpace(Arrays.asList("[\"moo\" [1 1 4]]"));
		GrayBinaryChromosome bcgray = new GrayBinaryChromosome(ss, new MersenneTwisterFast() );
		StandardBinaryChromosome bcstd = new StandardBinaryChromosome(ss, new MersenneTwisterFast() );

		for (long i = 0; i < (1L << numBits); i ++)
		{
			bcgray.binaryEncode(i, bits, 5, numBits);
			long dec = bcgray.binaryDecode(bits, 5, numBits);  
			Assert.assertEquals(i, dec);
			bcstd.binaryEncode(i, bits, 5, numBits);
			dec = bcstd.binaryDecode(bits, 5, numBits);  
//			if (i != dec)
//			{
//				System.out.println("i = " + i + ":  " + BinaryChromosome.toBinaryString(bits) + "   decode=" + dec);
//			}
			Assert.assertEquals(i, dec);
		}
		numBits = 62 ;
		bits= new boolean[numBits + 3];
		for (long i = 0; i < (1L << numBits); i += 15037093017331L)
		{
			bcgray.binaryEncode(i, bits, 2, numBits);
			long dec = bcgray.binaryDecode(bits, 2, numBits);  
			Assert.assertEquals(i, dec);
			bcstd.binaryEncode(i, bits, 2, numBits);
			dec = bcstd.binaryDecode(bits, 2, numBits);  
			Assert.assertEquals(i, dec);
		}		
	}


	public static <T> String join(final Iterable<T> objs, final String delimiter) {
	    java.util.Iterator<T> iter = objs.iterator();
	    if (!iter.hasNext())
	        return "";
	    StringBuffer buffer = new StringBuffer(String.valueOf(iter.next()));
	    while (iter.hasNext())
	        buffer.append(delimiter).append(String.valueOf(iter.next()));
	    return buffer.toString();
	}

	@Test
	public void testConsistentOutputResults() throws IOException , SAXException, SearchParameterException, BehaviorSearchException, InterruptedException, CmdLineException
	{
		LinkedHashMap<String,String> scenarios = new LinkedHashMap<String,String>();
		scenarios.put("TesterSuperRandom","-p test/TesterSuperRandom.bsearch -o test/tmp/TesterSuperRandom -t 7 -n 2 --randomseed 123 --quiet");
		scenarios.put("Tester1","-p test/Tester.bsearch -o test/tmp/Tester1 -t 1 -n 2 -f 3 --randomseed 1234 --quiet");
		scenarios.put("Tester2","-p test/Tester.bsearch -o test/tmp/Tester2 -t 2 -n 3 -f 10 --randomseed 99 --quiet");
		scenarios.put("TesterNoisy","-p test/TesterNoisy.bsearch -o test/tmp/TesterNoisy -t 2 -n 2 --randomseed 123 --quiet");
		scenarios.put("TesterGANoCache","-p test/TesterGANoCache.bsearch -o test/tmp/TesterGANoCache -n 1 -t 2 --randomseed 99 --quiet");
		scenarios.put("TesterSA_Deriv","-p test/TesterSA_Deriv.bsearch -o test/tmp/TesterSA_Deriv -n 2 -t 1 --randomseed 67 --quiet");
		scenarios.put("TesterNoisy_RS","-p test/TesterNoisy_RS.bsearch -o test/tmp/TesterNoisy_RS -t 1 -n 1 --randomseed 123 --quiet");
		scenarios.put("TesterNoisy_RS","-p test/TesterNoisy_RS.bsearch -o test/tmp/TesterNoisy_RS -t 5 -n 1 --randomseed 123 --quiet");
		
		List<String> outputExtensions = Arrays.asList(".searchConfig.xml", ".modelRunHistory.csv", 
				".objectiveFunctionHistory.csv",  ".bestHistory.csv", ".finalBests.csv", ".finalCheckedBests.csv"); 
		
		List<String> failedList = new ArrayList<String>();
		for (String key: scenarios.keySet())
		{
			//System.out.println("key: + " + key);
        	RunOptions clOptions = new RunOptions();
        	CmdLineParser parser = new CmdLineParser(clOptions);        	
        	parser.parseArgument(scenarios.get(key).split("\\s"));
			BehaviorSearch.runWithOptions(clOptions);

			for (String extension: outputExtensions)
			{
				String checkFileName = GeneralUtils.attemptResolvePathFromBSearchRoot("test/checks/" + key + extension);
				String testFileName = GeneralUtils.attemptResolvePathFromBSearchRoot("test/tmp/" + key + extension);
				File checkFile = new File(checkFileName);
				File testFile = new File(testFileName);
				String expected = checkFile.exists()?GeneralUtils.stringContentsOfFile(checkFile):"*no_file*";
				String result = testFile.exists()?GeneralUtils.stringContentsOfFile(testFile):"*no_file*";
				if (!result.equals(expected))
				{
					failedList.add(key + extension);
				}
			}
		}
		if (!failedList.isEmpty())
		{
			Assert.fail("Output files that differ: " + join(failedList, ", "));
		}
	}

}
