package ps5;

import java.awt.Color;
import java.util.*;
import junit.framework.*;
import ps3.*;

/**
 * <code>TrafficTest</code> contains some simple integration tests for your
 * traffic simulator using your <code>TrafficTestDriver</code> and is similar
 * to our private, automated staff tests.
 * You can assume that we will only pass well-formed input to your
 * test driver which meets the <code>requires</code> clauses of all
 * all the methods. You do not need to test how your driver behaves on
 * input which does not meet its requires clause.
 * 
 * The tests below use 8 lanes in a "plus sign" configuration.
 * 
 *                   gpNorth
 * 
 *           northIn  | |  northOut
 *                    v ^
 *                    | |
 *                    v ^
 *         westOut    | |     eastIn
 *         -<-<-<-<-<-/ \-<-<-<-<-<-
 * gpWest  ->->->->->|   |>->->->->-  gpEast
 *         westIn     \_/    eastOut
 *                    | |
 *                    v ^
 *                    | |
 *           southOut v ^  southIn
 *                    | |
 * 
 *                   gpSouth
 *
 * This file is just an example; at the very least, your solution should
 * pass these tests, but you will probably want to write some more complicated
 * tests to verify that your traffic simulator meets its specifications.
 */

public class TrafficTest extends TestCase {

    private static final double TOLERANCE = 0.01;

    private static TrafficTestDriver driver = new TrafficTestDriver();

    // center latitude and longitude and for GeoPoints
    private static int latCenter  = 42358333;
    private static int longCenter = -71060278;
    // one mile distances
    private static int lat1Mile   = 14488;
    private static int long1Mile  = 19579;
        
    // GeoPoints at the points and intersection of a "plus sign"
    private static GeoPoint gpCenter =
        new GeoPoint(latCenter, longCenter);
    private static GeoPoint gpWest = 
        new GeoPoint(latCenter, longCenter - long1Mile/2);
    private static GeoPoint gpEast =
        new GeoPoint(latCenter, longCenter + long1Mile/2);
    private static GeoPoint gpNorth = 
        new GeoPoint(latCenter + lat1Mile/2, longCenter);
    private static GeoPoint gpSouth = 
        new GeoPoint(latCenter - lat1Mile/2, longCenter);

    // Names (String labels) for the corresponding intersection
    private static String centerName = "center";

    // RoadSegments heading into the intersection or heading out.
    private static RoadSegment gsEastOut =
        new RoadSegment("East outbound", gpCenter, gpEast);
    private static RoadSegment gsEastIn =
        new RoadSegment("East inbound", gpEast, gpCenter);
    private static RoadSegment gsWestOut =
        new RoadSegment("West outbound", gpCenter, gpWest);
    private static RoadSegment gsWestIn =
        new RoadSegment("West inbound", gpWest, gpCenter);
    private static RoadSegment gsNorthOut =
        new RoadSegment("North outbound", gpCenter, gpNorth);
    private static RoadSegment gsNorthIn =
        new RoadSegment("North inbound", gpNorth, gpCenter);
    private static RoadSegment gsSouthOut =
        new RoadSegment("South outbound", gpCenter, gpSouth);
    private static RoadSegment gsSouthIn =
        new RoadSegment("South inbound", gpSouth, gpCenter);

    // Names (String labels) for the corresponding lanes
    private static String eastOutName  = "eastOut";
    private static String eastInName   = "eastIn";
    private static String westOutName  = "westOut";
    private static String westInName   = "westIn";
    private static String northOutName = "northOut";
    private static String northInName  = "northIn";
    private static String southOutName = "southOut";
    private static String southInName  = "southIn";

    // Routes starting at one outer point of the plus sign and ending at a
    // consecutive compass direction.
    private static Route rtWestNorth =
        new Route(gsWestIn).addSegment(gsNorthOut);
    private static Route rtWestSouth =
        new Route (gsWestIn).addSegment (gsSouthOut);
    private static Route rtSouthEast =
        new Route (gsSouthIn).addSegment (gsEastOut);
    private static Route rtSouthWest =
        new Route (gsSouthIn).addSegment (gsWestOut);
    private static Route rtNorthEast =
        new Route (gsNorthIn).addSegment (gsEastOut);
    private static Route rtNorthWest =
        new Route (gsNorthIn).addSegment (gsWestOut);
    private static Route rtEastNorth =
        new Route (gsEastIn).addSegment (gsNorthOut);
    private static Route rtEastSouth =
        new Route (gsEastIn).addSegment (gsSouthOut);

