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

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.Utility;

/**
 * A collection of methods to run the data driven number format test suite.
 */
public class DataDrivenNumberFormatTestUtility extends TestFmwk {

    /**
     * Base class for code under test.
     */
    public static abstract class CodeUnderTest {

        /**
         * Returns the ID of the code under test. This ID is used to identify
         * tests that are known to fail for this particular code under test.
         * This implementation returns null which means that by default all
         * tests should work with this code under test.
         * @return 'J' means ICU4J, 'K' means JDK
         */
        public Character Id() {
            return null;
        }

        /**
         *  Runs a single formatting test. On success, returns null.
         *  On failure, returns the error. This implementation just returns null.
         *  Subclasses should override.
         *  @param tuple contains the parameters of the format test.
         */
        public String format(DataDrivenNumberFormatTestData tuple) {
            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
            return null;
        }

        /**
         *  Runs a single toPattern test. On success, returns null.
         *  On failure, returns the error. This implementation just returns null.
         *  Subclasses should override.
         *  @param tuple contains the parameters of the format test.
         */
        public String toPattern(DataDrivenNumberFormatTestData tuple) {
            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
            return null;
        }

        /**
         *  Runs a single parse test. On success, returns null.
         *  On failure, returns the error. This implementation just returns null.
         *  Subclasses should override.
         *  @param tuple contains the parameters of the format test.
         */
        public String parse(DataDrivenNumberFormatTestData tuple) {
            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
            return null;
        }

        /**
         *  Runs a single parse currency test. On success, returns null.
         *  On failure, returns the error. This implementation just returns null.
         *  Subclasses should override.
         *  @param tuple contains the parameters of the format test.
         */
        public String parseCurrency(DataDrivenNumberFormatTestData tuple) {
            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
            return null;
        }

        /**
         * Runs a single select test. On success, returns null.
         *  On failure, returns the error. This implementation just returns null.
         *  Subclasses should override.
         * @param tuple contains the parameters of the format test.
         */
        public String select(DataDrivenNumberFormatTestData tuple) {
            if (tuple.output != null && tuple.output.equals("fail")) return "fail";
            return null;
        }
    }

    private static enum RunMode {
        SKIP_KNOWN_FAILURES,
        INCLUDE_KNOWN_FAILURES,
        CHECK_FOR_UNKNOWN_IDS
    }

    private final CodeUnderTest codeUnderTest;
    private final String allowedIDs;
    private String fileLine = null;
    private int fileLineNumber = 0;
    private String fileTestName = "";
    private DataDrivenNumberFormatTestData tuple = new DataDrivenNumberFormatTestData();

    /**
     * Runs all the tests in the data driven test suite against codeUnderTest.
     * @param fileName The name of the test file. A relative file name under
     *   com/ibm/icu/dev/data such as "data.txt"
     * @param codeUnderTest the code under test
     */

    public static void runSuite(
            String fileName, CodeUnderTest codeUnderTest) {
        new DataDrivenNumberFormatTestUtility(codeUnderTest)
                .run(fileName, RunMode.SKIP_KNOWN_FAILURES);
    }

    /**
     * Runs every format test in data driven test suite including those
     * that are known to fail.  If a test is supposed to fail but actually
     * passes, an error is printed.
     *
     * @param fileName The name of the test file. A relative file name under
     *   com/ibm/icu/dev/data such as "data.txt"
     * @param codeUnderTest the code under test
     */
    public static void runFormatSuiteIncludingKnownFailures(
            String fileName, CodeUnderTest codeUnderTest) {
        new DataDrivenNumberFormatTestUtility(codeUnderTest)
                .run(fileName, RunMode.INCLUDE_KNOWN_FAILURES);
    }

    /**
     * Checks the data file for no unknown IDs in "breaks" columns.
     */
    public static void checkNoUnknownIDs(String fileName, String allowedIDs) {
        new DataDrivenNumberFormatTestUtility(allowedIDs).run(fileName, RunMode.CHECK_FOR_UNKNOWN_IDS);
    }

