/*
 * Copyright (C) 2008 Steve Ratcliffe
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * 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.
 */
package uk.me.parabola.imgfmt.app.net;

import java.util.ArrayList;
import java.util.List;

import uk.me.parabola.imgfmt.app.BitReader;
import uk.me.parabola.imgfmt.app.BitWriter;

import func.lib.NumberReader;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.junit.Test;

import static org.junit.Assert.*;

/**
 * There are multiple ways of representing the same set of numbers. So these tests will employ a number
 * reader to parse the resulting bit stream and create a list of numberings that can be compared with
 * the input ones.
 */
public class NumberPreparerTest {

	@Test
	public void testNumberConstructor() {
		// A simple test with all numbers increasing.
		String spec = "0,O,1,7,E,2,12";
		Numbers n = new Numbers(spec);

		assertEquals(spec, n.toString());
	}

	/**
	 * Just test that the test infrastructure is working with a known byte stream, this
	 * is testing the tests.
	 */
	@Test
	public void testKnownStream() {
		byte[] buf = {0x41, 0x13, 0x27, 0x49, 0x60};
		BitReader br = new BitReader(buf);
		NumberReader nr = new NumberReader(br);
		nr.setNumberOfNodes(1);
		List<Numbers> numbers = nr.readNumbers(true);

		assertEquals(1, numbers.size());
		assertEquals("0,E,24,8,O,23,13", numbers.get(0).toString());
	}

	/**
	 * Simple test of numbers that increase on both sides.
	 */
	@Test
	public void testIncreasingNumbers() {
		run("0,O,1,11,E,2,12");
	}

	@Test
	public void testSwappedDefaultStyles() {
		List<Numbers> numbers = createList(new String[]{"0,E,2,12,O,1,11"});
		List<Numbers> output = writeAndRead(numbers);
		assertEquals(numbers, output);
	}

	@Test
	public void testIncreasingHighStarts() {
		String[] tests = {
				"0,O,1,5,E,2,6",
				"0,O,3,7,E,4,8",
				"0,O,91,99,E,92,98",
				"0,O,1,15,E,4,8",
		};

		for (String t : tests) {
			List<Numbers> numbers = createList(new String[]{t});
			List<Numbers> output = writeAndRead(numbers);
			assertEquals(numbers, output);
		}
	}

	@Test
	public void testSingleNumbers() {
		runSeparate("0,O,7,7,E,8,8", "0,O,7,7,E,6,6");
	}

	@Test
	public void testLargeDifferentStarts() {
		runSeparate("0,O,91,103,E,2,8", "0,E,90,102,O,3,9");
	}

	@Test
	public void testMultipleNodes() {
		List<Numbers> numbers = createList(new String[]{
				"0,O,1,9,E,2,12",
				"1,O,11,17,E,14,20",
				"2,O,21,31,E,26,36",
		});
		List<Numbers> output = writeAndRead(numbers);
		assertEquals(numbers, output);
	}

	@Test
	public void testMultipleNodesWithSkip() {
		List<Numbers> numbers = createList(new String[]{
				"0,O,1,9,E,2,12",
				"2,O,11,17,E,14,20",
				"3,O,21,31,E,26,36",
		});
		List<Numbers> output = writeAndRead(numbers);
		assertEquals(numbers, output);
	}

	@Test
	public void testMultipleWithReverse() {
		run("0,E,2,2,O,1,5", "1,E,2,10,O,5,17");
	}

	@Test
	public void testDecreasing() {
		run("0,O,25,11,E,24,20");
	}

	@Test
	public void testMixedStyles() {
		run("0,O,1,9,E,6,12", "1,E,14,22,O,9,17", "2,O,17,21,E,26,36");
	}

	@Test
	public void testOneSide() {
		runSeparate("0,N,-1,-1,O,9,3");
		runSeparate("0,E,2,8,N,-1,-1", "0,N,-1,-1,O,9,3");
	}

	@Test
	public void testBoth() {
		runSeparate("0,B,1,10,B,11,20");
	}

	@Test
	public void testLargeRunsAndGaps() {
		run("0,E,100,200,O,111,211", "1,E,400,500,O,421,501", "2,E,600,650,O,601,691");
	}

	@Test
	public void testSkip() {
		run("0,E,2,20,O,1,9", "3,O,3,9,E,2,2");
	}

	@Test
	public void testSkipFirst() {
		run("2,O,1,5,E,2,2");
	}

	@Test
	public void testLargeSkip() {
		run("0,N,-1,-1,E,2,4", "100,O,1,9,E,8,16");
	}

	@Test
	public void testRepeatingRun() {
		run("0,O,1,9,E,2,10",
				"1,O,11,19,E,12,20",
				"2,O,21,29,E,22,30",
				"3,O,31,39,E,32,40"
				);
		assertThat(bytesUsed, lessThanOrEqual(8));
	}

