// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
 *******************************************************************************
 * Copyright (C) 1996-2015, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.dev.test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.Policy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.TreeMap;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;

import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;

/**
 * TestFmwk is a base class for tests that can be run conveniently from the
 * command line as well as under the Java test harness.
 * <p>
 * Sub-classes implement a set of methods named Test <something>. Each of these
 * methods performs some test. Test methods should indicate errors by calling
 * either err or errln. This will increment the errorCount field and may
 * optionally print a message to the log. Debugging information may also be
 * added to the log via the log and logln methods. These methods will add their
 * arguments to the log only if the test is being run in verbose mode.
 */
abstract public class TestFmwk extends AbstractTestLog {
    /**
     * The default time zone for all of our tests. Used in @Before
     */
    private final static TimeZone defaultTimeZone = TimeZone.getTimeZone("America/Los_Angeles");

    /**
     * The default locale used for all of our tests. Used in @Before
     */
    private final static Locale defaultLocale = Locale.US;

    private static final String EXHAUSTIVENESS = "ICU.exhaustive";
    private static final int DEFAULT_EXHAUSTIVENESS = 0;
    private static final int MAX_EXHAUSTIVENESS = 10;

    private static final String LOGGING_LEVEL = "ICU.logging";
    private static final int DEFAULT_LOGGING_LEVEL = 0;
    private static final int MAX_LOGGING_LEVEL = 3;

    public static final int LOGGING_NONE = 0;
    public static final int LOGGING_WARN = 1;
    public static final int LOGGING_INFO = 2;
    public static final int LOGGING_DEBUG = 3;

    private static final String SEED = "ICU.seed";
    private static final String SECURITY_POLICY = "ICU.securitypolicy";

    private static final TestParams testParams;
    static {
        testParams = TestParams.create();
    }

    protected TestFmwk() {
    }

    @Before
    public final void testInitialize() {
        Locale.setDefault(defaultLocale);
        TimeZone.setDefault(defaultTimeZone);

        if (getParams().testSecurityManager != null) {
            System.setSecurityManager(getParams().testSecurityManager);
        }

        localTestInitialize();
    }

    /**
     * This method is called at the end of {@link #testInitialize()}.
     * Because JUnit does not guarantee the order of multiple Before
     * methods, TestFmwk implementation class should override this
     * method, instead of annotating Before.
     */
    protected void localTestInitialize() {
    }

    /**
     * This method is called at the beginning of {@link #testTeardown()}.
     * TestFmwk implementation class hould override this method, instead
     * of annotating After.
     */
    protected void localTestTeardown() {
    }

    @After
    public final void testTeardown() {
        localTestTeardown();

        if (getParams().testSecurityManager != null) {
            System.setSecurityManager(getParams().originalSecurityManager);
        }
    }

    private static TestParams getParams() {
        //return paramsReference.get();
        return testParams;
    }

    protected static boolean isVerbose() {
        return getParams().getLoggingLevel() >= LOGGING_INFO;
    }

    /**
     * 0 = fewest tests, 5 is normal build, 10 is most tests
     */
    protected static int getExhaustiveness() {
        return getParams().inclusion;
    }

    protected static boolean isQuick() {
        return getParams().getInclusion() == 0;
    }

    // use this instead of new random so we get a consistent seed
    // for our tests
    protected Random createRandom() {
        return new Random(getParams().getSeed());
    }

    /**
     * Integer Random number generator, produces positive int values.
     * Similar to C++ std::minstd_rand, with the same algorithm & constants.
     * Provided for compatibility with ICU4C.
     * Get & set of the seed allows for reproducible monkey tests.
     */
    protected class ICU_Rand {
        private int fLast;

        public ICU_Rand(int seed) {
            seed(seed);
        }

        public int next() {
            fLast = (int)((fLast * 48271L) % 2147483647L);
            return fLast;
        }

        public void seed(int seed) {
            if (seed <= 0) {
                seed = 1;
            }
            seed %= 2147483647;   // = 0x7FFFFFFF
            fLast = seed > 0 ? seed : 1;
        }

        public int getSeed() {
            return fLast;
        }

    }

    static final String ICU_TRAC_URL = "http://bugs.icu-project.org/trac/ticket/";
    static final String CLDR_TRAC_URL = "http://unicode.org/cldr/trac/ticket/";
    static final String CLDR_TICKET_PREFIX = "cldrbug:";

