/*
 *  Copyright 2001-2005 Stephen Colebourne
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.joda.time.chrono.gj;

import java.util.Random;

import junit.framework.TestCase;

import org.joda.time.Chronology;
import org.joda.time.DateTime;
import org.joda.time.DateTimeField;
import org.joda.time.chrono.GregorianChronology;
import org.joda.time.chrono.JulianChronology;

/**
 * Tests either the Julian or Gregorian chronology from org.joda.time.chrono.gj
 * against the implementations in this package. It tests all the date fields
 * against their principal methods.
 * <p>
 * Randomly generated values are fed into the DateTimeField methods and the
 * results are compared between the two chronologies. If any result doesn't
 * match, an error report is generated and the program exits. Each time this
 * test program is run, the pseudo random number generator is seeded with the
 * same value. This ensures consistent results between test runs.
 * <p>
 * The main method accepts three optional arguments: iterations, mode, seed. By
 * default, iterations is set to 1,000,000. The test will take several minutes
 * to run, depending on the computer's performance. Every 5 seconds a progress
 * message is printed.
 * <p>
 * The mode can be either 'g' for proleptic gregorian (the default) or 'j' for
 * proleptic julian. To override the default random number generator seed, pass
 * in a third argument which accepts a long signed integer.
 *
 * @author Brian S O'Neill
 */
public class MainTest extends TestCase {
    public static final int GREGORIAN_MODE = 0;
    public static final int JULIAN_MODE = 1;

    private static final long MILLIS_PER_YEAR = (long) (365.2425 * 24 * 60 * 60 * 1000);
    private static final long _1000_YEARS = 1000 * MILLIS_PER_YEAR;
    private static final long _500_YEARS = 500 * MILLIS_PER_YEAR;
    private static final long MAX_MILLIS = (10000 - 1970) * MILLIS_PER_YEAR;
    private static final long MIN_MILLIS = (-10000 - 1970) * MILLIS_PER_YEAR;

    // Show progess reports every 5 seconds.
    private static final long UPDATE_INTERVAL = 5000;

    /**
     * Arguments: iterations [mode [seed]]
     */
    public static void main(String[] args) throws Exception {
        int iterations = 1000000;
        int mode = GREGORIAN_MODE;
        long seed = 1345435247779935L;

        if (args.length > 0) {
            iterations = Integer.parseInt(args[0]);
            if (args.length > 1) {
                if (args[1].startsWith("g")) {
                    mode = GREGORIAN_MODE;
                } else if (args[1].startsWith("j")) {
                    mode = JULIAN_MODE;
                } else {
                    throw new IllegalArgumentException
                        ("Unknown mode: " + args[1]);
                }
                if (args.length > 2) {
                    seed = Long.parseLong(args[2]);
                }
            }
        }

        new MainTest(iterations, mode, seed).testChronology();
    }

    //-----------------------------------------------------------------------
    private final int iIterations;
    private final int iMode;
    private final long iSeed;
    private final Chronology iTest;
    private final Chronology iActual;

    /**
     * @param iterations number of test iterations to perform
     * @param mode GREGORIAN_MODE or JULIAN_MODE,0=Gregorian, 1=Julian
     * @param seed seed for random number generator
     */
    public MainTest(int iterations, int mode, long seed) {
        super("testChronology");
        iIterations = iterations;
        iMode = mode;
        iSeed = seed;
        if (mode == GREGORIAN_MODE) {
            iTest = new TestGregorianChronology();
            iActual = GregorianChronology.getInstanceUTC();
        } else {
            iTest = new TestJulianChronology();
            iActual = JulianChronology.getInstanceUTC();
        }
    }

