/* CityTester.java
 *   Csu370 :: 01/12/2008
 *   Test Program for 'City'; Assignment #1
 */

import java.util.Random;

/* A program to perform Black Box testing of the City class
 * In later assignments you may be asked to write a Test program
 *   much like this, so you should review any of the concepts 
 *   used here that are not familiar to you.  Or ask a TA
 *   or Professor if you are otherwised confused. */

public class CityTester {

    /** Main method. After compiling it can be run with: java CityTester
     *  Or imported into eclipse
     */

    public static void main(String[] args){
	/* Create a new Tester, run the tests, and Print out the results. */
	CityTester t = new CityTester();

	t.testCities();  //create some City objects and test them
	t.testExceptions();  //see if exceptions are thrown that should be

	/* A few Stats... */
	System.out.println("\nNumber of cities tested: " + t.totCities);
	System.out.println("Number of tests performed: " + t.ntests);
	System.out.println("Number of errors found: " + t.nerr);
    }

    /* Variables which will track the progress of out Tests */

    private int ntests = 0;        // Total Tests Run
    private int nerr = 0;          // Number of Errors

    /* Number of dots to print before we go to the next line */

    private static final int DOTS_PER_LINE = 50;

    /* Update the test counters based on the result given. Result
     *   is expected to be true for passing tests, and false for
     *   failing tests.  If a test fails, we print out the provided
     *   message so the user can see what might have gone wrong. 
     * Be sure to review anything that doesn't make sense. */

    private void assertTrue (boolean result, String msg){
	ntests++;
	if(!result){
	    System.out.println("\n**ERROR**: test# " + ntests + " -- " + msg);
	    nerr ++;
	}
	if(ntests % DOTS_PER_LINE == 0)System.out.println();
	System.out.print(".");
    }


    /* Variables that will track the totals for cities, population, northmost
       city, and southmost city in order to check the static methods */
    
    private int totCities = 0; // Number of Classrooms Created
    private int totPop = 0;   // Total Population
    private String northMost = null;  // Northmost city
    private String southMost = null;  // Southmost city
    private double northMostLatitude = 0;
    private double southMostLatitude = 0;
	
    /* How Many Cities to Create? */
    private static final int NUM_TO_TEST = 20;

    private void testCities(){
	// create a city which will be the base city to be compared with 
	// the other cities, and update the "global" variables
	City base = City.basic("Cambridge", 500000, 45, 75);
	northMostLatitude = 45;
	southMostLatitude = 45;
	northMost = "Cambridge";
	southMost = "Cambridge";
	totCities++;
	totPop += 500000;

	Random r = new Random();
	/* Create random Cities, and Test them */
	for(int i = 0; i < NUM_TO_TEST; i++){
	    String name = "City #" + i;
	    boolean b = r.nextInt(10) >= 5;
	    String state = b ? "State #" + i : null;
	    int pop = r.nextInt(1000000) + 1;
	    double latitude = b ? r.nextDouble() * 90 : r.nextDouble() * (-90);
	    double longitude 
		= b ? r.nextDouble() * 180 : r.nextDouble() * (-180);

	    /* Create the City object and test it */
	    testCity(name, state, pop, latitude, longitude, base);
	}

	/* Create "border case" Cities and test them */
	testCity("Boston", "MA", 600000, -90, -180, base);
	testCity("Boston", "MA", 600000, -90, 180, base);
	testCity("Boston", "MA", 600000, 90, -180, base);
	testCity("Boston", "MA", 600000, 90, 180, base);
	testCity("Malta", null, 1, 0, 90, base);
	testCity("Juneau", "Alaska", 77777, 90, 0, base);
    }

    /* Test a single City, including static methods */
    private void testCity(String name, String state, int pop,
			  double latitude, double longitude, City base) {
	try{
	    //create the object to be tested
	    City c;
	    String capstr = "";
	    if (state != null) {
		c = City.capital(name, state, pop, latitude, longitude);
		capstr = "is the capital of " + state + ", it";
		}
	    else
		c = City.basic(name, pop, latitude, longitude);

	    /* Update the numbers for our static method checks later*/
	    // update the northmost and southmost longitude if necessary
	    if (latitude > northMostLatitude) {
	       northMostLatitude = latitude;
	       northMost = name;
               }
	    if (latitude < southMostLatitude) {
	       southMostLatitude = latitude;
	       southMost = name;
	       }
	    totCities++;
	    totPop += pop;

	    /* Check each of the accessor methods */
	    assertTrue(c.getName().equals(name), "getName()");
	    assertTrue(c.getPopulation() == pop, "getPopulation()");
	    assertTrue(c.getLatitude() == latitude, "getLatitude()");
	    assertTrue(c.getLongitude() == longitude, "getLongitude()");
	    if (state != null) {
		assertTrue(c.capitalOf().equals(state), "capitalOf()");
		assertTrue(c.isCapital(), "isCapital()");
	    } else {
		assertTrue(! c.isCapital(), "isCapital()");
	    }	       
	    // northSouthDist
	    double expected = 69.17 * Math.abs(c.getLatitude() - 
					       base.getLatitude());
	    double real = c.northSouthDist(base);
	    assertTrue(closeEnough(expected, real), "northSouthDist()");

	    // eastWestDist
	    double diff = Math.abs(c.getLongitude() - base.getLongitude());
	    diff = (diff <= 180) ? diff : (360 - diff);
	    expected =  diff * 69.17 * 
		Math.cos(c.getLatitude() * Math.PI / 180);
	    real = c.eastWestDist(base);
	    assertTrue(closeEnough(expected, real), "eastWestDist()");

	    // isLarger
	    boolean b;
	    b = (c.getPopulation() > base.getPopulation()) ? true : false;
	    assertTrue(b == c.isLarger(base), "isLarger()");

	    // toString()
	    String expectedStr =
		name + capstr + " has population " + pop + 
		" and is located at [" + latitude + ", " + 
		longitude + "]";
	    assertTrue(c.toString().equals(expectedStr), "toString()");
	    
	    /* Test the static methods for each City  */
	    testStaticMethods();
	    
	} catch(RuntimeException e){
	    
	    /* If there was an exception anywhere in there, then we
	     *   have a problem */
	    assertTrue(false, "Exception: "+e.getMessage());
	}
    }