    /**
     * Log the known issue.
     * This method returns true unless -prop:logKnownIssue=no is specified
     * in the argument list.
     *
     * @param ticket A ticket number string. For an ICU ticket, use numeric characters only,
     * such as "10245". For a CLDR ticket, use prefix "cldrbug:" followed by ticket number,
     * such as "cldrbug:5013".
     * @param comment Additional comment, or null
     * @return true unless -prop:logKnownIssue=no is specified in the test command line argument.
     */
    protected static boolean logKnownIssue(String ticket, String comment) {
        if (!getBooleanProperty("logKnownIssue", true)) {
            return false;
        }

        // TODO: This method currently does not do very much.
        // See http://bugs.icu-project.org/trac/ticket/12589

        StringBuffer descBuf = new StringBuffer();
        // TODO(junit) : what to do about this?
        //getParams().stack.appendPath(descBuf);
        if (comment != null && comment.length() > 0) {
            descBuf.append(" (" + comment + ")");
        }
        String description = descBuf.toString();

        String ticketLink = "Unknown Ticket";
        if (ticket != null && ticket.length() > 0) {
            boolean isCldr = false;
            ticket = ticket.toLowerCase(Locale.ENGLISH);
            if (ticket.startsWith(CLDR_TICKET_PREFIX)) {
                isCldr = true;
                ticket = ticket.substring(CLDR_TICKET_PREFIX.length());
            }
            ticketLink = (isCldr ? CLDR_TRAC_URL : ICU_TRAC_URL) + ticket;
        }

        if (getParams().knownIssues == null) {
            getParams().knownIssues = new TreeMap<String, List<String>>();
        }
        List<String> lines = getParams().knownIssues.get(ticketLink);
        if (lines == null) {
            lines = new ArrayList<String>();
            getParams().knownIssues.put(ticketLink, lines);
        }
        if (!lines.contains(description)) {
            lines.add(description);
        }

        return true;
    }

    protected static String getProperty(String key) {
        return getParams().getProperty(key);
    }

    protected static boolean getBooleanProperty(String key) {
        return getParams().getBooleanProperty(key);
    }

    protected static boolean getBooleanProperty(String key, boolean defVal) {
        return getParams().getBooleanProperty(key, defVal);
    }

    protected static int getIntProperty(String key, int defVal) {
        return getParams().getIntProperty(key, defVal);
    }

    protected static int getIntProperty(String key, int defVal, int maxVal) {
        return getParams().getIntProperty(key, defVal, maxVal);
    }

    protected static TimeZone safeGetTimeZone(String id) {
        TimeZone tz = TimeZone.getTimeZone(id);
        if (tz == null) {
            // should never happen
            errln("FAIL: TimeZone.getTimeZone(" + id + ") => null");
        }
        if (!tz.getID().equals(id)) {
            warnln("FAIL: TimeZone.getTimeZone(" + id + ") => " + tz.getID());
        }
        return tz;
    }


    // Utility Methods

    protected static String hex(char[] s){
        StringBuffer result = new StringBuffer();
        for (int i = 0; i < s.length; ++i) {
            if (i != 0) result.append(',');
            result.append(hex(s[i]));
        }
        return result.toString();
    }

    protected static String hex(byte[] s){
        StringBuffer result = new StringBuffer();
        for (int i = 0; i < s.length; ++i) {
            if (i != 0) result.append(',');
            result.append(hex(s[i]));
        }
        return result.toString();
    }

    protected static String hex(char ch) {
        StringBuffer result = new StringBuffer();
        String foo = Integer.toString(ch, 16).toUpperCase();
        for (int i = foo.length(); i < 4; ++i) {
            result.append('0');
        }
        return result + foo;
    }

    protected static String hex(int ch) {
        StringBuffer result = new StringBuffer();
        String foo = Integer.toString(ch, 16).toUpperCase();
        for (int i = foo.length(); i < 4; ++i) {
            result.append('0');
        }
        return result + foo;
    }