    // Routes starting at one outer point of the plus sign and ending at the
    // opposite compass direction.
    private static Route rtWestEast =
        new Route (gsWestIn).addSegment (gsEastOut);
    private static Route rtSouthNorth =
        new Route (gsSouthIn).addSegment (gsNorthOut);
    private static Route rtNorthSouth =
        new Route (gsNorthIn).addSegment (gsSouthOut);
    private static Route rtEastWest =
        new Route (gsEastIn).addSegment (gsWestOut);

    // Routes starting at one outer point of the plus sign going to the next
    // compass direction, doubling back to the intersection, and ending
    // directly opposite the starting point.
    private static Route rtWestNorthNorthEast = 
        new Route (gsWestIn)
        .addSegment (gsNorthOut)
        .addSegment (gsNorthIn)
        .addSegment (gsEastOut);
    private static Route rtSouthEastEastWest = 
        new Route (gsSouthIn)
        .addSegment (gsEastOut)
        .addSegment (gsEastIn)
        .addSegment (gsWestOut);
                
    // The cars
    private static Car car1;
    private static Car car2;
    private static Car car3;
    private static Car car4;
    private static Car car5;
    private static Car car6;
    private static Car car7;
    private static Car car8;
        
    private static String car1Name = "Car1";
    private static String car2Name = "Car2";
    private static String car3Name = "Car3";
    private static String car4Name = "Car4";
    private static String car5Name = "Car5";
    private static String car6Name = "Car6";
    private static String car7Name = "Car7";
    private static String car8Name = "Car8";

    // Define your own fixture here (use private fields)

    public TrafficTest(String name) {
        super(name);
    }

    // Tell JUnit to run all the tests in this class
    public static Test suite() {
        return new TestSuite(TrafficTest.class);
    }

    // JUnit calls setUp() before each test__ method is run
    protected void setUp() {
        // set up your fixture here before every test case
        car1 = new Car("Cooper Mini", Color.BLUE, rtWestNorth);
        car2 = new Car("Dodge Caravan", Color.BLACK, rtSouthWest);
        car3 = new Car("Delorean", Color.DARK_GRAY, rtEastSouth);
        car4 = new Car("Toyota Prius", Color.GREEN, rtNorthEast);
        car5 = new Car("Chevy Cavalier", Color.RED, rtWestNorth);
        car6 = new Car("BMW Z3 Roadster", Color.YELLOW, rtSouthEast);
        car7 = new Car("Checkered Cab", Color.WHITE, rtEastNorth);
        car8 = new Car("Touring Machine", Color.PINK, rtNorthWest);
    }

    // tests that injecting cars into a lane works
    public void testExample1() {
        String simName = "testExample1";
        int speedLimit = 22;

        driver.createSimulator(simName, 100.0);
        driver.addLane(simName, westInName, speedLimit, gsWestIn);
        driver.injectCar(simName, westInName, car1Name, car1);
        
        checkLane(simName, westInName, new String[] { car1Name });

        // drive the car for 1 second
        // this should be more than 30 feet, injecting a new car should work
        driver.simulate(simName);
        driver.injectCar(simName, westInName, car5Name, car5);
                
        checkLane(simName, westInName, new String[] { car1Name, car5Name });
    }