    //-----------------------------------------------------------------------
    /**
     * Main junit test
     */
    public void testChronology() {
        int iterations = iIterations;
        long seed = iSeed;
        String modeStr;
        if (iMode == GREGORIAN_MODE) {
            modeStr = "Gregorian";
        } else {
            modeStr = "Julian";
        }

        System.out.println("\nTesting " + modeStr + " chronology over " + iterations + " iterations");

        Random rnd = new Random(seed);
        long updateMillis = System.currentTimeMillis() + UPDATE_INTERVAL;

        for (int i=0; i<iterations; i++) {
            long now = System.currentTimeMillis();
            if (now >= updateMillis) {
                updateMillis = now + UPDATE_INTERVAL;
                double complete = ((int)((double)i / iterations * 1000.0)) / 10d;
                if (complete < 100) {
                    System.out.println("" + complete + "% complete (i=" + i + ")");
                }
            }

            long millis = randomMillis(rnd);
            int value = rnd.nextInt(200) - 100;
            // millis2 is used for difference tests.
            long millis2 = millis + rnd.nextLong() % _1000_YEARS - _500_YEARS;

            try {
                testFields(millis, value, millis2);
            } catch (RuntimeException e) {
                System.out.println("Failure index: " + i);
                System.out.println("Test millis: " + millis);
                System.out.println("Test value: " + value);
                System.out.println("Test millis2: " + millis2);
                fail(e.getMessage());
            }
        }

        System.out.println("100% complete (i=" + iterations + ")");
    }

    //-----------------------------------------------------------------------
    private void testFields(long millis, int value, long millis2) {
        testField(iTest.year(), iActual.year(), millis, value, millis2);
        testField(iTest.monthOfYear(), iActual.monthOfYear(), millis, value, millis2);
        testField(iTest.dayOfMonth(), iActual.dayOfMonth(), millis, value, millis2);
        testField(iTest.weekyear(), iActual.weekyear(), millis, value, millis2);
        testField(iTest.weekOfWeekyear(),
                  iActual.weekOfWeekyear(), millis, value, millis2);
        testField(iTest.dayOfWeek(), iActual.dayOfWeek(), millis, value, millis2);
        testField(iTest.dayOfYear(), iActual.dayOfYear(), millis, value, millis2);
    }

    private void testField(DateTimeField fieldA, DateTimeField fieldB, long millis,
                           int value, long millis2)
    {
        int a, b;
        long x, y;
        boolean m, n;

        // get test
        a = fieldA.get(millis);
        b = fieldB.get(millis);
        testValue(fieldA, fieldB, "get", millis, a, b);

        // getMaximumValue test
        // Restrict this test to the fields that matter.
        Class fieldClass = fieldA.getClass();
        if (fieldClass == TestGJDayOfYearField.class ||
            fieldClass == TestGJDayOfMonthField.class ||
            fieldClass == TestGJWeekOfWeekyearField.class) {
            
            a = fieldA.getMaximumValue(millis);
            b = fieldB.getMaximumValue(millis);
            testValue(fieldA, fieldB, "getMaximumValue", millis, a, b);
        }

        // set test
        a = getWrappedValue
            (value, fieldA.getMinimumValue(millis), fieldA.getMaximumValue(millis));
        b = getWrappedValue
            (value, fieldB.getMinimumValue(millis), fieldB.getMaximumValue(millis));
        if (iMode == JULIAN_MODE && a == 0
            && (fieldA.getName().equals("year") || fieldA.getName().equals("weekyear"))) {
            // Exclude setting Julian year of zero.
        } else {
            x = fieldA.set(millis, a);
            y = fieldB.set(millis, b);
            testMillis(fieldA, fieldB, "set", millis, x, y, a, b);
        }

        // roundFloor test
        x = fieldA.roundFloor(millis);
        y = fieldB.roundFloor(millis);
        testMillis(fieldA, fieldB, "roundFloor", millis, x, y);

        // roundCeiling test
        x = fieldA.roundCeiling(millis);
        y = fieldB.roundCeiling(millis);
        testMillis(fieldA, fieldB, "roundCeiling", millis, x, y);

        // roundHalfFloor test
        x = fieldA.roundHalfFloor(millis);
        y = fieldB.roundHalfFloor(millis);
        testMillis(fieldA, fieldB, "roundHalfFloor", millis, x, y);

        // roundHalfEven test
        x = fieldA.roundHalfEven(millis);
        y = fieldB.roundHalfEven(millis);
        testMillis(fieldA, fieldB, "roundHalfEven", millis, x, y);

        // remainder test
        x = fieldA.remainder(millis);
        y = fieldB.remainder(millis);
        testMillis(fieldA, fieldB, "remainder", millis, x, y);

        // add test
        x = fieldA.add(millis, value);
        y = fieldB.add(millis, value);
        testMillis(fieldA, fieldB, "add", millis, x, y);

        // addWrapField test
        x = fieldA.addWrapField(millis, value);
        y = fieldB.addWrapField(millis, value);
        testMillis(fieldA, fieldB, "addWrapField", millis, x, y);

        // getDifference test
        x = fieldA.getDifference(millis, millis2);
        y = fieldB.getDifference(millis, millis2);
        try {
            testValue(fieldA, fieldB, "getDifference", millis, x, y);
        } catch (RuntimeException e) {
            System.out.println("Test datetime 2: " + makeDatetime(millis2));
            throw e;
        }

        // isLeap test
        m = fieldA.isLeap(millis);
        n = fieldB.isLeap(millis);
        testBoolean(fieldA, fieldB, "isLeap", millis, m, n);

        // getLeapAmount test
        a = fieldA.getLeapAmount(millis);
        b = fieldB.getLeapAmount(millis);
        testValue(fieldA, fieldB, "getLeapAmount", millis, a, b);
    }