    protected static String hex(CharSequence s) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < s.length(); ++i) {
            if (i != 0)
                result.append(',');
            result.append(hex(s.charAt(i)));
        }
        return result.toString();
    }

    protected static String prettify(CharSequence s) {
        StringBuilder result = new StringBuilder();
        int ch;
        for (int i = 0; i < s.length(); i += Character.charCount(ch)) {
            ch = Character.codePointAt(s, i);
            if (ch > 0xfffff) {
                result.append("\\U00");
                result.append(hex(ch));
            } else if (ch > 0xffff) {
                result.append("\\U000");
                result.append(hex(ch));
            } else if (ch < 0x20 || 0x7e < ch) {
                result.append("\\u");
                result.append(hex(ch));
            } else {
                result.append((char) ch);
            }

        }
        return result.toString();
    }

    private static java.util.GregorianCalendar cal;

    /**
     * Return a Date given a year, month, and day of month. This is similar to
     * new Date(y-1900, m, d). It uses the default time zone at the time this
     * method is first called.
     *
     * @param year
     *            use 2000 for 2000, unlike new Date()
     * @param month
     *            use Calendar.JANUARY etc.
     * @param dom
     *            day of month, 1-based
     * @return a Date object for the given y/m/d
     */
    protected static synchronized java.util.Date getDate(int year, int month,
            int dom) {
        if (cal == null) {
            cal = new java.util.GregorianCalendar();
        }
        cal.clear();
        cal.set(year, month, dom);
        return cal.getTime();
    }

    private static class TestParams {

        private int inclusion;
        private long seed;
        private int loggingLevel;

        private String policyFileName;
        private SecurityManager testSecurityManager;
        private SecurityManager originalSecurityManager;

        private Map<String, List<String>> knownIssues;

        private Properties props;


        private TestParams() {
        }

        static TestParams create() {
            TestParams params = new TestParams();
            Properties props = System.getProperties();
            params.parseProperties(props);
            return params;
        }

        private void parseProperties(Properties props) {
            this.props = props;

            inclusion = getIntProperty(EXHAUSTIVENESS, DEFAULT_EXHAUSTIVENESS, MAX_EXHAUSTIVENESS);
            seed = getLongProperty(SEED, System.currentTimeMillis());
            loggingLevel = getIntProperty(LOGGING_LEVEL, DEFAULT_LOGGING_LEVEL, MAX_LOGGING_LEVEL);

            policyFileName = getProperty(SECURITY_POLICY);
            if (policyFileName != null) {
                String originalPolicyFileName = System.getProperty("java.security.policy");
                originalSecurityManager = System.getSecurityManager();
                System.setProperty("java.security.policy", policyFileName);
                Policy.getPolicy().refresh();
                testSecurityManager = new SecurityManager();
                System.setProperty("java.security.policy", originalPolicyFileName==null ? "" : originalPolicyFileName);
            }
        }

        public String getProperty(String key) {
            String val = null;
            if (key != null && key.length() > 0) {
                val = props.getProperty(key);
            }
            return val;
        }

        public boolean getBooleanProperty(String key) {
            return getBooleanProperty(key, false);
        }

        public boolean getBooleanProperty(String key, boolean defVal) {
            String s = getProperty(key);
            if (s == null) {
                return defVal;
            }
            if (s.equalsIgnoreCase("yes") || s.equalsIgnoreCase("true") || s.equals("1")) {
                return true;
            }
            return false;
        }

        public int getIntProperty(String key, int defVal) {
            return getIntProperty(key, defVal, -1);
        }

        public int getIntProperty(String key, int defVal, int maxVal) {
            String s = getProperty(key);
            if (s == null) {
                return defVal;
            }
            return (maxVal == -1) ? Integer.valueOf(s) : Math.max(Integer.valueOf(s), maxVal);
        }

        public long getLongProperty(String key, long defVal) {
            String s = getProperty(key);
            if (s == null) {
                return defVal;
            }
            return Long.valueOf(s);
        }

        public int getInclusion() {
            return inclusion;
        }

        public long getSeed() {
            return seed;
        }

        public int getLoggingLevel() {
            return loggingLevel;
        }
    }

    /**
     * Check the given array to see that all the strings in the expected array
     * are present.
     *
     * @param msg
     *            string message, for log output
     * @param array
     *            array of strings to check
     * @param expected
     *            array of strings we expect to see, or null
     * @return the length of 'array', or -1 on error
     */
    protected static int checkArray(String msg, String array[], String expected[]) {
        int explen = (expected != null) ? expected.length : 0;
        if (!(explen >= 0 && explen < 31)) { // [sic] 31 not 32
            errln("Internal error");
            return -1;
        }
        int i = 0;
        StringBuffer buf = new StringBuffer();
        int seenMask = 0;
        for (; i < array.length; ++i) {
            String s = array[i];
            if (i != 0)
                buf.append(", ");
            buf.append(s);
            // check expected list
            for (int j = 0, bit = 1; j < explen; ++j, bit <<= 1) {
                if ((seenMask & bit) == 0) {
                    if (s.equals(expected[j])) {
                        seenMask |= bit;
                        logln("Ok: \"" + s + "\" seen");
                    }
                }
            }
        }
        logln(msg + " = [" + buf + "] (" + i + ")");
        // did we see all expected strings?
        if (((1 << explen) - 1) != seenMask) {
            for (int j = 0, bit = 1; j < expected.length; ++j, bit <<= 1) {
                if ((seenMask & bit) == 0) {
                    errln("\"" + expected[j] + "\" not seen");
                }
            }
        }
        return array.length;
    }

    /**
     * Check the given array to see that all the locales in the expected array
     * are present.
     *
     * @param msg
     *            string message, for log output
     * @param array
     *            array of locales to check
     * @param expected
     *            array of locales names we expect to see, or null
     * @return the length of 'array'
     */
    protected static int checkArray(String msg, Locale array[], String expected[]) {
        String strs[] = new String[array.length];
        for (int i = 0; i < array.length; ++i) {
            strs[i] = array[i].toString();
        }
        return checkArray(msg, strs, expected);
    }

    /**
     * Check the given array to see that all the locales in the expected array
     * are present.
     *
     * @param msg
     *            string message, for log output
     * @param array
     *            array of locales to check
     * @param expected
     *            array of locales names we expect to see, or null
     * @return the length of 'array'
     */
    protected static int checkArray(String msg, ULocale array[], String expected[]) {
        String strs[] = new String[array.length];
        for (int i = 0; i < array.length; ++i) {
            strs[i] = array[i].toString();
        }
        return checkArray(msg, strs, expected);
    }

    // JUnit-like assertions.

    protected static boolean assertTrue(String message, boolean condition) {
        return handleAssert(condition, message, "true", null);
    }

    protected static boolean assertFalse(String message, boolean condition) {
        return handleAssert(!condition, message, "false", null);
    }

    protected static boolean assertEquals(String message, boolean expected,
            boolean actual) {
        return handleAssert(expected == actual, message, String
                .valueOf(expected), String.valueOf(actual));
    }

    protected static boolean assertEquals(String message, long expected, long actual) {
        return handleAssert(expected == actual, message, String
                .valueOf(expected), String.valueOf(actual));
    }

    // do NaN and range calculations to precision of float, don't rely on
    // promotion to double
    protected static boolean assertEquals(String message, float expected,
            float actual, double error) {
        boolean result = Float.isInfinite(expected)
                ? expected == actual
                : !(Math.abs(expected - actual) > error); // handles NaN
        return handleAssert(result, message, String.valueOf(expected)
                + (error == 0 ? "" : " (within " + error + ")"), String
                .valueOf(actual));
    }

    protected static boolean assertEquals(String message, double expected,
            double actual, double error) {
        boolean result = Double.isInfinite(expected)
                ? expected == actual
                : !(Math.abs(expected - actual) > error); // handles NaN
        return handleAssert(result, message, String.valueOf(expected)
                + (error == 0 ? "" : " (within " + error + ")"), String
                .valueOf(actual));
    }

    protected static <T> boolean assertEquals(String message, T[] expected, T[] actual) {
        // Use toString on a List to get useful, readable messages
        String expectedString = expected == null ? "null" : Arrays.asList(expected).toString();
        String actualString = actual == null ? "null" : Arrays.asList(actual).toString();
        return assertEquals(message, expectedString, actualString);
    }

    protected static boolean assertEquals(String message, Object expected,
            Object actual) {
        boolean result = expected == null ? actual == null : expected
                .equals(actual);
        return handleAssert(result, message, stringFor(expected),
                stringFor(actual));
    }

    protected static boolean assertNotEquals(String message, Object expected,
            Object actual) {
        boolean result = !(expected == null ? actual == null : expected
                .equals(actual));
        return handleAssert(result, message, stringFor(expected),
                stringFor(actual), "not equal to", true);
    }

    protected boolean assertSame(String message, Object expected, Object actual) {
        return handleAssert(expected == actual, message, stringFor(expected),
                stringFor(actual), "==", false);
    }

    protected static boolean assertNotSame(String message, Object expected,
            Object actual) {
        return handleAssert(expected != actual, message, stringFor(expected),
                stringFor(actual), "!=", true);
    }

    protected static boolean assertNull(String message, Object actual) {
        return handleAssert(actual == null, message, null, stringFor(actual));
    }

    protected static boolean assertNotNull(String message, Object actual) {
        return handleAssert(actual != null, message, null, stringFor(actual),
                "!=", true);
    }

    protected static void fail() {
        fail("");
    }

    protected static void fail(String message) {
        if (message == null) {
            message = "";
        }
        if (!message.equals("")) {
            message = ": " + message;
        }
        errln(sourceLocation() + message);
    }

    private static boolean handleAssert(boolean result, String message,
            String expected, String actual) {
        return handleAssert(result, message, expected, actual, null, false);
    }

    public static boolean handleAssert(boolean result, String message,
            Object expected, Object actual, String relation, boolean flip) {
        if (!result || isVerbose()) {
            if (message == null) {
                message = "";
            }
            if (!message.equals("")) {
                message = ": " + message;
            }
            relation = relation == null ? ", got " : " " + relation + " ";
            if (result) {
                logln("OK " + message + ": "
                        + (flip ? expected + relation + actual : expected));
            } else {
                // assert must assume errors are true errors and not just warnings
                // so cannot warnln here
                errln(  message
                        + ": expected"
                        + (flip ? relation + expected : " " + expected
                                + (actual != null ? relation + actual : "")));
            }
        }
        return result;
    }

    private static final String stringFor(Object obj) {
        if (obj == null) {
            return "null";
        }
        if (obj instanceof String) {
            return "\"" + obj + '"';
        }
        return obj.getClass().getName() + "<" + obj + ">";
    }

    // Return the source code location of the caller located callDepth frames up the stack.
    protected static String sourceLocation() {
        // Walk up the stack to the first call site outside this file
        for (StackTraceElement st : new Throwable().getStackTrace()) {
            String source = st.getFileName();
            if (source != null && !source.equals("TestFmwk.java") && !source.equals("AbstractTestLog.java")) {
                String methodName = st.getMethodName();
                if (methodName != null &&
                       (methodName.startsWith("Test") || methodName.startsWith("test") || methodName.equals("main"))) {
                    return "(" + source + ":" + st.getLineNumber() + ") ";
                }
            }
        }
        throw new InternalError();
    }

    protected static boolean checkDefaultPrivateConstructor(String fullyQualifiedClassName) throws Exception {
        return checkDefaultPrivateConstructor(Class.forName(fullyQualifiedClassName));
    }

    protected static boolean checkDefaultPrivateConstructor(Class<?> classToBeTested) throws Exception {
        Constructor<?> constructor = classToBeTested.getDeclaredConstructor();

        // Check that the constructor is private.
        boolean isPrivate = Modifier.isPrivate(constructor.getModifiers());

        // Call the constructor for coverage.
        constructor.setAccessible(true);
        constructor.newInstance();

        if (!isPrivate) {
            errln("Default private constructor for class: " + classToBeTested.getName() + " is not private.");
        }
        return isPrivate;
    }

    /**
     * Tests the toString method on a private or hard-to-reach class.  Assumes constructor of the class does not
     * take any arguments.
     * @param fullyQualifiedClassName
     * @return The output of the toString method.
     * @throws Exception
     */
    protected static String invokeToString(String fullyQualifiedClassName) throws Exception {
        return invokeToString(fullyQualifiedClassName, new Class<?>[]{}, new Object[]{});
    }

    /**
     * Tests the toString method on a private or hard-to-reach class.  Assumes constructor of the class does not
     * take any arguments.
     * @param classToBeTested
     * @return The output of the toString method.
     * @throws Exception
     */
    protected static String invokeToString(Class<?> classToBeTested) throws Exception {
        return invokeToString(classToBeTested, new Class<?>[]{}, new Object[]{});
    }

    /**
     * Tests the toString method on a private or hard-to-reach class.  Allows you to specify the argument types for
     * the constructor.
     * @param fullyQualifiedClassName
     * @return The output of the toString method.
     * @throws Exception
     */
    protected static String invokeToString(String fullyQualifiedClassName,
            Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception {
        return invokeToString(Class.forName(fullyQualifiedClassName), constructorParamTypes, constructorParams);
    }

    /**
     * Tests the toString method on a private or hard-to-reach class.  Allows you to specify the argument types for
     * the constructor.
     * @param classToBeTested
     * @return The output of the toString method.
     * @throws Exception
     */
    protected static String invokeToString(Class<?> classToBeTested,
            Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception {
        Constructor<?> constructor = classToBeTested.getDeclaredConstructor(constructorParamTypes);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(constructorParams);
        Method toStringMethod = classToBeTested.getDeclaredMethod("toString");
        toStringMethod.setAccessible(true);
        return (String) toStringMethod.invoke(obj);
    }


    // End JUnit-like assertions

    // TODO (sgill): added to keep errors away
    /* (non-Javadoc)
     * @see com.ibm.icu.dev.test.TestLog#msg(java.lang.String, int, boolean, boolean)
     */
    //@Override
    protected static void msg(String message, int level, boolean incCount, boolean newln) {
        if (level == TestLog.WARN || level == TestLog.ERR) {
            Assert.fail(message);
        }
        // TODO(stuartg): turned off - causing OOM running under ant
//        while (level > 0) {
//            System.out.print(" ");
//            level--;
//        }
//        System.out.print(message);
//        if (newln) {
//            System.out.println();
//        }
    }

}