    // tests that basic cloverleaf turning works
    public void testExample2() {
        String simName = "testExample2";
        String cloverLeafName = centerName;
        int speedLimit = 22;

        driver.createSimulator(simName, 50.0);

        driver.addLane(simName, eastInName,  speedLimit, gsEastIn);
        driver.addLane(simName, westInName,  speedLimit, gsWestIn);
        driver.addLane(simName, southInName, speedLimit, gsSouthIn);
        driver.addLane(simName, northInName, speedLimit, gsNorthIn);

        driver.addLane(simName, eastOutName,  speedLimit, gsEastOut);
        driver.addLane(simName, westOutName,  speedLimit, gsWestOut);
        driver.addLane(simName, southOutName, speedLimit, gsSouthOut);
        driver.addLane(simName, northOutName, speedLimit, gsNorthOut);

        makeCloverLeaf(simName, cloverLeafName, gpCenter, speedLimit,
                       new String[] { eastInName,
                                      westInName,
                                      southInName,
                                      northInName
                       },
                       new String[] { eastOutName,
                                      westOutName,
                                      southOutName,
                                      northOutName });

        // inject one car at each outer point of the plus sign
        driver.injectCar(simName, westInName, car1Name, car1);
        driver.injectCar(simName, southInName, car2Name, car2);
        driver.injectCar(simName, eastInName, car3Name, car3);
        driver.injectCar(simName, northInName, car4Name, car4);
                
        // drive the cars to free up the lane entrance
        driver.simulate(simName);
                
        // inject new cars into the outer points of the plus sign
        driver.injectCar(simName, westInName, car5Name, car5);
        driver.injectCar(simName, southInName, car6Name, car6);
        driver.injectCar(simName, eastInName, car7Name, car7);
        driver.injectCar(simName, northInName, car8Name, car8);

        Vector carTurns = new Vector();
                
        carTurns.add(new CarTurn(car1Name, westInName, northOutName,
                                 simName, true));
        carTurns.add(new CarTurn(car2Name, southInName, westOutName,
                                 simName, true));
        carTurns.add(new CarTurn(car3Name, eastInName, southOutName,
                                 simName, true));
        carTurns.add(new CarTurn(car4Name, northInName, eastOutName,
                                 simName, true));
        
        checkCarTurns(simName, carTurns);
    }

    // tests that basic traffic light turning works
    public void testExample3() {
        String simName = "testExample3";
        int speedLimit = 22;
        double duration = 60.0;

        driver.createSimulator(simName, 50.0);
        
        driver.addLane(simName, eastInName, speedLimit, gsEastIn);
        driver.addLane(simName, westInName, speedLimit, gsWestIn);
        driver.addLane(simName, southInName, speedLimit, gsSouthIn);
        driver.addLane(simName, northInName, speedLimit, gsNorthIn);

        driver.addLane(simName, eastOutName, speedLimit, gsEastOut);
        driver.addLane(simName, westOutName, speedLimit, gsWestOut);
        driver.addLane(simName, southOutName, speedLimit, gsSouthOut);
        driver.addLane(simName, northOutName, speedLimit, gsNorthOut);

        makeTrafficLight(simName, centerName, gpCenter, speedLimit, duration,
                         new String[][] {new String[] { eastInName,
                                                        westInName },
                                         new String[] { southInName,
                                                        northInName }},
                         new String[] { eastOutName,
                                        westOutName,
                                        southOutName,
                                        northOutName });

        // inject one car at each outer point of the plus sign
        driver.injectCar(simName, westInName, car1Name, car1);
        driver.injectCar(simName, southInName, car2Name, car2);
        driver.injectCar(simName, eastInName, car3Name, car3);
        driver.injectCar(simName, northInName, car4Name, car4);
                
        // drive the cars to free up the lane entrance
        driver.simulate(simName);

        // inject new cars into the outer points of the plus sign
        driver.injectCar(simName, westInName, car5Name, car5);
        driver.injectCar(simName, southInName, car6Name, car6);
        driver.injectCar(simName, eastInName, car7Name, car7);
        driver.injectCar(simName, northInName, car8Name, car8);

        Vector carTurns = new Vector();

        // southInName has lowest heading (0 degrees), so cars in southIn and
        // northIn should turn while cars in eastIn and westIn should block.
        carTurns.add(new CarTurn(car1Name, westInName, northOutName,
                                 simName, false));
        carTurns.add(new CarTurn(car2Name, southInName, westOutName,
                                 simName, true));
        carTurns.add(new CarTurn(car3Name, eastInName, southOutName,
                                 simName, false));
        carTurns.add(new CarTurn(car4Name, northInName, eastOutName,
                                 simName, true));
        
        checkCarTurns(simName, carTurns);

    }