    private int getWrappedValue(int value, int minValue, int maxValue) {
        if (minValue >= maxValue) {
            throw new IllegalArgumentException("MIN > MAX");
        }

        int wrapRange = maxValue - minValue + 1;
        value -= minValue;

        if (value >= 0) {
            return (value % wrapRange) + minValue;
        }

        int remByRange = (-value) % wrapRange;

        if (remByRange == 0) {
            return 0 + minValue;
        }
        return (wrapRange - remByRange) + minValue;
    }

    private void testValue(DateTimeField fieldA, DateTimeField fieldB,
                           String method, long millis, long valueA, long valueB) {
        if (valueA != valueB) {
            failValue(fieldA, fieldB, method, millis, valueA, valueB);
        }
    }

    private void testMillis(DateTimeField fieldA, DateTimeField fieldB,
                            String method, long millis, long millisA, long millisB) {
        if (millisA != millisB) {
            failMillis(fieldA, fieldB, method, millis, millisA, millisB);
        }
    }

    private void testMillis(DateTimeField fieldA, DateTimeField fieldB,
                            String method, long millis, long millisA, long millisB,
                            int valueA, int valueB) {
        if (millisA != millisB) {
            failMillis(fieldA, fieldB, method, millis, millisA, millisB, valueA, valueB);
        }
    }

    private void testBoolean(DateTimeField fieldA, DateTimeField fieldB,
                             String method, long millis, boolean boolA, boolean boolB) {
        if (boolA != boolB) {
            failBoolean(fieldA, fieldB, method, millis, boolA, boolB);
        }
    }

    private void failValue(DateTimeField fieldA, DateTimeField fieldB,
                           String method, long millis, long valueA, long valueB) {
        System.out.println("Failure on " + makeName(fieldA, fieldB) + "." + method);
        System.out.println(fieldA.getClass().getName() + "\n\tvs. "
                           + fieldB.getClass().getName());
        System.out.println("Datetime: " + makeDatetime(millis));
        System.out.println("Millis from 1970: " + millis);
        System.out.println(valueA + " != " + valueB);
        throw new RuntimeException();
    }

    private void failMillis(DateTimeField fieldA, DateTimeField fieldB,
                            String method, long millis, long millisA, long millisB) {
        System.out.println("Failure on " + makeName(fieldA, fieldB) + "." + method);
        System.out.println(fieldA.getClass().getName() + "\n\tvs. "
                           + fieldB.getClass().getName());
        System.out.println("Datetime: " + makeDatetime(millis));
        System.out.println("Millis from 1970: " + millis);
        System.out.println(makeDatetime(millisA) + " != " + makeDatetime(millisB));
        System.out.println(millisA + " != " + millisB);
        System.out.println("Original value as reported by first field: " +
                           fieldA.get(millis));
        System.out.println("Original value as reported by second field: " +
                           fieldB.get(millis));
        System.out.println("First new value as reported by first field: " +
                           fieldA.get(millisA));
        System.out.println("First new value as reported by second field: " +
                           fieldB.get(millisA));
        System.out.println("Second new value as reported by first field: " +
                           fieldA.get(millisB));
        System.out.println("Second new value as reported by second field: " +
                           fieldB.get(millisB));
        throw new RuntimeException();
    }