    private DataDrivenNumberFormatTestUtility(CodeUnderTest codeUnderTest) {
        this.codeUnderTest = codeUnderTest;
        this.allowedIDs = null;
    }

    private DataDrivenNumberFormatTestUtility(String allowedIDs) {
        this.codeUnderTest = null;
        this.allowedIDs = allowedIDs;
    }

    private void run(String fileName, RunMode runMode) {
        char codeUnderTestId = (codeUnderTest == null) ? 0 : codeUnderTest.Id();
        BufferedReader in = null;
        try {
            in = TestUtil.getDataReader("numberformattestspecification.txt", "UTF-8");
            // read first line and remove BOM if present
            readLine(in);
            if (fileLine != null && fileLine.charAt(0) == '\uFEFF') {
                fileLine = fileLine.substring(1);
            }

            int state = 0;
            List<String> columnValues;
            List<String> columnNames = null;
            while (true) {
                if (fileLine == null || fileLine.length() == 0) {
                    if (!readLine(in)) {
                        break;
                    }
                    if (fileLine.length() == 0 && state == 2) {
                        state = 0;
                    }
                    continue;
                }
                if (fileLine.startsWith("//")) {
                    fileLine = null;
                    continue;
                }
                // Initial setup of test.
                if (state == 0) {
                    if (fileLine.startsWith("test ")) {
                        fileTestName = fileLine;
                        tuple = new DataDrivenNumberFormatTestData();
                    } else if (fileLine.startsWith("set ")) {
                        if (!setTupleField()) {
                            return;
                        }
                    } else if(fileLine.startsWith("begin")) {
                        state = 1;
                    } else {
                        showError("Unrecognized verb.");
                        return;
                    }
                // column specification
                } else if (state == 1) {
                    columnNames = splitBy((char) 0x09);
                    state = 2;
                    // "breaks" column must be last!
                    if (columnNames.contains("breaks")
                            && columnNames.indexOf("breaks") != columnNames.size() - 1) {
                        showError("'breaks' column must be last!");
                    }
                // run the tests
                } else {
                    int columnNamesSize = columnNames.size();
                    columnValues = splitBy(columnNamesSize, (char) 0x09);
                    int columnValuesSize = columnValues.size();
                    for (int i = 0; i < columnValuesSize; ++i) {
                        if (!setField(columnNames.get(i), columnValues.get(i))) {
                            return;
                        }
                    }
                    for (int i = columnValuesSize; i < columnNamesSize; ++i) {
                        if (!clearField(columnNames.get(i))) {
                            return;
                        }
                    }
                    if (runMode == RunMode.CHECK_FOR_UNKNOWN_IDS) {
                        String actualIDs = tuple.breaks;
                        if (actualIDs != null) {
                            // Make sure there are no IDs in actualIDs that are not in allowedIDs.
                            // As a bonus, check that actualIDs are in alphabetical order.
                            char prevID = 0;
                            for (int i=0; i<actualIDs.length(); i++) {
                                char currID = actualIDs.charAt(i);
                                if (allowedIDs.indexOf(currID) == -1) {
                                    showError("Unknown ID: " + currID);
                                }
                                if (prevID > currID) {
                                    showError("IDs not in alphabetical order: " + actualIDs);
                                }
                                prevID = currID;
                            }
                        }
                    } else if (runMode == RunMode.INCLUDE_KNOWN_FAILURES || !breaks(codeUnderTestId)) {
                        String errorMessage;
                        Exception err = null;
                        boolean shouldFail = (tuple.output != null && tuple.output.equals("fail"))
                                ? !breaks(codeUnderTestId)
                                : breaks(codeUnderTestId);
                        try {
                            errorMessage = isPass(tuple);
                        } catch (Exception e) {
                            err = e;
                            errorMessage = "Exception: " + e + ": " + e.getCause();
                        }
                        if (shouldFail && errorMessage == null) {
                            showError("Expected failure, but passed");
                        } else if (!shouldFail && errorMessage != null) {
                            if (err != null) {
                                ByteArrayOutputStream os = new ByteArrayOutputStream();
                                PrintStream ps = new PrintStream(os);
                                err.printStackTrace(ps);
                                String stackTrace = os.toString();
                                showError(errorMessage + "     Stack trace: " + stackTrace.substring(0, 500));
                            } else {
                                showError(errorMessage);
                            }
                        }
                    }
                }
                fileLine = null;
            }
        } catch (Exception e) {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            PrintStream ps = new PrintStream(os);
            e.printStackTrace(ps);
            String stackTrace = os.toString();
            showError("MAJOR ERROR: " + e.toString() + "     Stack trace: " + stackTrace.substring(0,500));
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private boolean breaks(char code) {
       String breaks = tuple.breaks == null ? "" : tuple.breaks;
       return (breaks.toUpperCase().indexOf(code) != -1);
    }

    private static boolean isSpace(char c) {
        return (c == 0x09 || c == 0x20 || c == 0x3000);
    }

    private boolean setTupleField() {
        List<String> parts = splitBy(3, (char) 0x20);
        if (parts.size() < 3) {
            showError("Set expects 2 parameters");
            return false;
        }
        return setField(parts.get(1), parts.get(2));
    }

    private boolean setField(String name, String value) {
        try {
            tuple.setField(name,  Utility.unescape(value));
            return true;
        } catch (Exception e) {
            showError("No such field: " + name + ", or bad value: " + value + ": " + e);
            return false;
        }
    }

    private boolean clearField(String name) {
        try {
            tuple.clearField(name);
            return true;
        } catch (Exception e) {
            showError("Field cannot be clared: " + name);
            return false;
        }
    }

    private void showError(String message) {
        TestFmwk.errln(String.format("line %d: %s\n%s\n%s", fileLineNumber, Utility.escape(message), fileTestName,fileLine));
    }

    private List<String> splitBy(char delimiter) {
        return splitBy(Integer.MAX_VALUE, delimiter);
    }

    private List<String> splitBy(int max, char delimiter) {
        ArrayList<String> result = new ArrayList<String>();
        int colIdx = 0;
        int colStart = 0;
        int len = fileLine.length();
        for (int idx = 0; colIdx < max - 1 && idx < len; ++idx) {
            char ch = fileLine.charAt(idx);
            if (ch == delimiter) {
                result.add(
                        fileLine.substring(colStart, idx));
                ++colIdx;
                colStart = idx + 1;
            }
        }
        result.add(fileLine.substring(colStart, len));
        return result;
    }

    private boolean readLine(BufferedReader in) throws IOException {
        String line = in.readLine();
        if (line == null) {
            fileLine = null;
            return false;
        }
        ++fileLineNumber;
        // Strip trailing comments and spaces
        int idx = line.length();
        for (; idx > 0; idx--) {
            if (!isSpace(line.charAt(idx -1))) {
                break;
            }
        }
        fileLine = idx == 0 ? "" : line;
        return true;
    }

    private String isPass(DataDrivenNumberFormatTestData tuple) {
        StringBuilder result = new StringBuilder();
        if (tuple.format != null && tuple.output != null) {
            String errorMessage = codeUnderTest.format(tuple);
            if (errorMessage != null) {
                result.append(errorMessage);
            }
        } else if (tuple.toPattern != null || tuple.toLocalizedPattern != null) {
            String errorMessage = codeUnderTest.toPattern(tuple);
            if (errorMessage != null) {
                result.append(errorMessage);
            }
        } else if (tuple.parse != null && tuple.output != null && tuple.outputCurrency != null) {
            String errorMessage = codeUnderTest.parseCurrency(tuple);
            if (errorMessage != null) {
                result.append(errorMessage);
            }
        } else if (tuple.parse != null && tuple.output != null) {
            String errorMessage = codeUnderTest.parse(tuple);
            if (errorMessage != null) {
                result.append(errorMessage);
            }
        } else if (tuple.plural != null) {
            String errorMessage = codeUnderTest.select(tuple);
            if (errorMessage != null) {
                result.append(errorMessage);
            }
        } else {
            result.append("Unrecognized test type.");
        }
        if (result.length() > 0) {
            result.append(": ");
            result.append(tuple);
            return result.toString();
        }
        return null;
    }
}