    /**
     * @requires driver.createSimulator(simName) &&
     *           !driver.addCloverLeaf(simName, cloverLeafName, location) &&
     *           for each incomingLane in incomingLaneNames,
     *               driver.addLane(simName, incomingLane, roadSeg) where
     *               roadSeg.getP2().equals(location) &&
     *           for each outgoingLane in outgoingLaneNames,
     *               driver.addLane(simName, trafficLightName, roadSeg) where
     *               roadSeg.getP1().equals(location) &&
     *           all params are non-null
     * @modifies driver
     * @effects creates a new clover leaf intersection in simName
     *          with the given name and location, and adds edges from
     *          all of the incoming edges to the intersection and
     *          adds edges to all of the outgoing edges from the intersection.
     */
    private void makeCloverLeaf(String simName, String cloverLeafName,
                                GeoPoint location, int speedLimit,
                                String[] incomingLaneNames,
                                String[] outgoingLaneNames) {
        assertTrue(simName != null);
        assertTrue(cloverLeafName != null);
        assertTrue(location != null);
        assertTrue(incomingLaneNames != null);
        assertTrue(outgoingLaneNames != null);

        driver.addCloverLeaf(simName, cloverLeafName, location);

        for (int i = 0; i < incomingLaneNames.length; i++) {
            driver.addEdge(simName, incomingLaneNames[i], cloverLeafName);
        }
        for (int i = 0; i < outgoingLaneNames.length; i++) {
            driver.addEdge(simName, cloverLeafName, outgoingLaneNames[i]);
        }
                           
    }

    /**
     * @requires driver.createSimulator(simName) &&
     *           !driver.addTrafficLight(simName, trafficLightName,
     *                                   location) &&
     *           for each greenGroup in greenGroupLaneNames,
     *               greenGroup is a String array with 1 or 2 elements &&
     *               for each incomingLane in greenGroup
     *                 driver.addLane(simName, incomingLane, roadSeg)
     *                 has been called where
     *                 roadSeg.getP2().equals(location) &&
     *           for each outgoingLane in outgoingLaneNames,
     *               driver.addLane(simName, trafficLightName, roadSeg)
     *               has been called where
     *               roadSeg.getP1().equals(location) &&
     *           all params are non-null &&
     *           greenGroupLaneNames, outgoingLaneNames do not have null or
     *             duplicate elements &&
     *           duration > 0.0
     * @modifies driver
     * @effects creates a new traffic light intersection in simName
     *          with the given name and location and adds edges from
     *          all of the incoming lanes in greenGroupLaneNames to the
     *          intersection and
     *          edges to all of the outgoing edges from the intersection.
     *          Furthermore, green groups are created for every String array of
     *          lane names in greenGroupLaneNames with the
     *          given duration.
     */
    private void makeTrafficLight(String simName, String trafficLightName,
                                  GeoPoint location, int speedLimit,
                                  double duration,
                                  String[][] greenGroupLaneNames,
                                  String[] outgoingLaneNames) {
        assertTrue(simName != null);
        assertTrue(trafficLightName != null);
        assertTrue(location != null);
        assertTrue(greenGroupLaneNames != null);
        assertTrue(outgoingLaneNames != null);
        
        driver.addTrafficLight(simName, trafficLightName, location);
        
        for (int i = 0; i < greenGroupLaneNames.length; i++) {
            int laneCount = greenGroupLaneNames[i].length;
            assertTrue(laneCount == 1 || laneCount == 2);
            for (int j = 0; j < laneCount; j++) {
                driver.addEdge(simName, greenGroupLaneNames[i][j],
                               trafficLightName);
            }
            if (laneCount == 1) {
                driver.defineGreenGroup(simName, greenGroupLaneNames[i][0],
                                        duration);
            }
            else {
                driver.defineGreenGroup(simName, greenGroupLaneNames[i][0],
                                        greenGroupLaneNames[i][1], duration);
            }
        }
        for (int i = 0; i < outgoingLaneNames.length; i++) {
            driver.addEdge(simName, trafficLightName, outgoingLaneNames[i]);
        }
                           
    }
    
    /**
     * @requires all parameters non-null &&
     *           driver.createSimulator(simName) &&
     *           driver.addLane(simName, laneName)
     * @effects If the given carNames do not appear in laneName in the given
     *           order, throws ComparisonFailedError. Otherwise does nothing.
     */
    private void checkLane(String simName, String laneName,
                           String[] carNames) {
        String[] actualNames = driver.listCars(simName, laneName);
        assertEquals("Correct number of cars", carNames.length,
                     actualNames.length);
        for (int i = 0; i < carNames.length; i++) {
            assertEquals("Correct car in position " + i, carNames[i],
                         actualNames[i]);
        }               
    }

