package knightstour;


import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;


import org.junit.Test;

/**
 * TourTest tests an implementation of the Tour interface. The main focus is on
 * the correctness of computed solutions to the knight's tour problem. As a
 * board can have many different solutions, the tests check a computed solution
 * based on 3 conditions which together are necessary and sufficient.<p> 1. the
 * length of the tour matches with the number of fields on the board<p> 2. each
 * pair of adjacent positions in the tour describe a legal step of a knight on a
 * chess board<p> 3. each field on the board is visited exactly once.<p> In addition
 * to this: If the initial position is fixed, it remains to check the starting
 * position. The Tour interface asks for open tours. If the tour is expected to
 * be closed, existence of a legal knight step from the last position to the
 * first position needs to be checked.
 * <br>
 * Tests consider chess boards of dimensions (0,0), (1,1), up to (5,5). Low
 * dimensions are tested as border line cases with no or trivial solutions.
 * (5,5) is tested as a realistic use case with multiple solutions but on a
 * small scale. This size is not expected to detect performance problems due to
 * inefficient implementations.
 * <br>
 * The printing functionality is only mildly tested.
 * <br>
 * If the class under test is named differently than KnightsTourP1, the
 * corresponding constructor call in method createTour() needs to be adjusted.
 * This is the only location that needs an adjustment.
 * <br>
 * @author peterkemper
 */
public class TourTest {

	/**
	 * Helper class to encapsulate constructor call, which relies on the
	 * specific class name of the class that implements the Tour interface.
	 * @return an instance of a tour
	 */
	private Tour createTour() {
	    return new KnightsTourP1() ;
	}

	/**
	 * Test get/set pair of methods for dimension.
	 * a) illegal values n < 0 should result in a dimension of 0
	 * b) level values 0 <= should result in an identity
	 */
	@Test
	public final void testSetDimension() {
		final int invalidSmall = -10;
		final int invalidBorder = -1;
		final int validBorder = 0;
		final int validBorderAbove = 1;
		final int validMedium = 5;
		final int validLarge = 10;
		final int validDefault = 0;
		Tour t = createTour();
		t.setDimension(invalidSmall);
		assertTrue(validDefault == t.getDimension());
		t.setDimension(invalidBorder);
		assertTrue(validDefault == t.getDimension());
		t.setDimension(validBorder);
		assertTrue(validDefault == t.getDimension());
		t.setDimension(validBorderAbove);
		assertTrue(validBorderAbove == t.getDimension());
		t.setDimension(validMedium);
		assertTrue(validMedium == t.getDimension());
		t.setDimension(validLarge);
		assertTrue(validLarge == t.getDimension());
	}

	/**
	 * Test if a tour calculation is correct for a board of size 0 x 0.
	 * Initialization is not possible with any reasonable value,
	 * we try r=1,c=1,
	 */
	@Test
	public final void testGetTour0() {
		final int dimension = 0;
		Tour t = createTour();
		t.setDimension(dimension);
		// check an illegal initialization
		int[] tour = t.getTour(1, 1);
		assertTrue(0 == tour.length);
	}

	/**
	 * Test if a tour calculation is correct for a board of size 1 x 1.
	 * Initialization is only possible with r=0,c=0,
	 * others should result in an error For r=0,c=0,
	 * trivial tour of length 1 is a valid solution.
	 */
	@Test
	public final void testGetTour1() {
		final int dimension = 1;
		Tour t = createTour();
		t.setDimension(dimension);
		int[] tour = t.getTour(0, 0);
		assertTrue(2 == tour.length);
		assertTrue(0 == tour[0]);
		assertTrue(0 == tour[1]);
	}

	/**
	 * Test if a tour calculation is correct for a board of size 2 x 2.
	 * Initialization is only possible with different value pairs,
	 * e.g. r=0,c=0, however there is no solution since the knight
	 * cannot perform any legal moves.
	 */
	@Test
	public final void testGetTour2() {
		final int dimension = 2;
		Tour t = createTour();
		t.setDimension(dimension);
		assertTrue(hasNoSolution(t));
	}

	/**
	 * Test if a tour calculation is correct for a board of size 4 x 4.
	 * Initialization is only possible with different value pairs,
	 * e.g. r=0,c=0, however there is no solution.
	 */
	@Test
	public final void testGetTour4() {
		final int dimension = 4;
		Tour t = createTour();
		t.setDimension(dimension);
		assertTrue(hasNoSolution(t));
	}

	/**
	 * Test if a tour calculation is correct for a board of size 5 x 5.
	 * Initialization is only possible with different value pairs,
	 * e.g. r=0,c=0.
	 * This configurations has solutions. Note that by symmetry arguments, a
	 * position that has a solution implies 3 others must have one as well.
	 */
	@Test
	public final void testGetTour5() {
		final int dimension = 5;
		Tour t = createTour();
		t.setDimension(dimension);
		int[] tour;
		// corner positions have a solution
		tour = t.getTour(0, 0);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(0, 4);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(4, 0);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(4, 4);
		assertTrue(isSolution(t, tour));
		// middle positions have a solution
		tour = t.getTour(0, 2);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(2, 0);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(2, 4);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(4, 2);
		assertTrue(isSolution(t, tour));
		// diagonal positions have a solution
		tour = t.getTour(1, 1);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(2, 2);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(3, 3);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(1, 3);
		assertTrue(isSolution(t, tour));
		tour = t.getTour(3, 1);
		assertTrue(isSolution(t, tour));
		// some others do not have a solution
		tour = t.getTour(0, 1);
		assertTrue(isNoSolution(tour));
		tour = t.getTour(1, 0);
		assertTrue(isNoSolution(tour));
		tour = t.getTour(1, 2);
		assertTrue(isNoSolution(tour));
		tour = t.getTour(2, 1);
		assertTrue(isNoSolution(tour));
	}