	/**
	 * What to do about the number zero.
	 */
	@Test
	public void testZero() {
		// Includes invalid cases where the numbers are the same down both sides.
		runSeparate("0,E,0,10,N,-1,-1",
				"1,B,0,4,B,0,8"
		);
	}

	@Test
	public void testVeryLargeNumber() {
		String[] numbers = {"0,E,55892490,55892500,N,-1,-1"};

		// Number is way too big, no exception, result just marked invalid.
		NumberPreparer preparer = new NumberPreparer(createList(numbers));
		assertFalse(preparer.isValid());

		numbers = new String[] {"0,E,10,55892500,N,-1,-1"};
		// The difference is too big.
		preparer = new NumberPreparer(createList(numbers));
		assertFalse(preparer.isValid());
	}

	@Test
	public void testLargeButOK() {
		run("0,O,1,10001,E,2,12000",
				"1,O,10003,10301,E,12002,12060",
				"2,E,1047000,1048000,N,-1,-1");
		runSeparate("3,E,131000,2,N,-1,-1");
	}

	/**
	 * Range with differences that are too large. The difference between the start and end
	 * of a range has a lower range than from initial-or-end to start.
	 */
	@Test
	public void testLargeDifferenceError() {
		String[] numbers = {"3,E,131080,2,N,-1,-1"};
		NumberPreparer preparer = new NumberPreparer(createList(numbers));
		assertFalse(preparer.isValid());
	}

	/**
	 * Tests sequences of number ranges that have previously been discovered to fail using the
	 * random range generator test.
	 */
	@Test
	public void testRegression() {
		String[][] tests = {
				{"0,E,4,2,E,2,2", "1,E,10,8,O,3,1", "2,B,8,6,B,3,3", "3,E,8,2,E,2,2"},
				{"0,O,5,7,O,9,5", "1,N,-1,-1,O,3,7", "2,N,-1,-1,O,3,5"},
				{"0,N,-1,-1,O,3,5", "1,O,1,3,N,-1,-1", "2,E,4,4,E,6,8"},
				{"0,N,-1,-1,E,4,4", "1,E,4,4,O,3,11"},
				{"0,B,4,8,O,5,9", "1,O,5,3,O,7,7", "2,O,3,3,E,4,20"},
				{"0,E,8,6,B,6,2", "1,O,5,5,E,4,8"},
				{"0,B,16,1,B,10,5", "1,O,3,7,E,2,8"},
				{"0,B,10,5,E,22,10", "1,O,3,1,O,3,5"},
				{"0,B,10,10,N,-1,-1", "1,O,11,9,O,1,11", "2,O,3,3,E,8,4", "3,O,7,19,E,6,2", "4,E,10,6,E,4,4"},
				{"0,N,-1,-1,B,6,5", "1,O,3,11,O,3,3"},
				{"0,O,7,1,O,9,5", "1,O,27,23,O,3,5"},
				{"0,B,5,5,E,12,8"},
		};

		for (String[] sarr : tests)
			run(sarr);
	}

	// Helper routines
	private void runSeparate(String... numbers) {
		for (String s : numbers)
			run(s);
	}

	private void run(String ... numbers) {
		List<Numbers> nList = createList(numbers);
		List<Numbers> output = writeAndRead(nList);
		assertEquals(nList, output);
	}

	private int bytesUsed;

	private List<Numbers> writeAndRead(List<Numbers> numbers) {
		NumberPreparer preparer = new NumberPreparer(numbers);
		BitWriter bw = preparer.fetchBitStream();
		bytesUsed += bw.getLength();

		assertTrue("check valid flag", preparer.isValid());

		boolean swapped = preparer.getSwapped();

		// Now read it all back in again
		byte[] b1 = bw.getBytes();
		byte[] bytes = new byte[bw.getLength()];
		System.arraycopy(b1, 0, bytes, 0, bw.getLength());

		BitReader br = new BitReader(bytes);
		NumberReader nr = new NumberReader(br);
		nr.setNumberOfNodes(numbers.get(numbers.size()-1).getIndex() + 1);
		List<Numbers> list = nr.readNumbers(swapped);
		for (Numbers n : list)
			n.setPolishIndex(n.getIndex());

		return list;
	}

	private static List<Numbers> createList(String[] specs) {
		List<Numbers> numbers = new ArrayList<>();
		for (String s : specs) {
			Numbers n = new Numbers(s);
			n.setIndex(n.getPolishIndex());
			numbers.add(n);
		}
		return numbers;
	}

	private static Matcher<Integer> lessThanOrEqual(final int val) {
		return new BaseMatcher<Integer>() {
			public boolean matches(Object o) {
				return (Integer) o <= val;
			}

			public void describeTo(Description description) {
				description.appendText("value is less than ").appendValue(val);
			}
		};
	}
}