    private void failMillis(DateTimeField fieldA, DateTimeField fieldB,
                            String method, long millis, long millisA, long millisB,
                            int valueA, int valueB) {
        System.out.println("Failure on " + makeName(fieldA, fieldB) + "." + method);
        System.out.println(fieldA.getClass().getName() + "\n\tvs. "
                           + fieldB.getClass().getName());
        System.out.println("Datetime: " + makeDatetime(millis));
        System.out.println("Millis from 1970: " + millis);
        System.out.println(makeDatetime(millisA) + " != " + makeDatetime(millisB));
        System.out.println(millisA + " != " + millisB);
        System.out.println("Original value as reported by first field: " +
                           fieldA.get(millis));
        System.out.println("Original value as reported by second field: " +
                           fieldB.get(millis));
        System.out.println("First new value as reported by first field: " +
                           fieldA.get(millisA));
        System.out.println("First new value as reported by second field: " +
                           fieldB.get(millisA));
        System.out.println("Second new value as reported by first field: " +
                           fieldA.get(millisB));
        System.out.println("Second new value as reported by second field: " +
                           fieldB.get(millisB));
        System.out.println("Value to set for first field: " + valueA);
        System.out.println("Value to set for second field: " + valueB);
        throw new RuntimeException();
    }

    private void failBoolean(DateTimeField fieldA, DateTimeField fieldB,
                             String method, long millis, boolean boolA, boolean boolB) {
        System.out.println("Failure on " + makeName(fieldA, fieldB) + "." + method);
        System.out.println(fieldA.getClass().getName() + "\n\tvs. "
                           + fieldB.getClass().getName());
        System.out.println("Datetime: " + makeDatetime(millis));
        System.out.println("Millis from 1970: " + millis);
        System.out.println(boolA + " != " + boolB);
        throw new RuntimeException();
    }

    private String makeName(DateTimeField fieldA, DateTimeField fieldB) {
        if (fieldA.getName().equals(fieldB.getName())) {
            return fieldA.getName();
        } else {
            return fieldA.getName() + "/" + fieldB.getName();
        }
    }

    private String makeDatetime(long millis) {
        return makeDatetime(millis, iActual);
    }

    private String makeDatetime(long millis, Chronology chrono) {
        return chrono.dayOfWeek().getAsShortText(millis) + " "
            + new DateTime(millis, chrono).toString() + " / " +
            chrono.weekyear().get(millis) + "-W" + chrono.weekOfWeekyear().get(millis) +
            "-" + chrono.dayOfWeek().get(millis);
    }

    private String makeDate(long millis) {
        return makeDate(millis, iActual);
    }

    private String makeDate(long millis, Chronology chrono) {
        return chrono.dayOfWeek().getAsShortText(millis) + " "
            + new DateTime(millis, chrono).toString("yyyy-MM-dd") + " / " +
            chrono.weekyear().get(millis) + "-W" + chrono.weekOfWeekyear().get(millis) +
            "-" + chrono.dayOfWeek().get(millis);
    }

    //-----------------------------------------------------------------------
    private static long randomMillis(Random rnd) {
        long millis = rnd.nextLong();
        if (millis >= 0) {
            millis = millis % MAX_MILLIS;
        } else {
            millis = millis % -MIN_MILLIS;
        }
        return millis;
    }

    private static void dump(Chronology chrono, long millis) {
        System.out.println("year:           " + chrono.year().get(millis));
        System.out.println("monthOfYear:    " + chrono.monthOfYear().get(millis));
        System.out.println("dayOfMonth:     " + chrono.dayOfMonth().get(millis));
        System.out.println("weekyear:       " + chrono.weekyear().get(millis));
        System.out.println("weekOfWeekyear: " + chrono.weekOfWeekyear().get(millis));
        System.out.println("dayOfWeek:      " + chrono.dayOfWeek().get(millis));
        System.out.println("dayOfYear:      " + chrono.dayOfYear().get(millis));
    }

}