    /* Safe test for floating-point numbers.  Sometimes due to rounding/
     *   arithmetic ordering it's possible to get two different results
     *   for the same mathematical expression.  I'm assuming here that
     *   we will be at least within 1/10000 th of each other. */

    private static boolean closeEnough(double a, double b)
    {   return ((Math.abs(a-b)) < 0.0001); }

    /* Make sure each of the static methods agrees with our number (and
     *   the specifications). */

    private void testStaticMethods(){
	assertTrue(City.cityCount() == totCities, 
		   "City.cityCount()");
	assertTrue(City.populationTotal() == totPop, 
		   "City.populationTotal()");
	assertTrue(City.northMost().equals(northMost), "City.northMost()");
	assertTrue(City.southMost().equals(southMost), "City.southMost()");
    }

    /* Make sure exceptions are thrown for border cases.
     * Review try{...}catch(...){...}
     * if this looks fishy, or ask your friendly TA ;) */

    /* We make sure that good tests get counted by using:
     *       assertTrue(true,""); 
     *   and force bad tests to fail with:
     *       assertTrue(false,"Failure Message"); 
     *   making sure that we test both the basic and capital 
     *   static creators.  */

    private void testExceptions(){
	/* latitude < -90 */
	try{ 
	    if (City.basic("Boston", 600000, -91, 75) != null)
		assertTrue (false, "[basic] No Exception with latitude < -90");
	} catch(RuntimeException e){ assertTrue (true, ""); }
	/* latitude > 90 */
	try{ 
	    if (City.basic("Boston", 600000, 105, 75) != null)
		assertTrue (false, "[basic] No Exception with latitude > 90");
	} catch(RuntimeException e){ assertTrue (true, ""); }
	/* longitude < -180 */
	try{ 
	    if (City.basic("Boston", 600000, 50, -181) != null)
		assertTrue (false, 
			    "[basic] No Exception with longitude < -180");
	} catch(RuntimeException e){ assertTrue (true, ""); }
	/* longitude > 180 */
	try{ 
	    if (City.basic("Boston", 600000, 50, 250) != null)
		assertTrue (false, 
			    "[basic] No Exception with longitude > 180");
	} catch(RuntimeException e){ assertTrue (true, ""); }

	/* latitude < -90 */
	try{ 
	    if (City.capital("Boston", "MA", 600000, -91, 75) != null)
		assertTrue (false, 
			    "[capital] No Exception with latitude < -90");
	} catch(RuntimeException e){ assertTrue (true, ""); }
	/* latitude > 90 */
	try{ 
	    if (City.capital("Boston", "MA", 600000, 105, 75) != null)
		assertTrue (false, "[capital] No Exception with latitude > 90");
	} catch(RuntimeException e){ assertTrue (true, ""); }
	/* longitude < -180 */
	try{ 
	    if (City.capital("Boston", "MA", 600000, 50, -181) != null)
		assertTrue (false, 
			    "[capital] No Exception with longitude < -180");
	} catch(RuntimeException e){ assertTrue (true, ""); }
	/* longitude > 180 */
	try{ 
// 	    if (City.capital("Boston", "MA", 600000, 50, 250) != null)
	    City.capital("Boston", "MA", 600000, 50, 250);
		assertTrue (false, 
			    "[capital] No Exception with longitude > 180");
	} catch(RuntimeException e){ assertTrue (true, ""); }

	/* call capitalOf() on a city which isn't a capital */
	try{ 
	    City.basic("Cambridge", 500000, 50, 140).capitalOf();
	    assertTrue (false, 
			"[basic] No Exception with calling capitalOf() "
			+ "on a non-capital city");
	} catch(RuntimeException e){ assertTrue (true, ""); }
    }
}