	/**
	 * Test if the output is produced. Case: solution exists and is
	 * successfully computed. Trivial case of 1 x 1 matrix.
	 */
	@Test
	public final void testPrintTour1() {
		int dim = 1;
		Tour t = createTour();
		t.setDimension(dim);
		String result = t.printTour(0, 0);
		// perform a trivial test if a string output has been computed
		assertFalse("".equalsIgnoreCase(result));
		// allow for a visual inspection of layout
		System.out.println("Computed result for a 1 x 1 matrix"
				+ " and initial position (0,0)");
		System.out.println(result);
	}

	/**
	 * Test if the output is produced. Case: solution exists and is
	 * successfully computed. Non-trivial case of 5 x 5 matrix.
	 */
	@Test
	public final void testPrintTour5() {
		final int dimension = 5;
		Tour t = createTour();
		t.setDimension(dimension);
		String result = t.printTour(0, 0);
		// perform a trivial test if a string output has been computed
		assertFalse("".equalsIgnoreCase(result));
		// allow for a visual inspection of layout
		System.out.println("Computed result for a 5 x 5 matrix "
				+ "and initial position (0,0)");
		System.out.println(result);
	}

	/**
	 * Test if the output is produced. Case: solution does not exist.
	 * Non-trivial case of 5 x 5 matrix.
	 */
	@Test
	public final void testPrintTour5b() {
		final int dimension = 5;
		Tour t = createTour();
		t.setDimension(dimension);
		String result = t.printTour(0, 1);
		// perform a trivial test if a string output has been computed
		// empty string signals that there is no solution
		assertTrue("".equalsIgnoreCase(result));
		// allow for a visual inspection of layout
		// System.out.println("Computed result for a 5 x 5 matrix "
		// +"and initial position (0,0)");
		// System.out.println(result);
	}

	/**
	 * Helper method to check if a pair of coordinates is a legal step for a
	 * knight.
	 * @param r1 row coordinate of first position
	 * @param c1 column coordinate of first position
	 * @param r2 row coordinate of second position
	 * @param c2 column coordinate of second position
	 * @return true is step from (r1,c2) to (r2,c2) is a legal step for a
	 * knight on a chess board, false otherwise
	 */
	private boolean isLegalStep(final int r1, final int c1,
			final int r2, final int c2) {
		// step is legal if the difference in one direction is 2 steps,
		// in the other direction it is 1 step
		int d1 = Math.abs(r1 - r2);
		int d2 = Math.abs(c1 - c2);
		// total of differences must be 3
		if (3 == d1 + d2) {
			// check disjunction of the two possible combinations
			if (((1 == d1) && (2 == d2))
					|| ((2 == d1) && (1 == d2))) {
				return true;
			}
			// else matches with return false at end
		}
		return false;
	}

	/**
	 * Helper method to check that problem has no solution regardless of the
	 * chosen initial position.
	 * @param t specifies the problem
	 * @return true if no solution is found, false otherwise
	 */
	private boolean hasNoSolution(final Tour t) {
		int n = t.getDimension();
		int[] tour;
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				tour = t.getTour(i, j);
				assertTrue(0 == tour.length);
				// added assertion to localize test failure
				if (0 != tour.length) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * Helper method to check that tour is no solution.
	 * @param tour is the solution to evaluate
	 * @return true if length of tour is 0
	 */
	private boolean isNoSolution(final int[] tour) {
		return (0 == tour.length);
	}

	/**
	 * Helper method to check that problem has a solution. Argument on
	 * correctness is based on three conditions: 1. length of solution
	 * matches the number of fields on the board 2. each step,
	 * i.e. to adjacent positions in the solution, are a legal step of
	 * a knight 3. each field is visited exactly once.
	 * The conjunction of all 3 conditions is necessary and
	 * sufficient for a correct solution.
	 * @param t specifies the problem
	 * @param tour is the solution to evaluate
	 * @return true if given solution is correct
	 */
	private boolean isSolution(final Tour t, final int[] tour) {
		// check condition 1: length of sequence
		if (2 * t.getDimension() * t.getDimension() != tour.length) {
			return false;
		}
		// check condition 2: all steps are legal for a knight
		for (int i = 0; i < tour.length - 2; i = i + 2) {
			if (!isLegalStep(tour[i], tour[i + 1], tour[i + 2],
					tour[i + 3])) {
				return false;
			}
		}
		// check condition 3: each field visited exactly once
		// if sequence touches each field only once
		// we take advantage of the fact the (i,j) are
		// from range 0, 1, ... n-1
		// such that it can be mapped to values 0,1,...,2n-1
		// by a unique mapping k = i*n+j
		boolean[] m = new boolean[t.getDimension() * t.getDimension()];
		// make sure all values are set to false
		for (int i = 0; i < m.length; i++) {
			m[i] = false;
		}
		// check each field
		int index;
		for (int i = 0; i < tour.length; i = i + 2) {
			index = tour[i] * t.getDimension() + tour[i + 1];
			// if field is visited the first time (false)
			// mark as visited
			if (!m[index]) {
				m[index] = true;
			} else {
				// if visited the 2nd time, stop,
				// condition 3 failed, return false,
				return false;
			}
		}
		// passed all checks
		return true;
	}
}