    /**
     * @requires all parameters non-null && carTurns is a collection of CarTurn
     *           objects.
     * @effects If any one of the CarTurns fails its checkBefore and checkAfter
     *          method calls, then throws ComparisonFailedError. Otherwise,
     *          does nothing.
     */    
    private void checkCarTurns(String simName, Collection carTurns) {
        Iterator carTurnIt;

        carTurnIt = carTurns.iterator();
        while (carTurnIt.hasNext()) {
            CarTurn ct = (CarTurn)carTurnIt.next();
            ct.checkBefore();
        }

        driver.simulate(simName);

        carTurnIt = carTurns.iterator();
        while (carTurnIt.hasNext()) {
            CarTurn ct = (CarTurn)carTurnIt.next();
            ct.checkAfter();
        }
    }
    
    /**
     * Inner class which represents a car turn event for testing.
     * It is mutable.
     * 
     * @specfield carName       : String // label for a car
     * @specfield startLaneName : String // label for the car's current lane
     * @specfield endLaneName   : String // label for the lane the car will
     *                                      turn onto
     * @specfield simName       : String // label for the simulator containing
     *                                      the car and lanes
     * @specfield turn          : boolean // whether or not the turn should
     *                                       succeed
     */
    private static class CarTurn {
        
        // AF = record type, direct map from spec fields to real fields.
        // RI = all fields non-null &&
        //      startBeforeLength is the number of cars in startLaneName before
        //         the simulation step
        //      endBeforeLength is the number of cars in endLaneName before the
        //         the simulation step 
        
        private String carName;
        private String startLaneName;
        private String endLaneName;
        private String simName;
        private int startBeforeLength;
        private int endBeforeLength;
        private boolean turn;
        
        /**
         * @requires driver.createSimulator(simName) &&
         *           driver.addLane(simName, startLaneName) &&
         *           driver.addLane(simName, endLaneName) &&
         *           startLaneName is incoming to the same intersection that
         *           endLaneName is outgoing from &&
         *           carName names a car on startLaneName that will turn onto
         *           endLaneName
         * @modifies this
         * @effects initializes a new CarTurn object.
         */
        public CarTurn(String carName, String startLaneName,
                       String endLaneName, String simName, boolean turn) {
            this.carName = carName;
            this.startLaneName = startLaneName;
            this.endLaneName = endLaneName;
            this.simName = simName;
            this.turn = turn;
        }
        
        /**
         * @effects If carName does not appear in startLaneName, throws
         *          ComparisonFailedError. 
         */
        public void checkBefore() {
            String[] startCarNamesBefore = 
                driver.listCars(simName, startLaneName);
            String[] endCarNamesBefore = 
                driver.listCars(simName, endLaneName);
            assertEquals("startLane at least has this car in it", carName,
                         startCarNamesBefore[0]);
            startBeforeLength = startCarNamesBefore.length;
            endBeforeLength = endCarNamesBefore.length;
        }

        /**
         * @effects If turn is true and carName does not disappear from
         *          startLaneName and appear in endLaneName, or
         *          if turn is false and carName does not stay in
         *          startLaneName and remain absent from
         *          endLaneName, throws ComparisonFailedError.
         */     
        public void checkAfter() {
            String[] startCarNamesAfter =
                driver.listCars(simName, startLaneName);
            String[] endCarNamesAfter =
                driver.listCars(simName, endLaneName);
            if (turn) {
                assertEquals("startLane has decreased car count by one",
                             startBeforeLength - 1, startCarNamesAfter.length);
                assertEquals("endLane has increased car count by one",
                             endBeforeLength + 1, endCarNamesAfter.length);
                assertEquals("endLane's new car is this car", carName,
                             endCarNamesAfter[endBeforeLength]);
            }
            else {
                assertEquals("startLane has same car count",
                             startBeforeLength, startCarNamesAfter.length);
                assertEquals("endLane has same car count",
                             endBeforeLength, endCarNamesAfter.length);
                assertEquals("this car is still waiting to turn", carName,
                             startCarNamesAfter[0]);
            }
        }
    }

}
