/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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
 *
 *      https://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.apache.commons.cli.bug;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.ParameterMetaData;
import java.sql.Types;

import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class BugCLI162Test {
    /** Constant for the line separator. */
    private static final String CR = System.lineSeparator();

    // Constants used for options
    private static final String OPT = "-";

    private static final String OPT_COLUMN_NAMES = "l";

    private static final String OPT_CONNECTION = "c";

    private static final String OPT_DESCRIPTION = "e";

    private static final String OPT_DRIVER = "d";

    private static final String OPT_DRIVER_INFO = "n";

    private static final String OPT_FILE_BINDING = "b";

    private static final String OPT_FILE_JDBC = "j";

    private static final String OPT_FILE_SFMD = "f";

    private static final String OPT_HELP = "h";

    private static final String OPT_HELP_LONG = "help";

    private static final String OPT_INTERACTIVE = "i";

    private static final String OPT_JDBC_TO_SFMD = "2";

    private static final String OPT_JDBC_TO_SFMD_L = "jdbc2sfmd";

    private static final String OPT_METADATA = "m";

    private static final String OPT_PARAM_MODES_INT = "o";

    private static final String OPT_PARAM_MODES_NAME = "O";

    private static final String OPT_PARAM_NAMES = "a";

    private static final String OPT_PARAM_TYPES_INT = "y";

    private static final String OPT_PARAM_TYPES_NAME = "Y";

    private static final String OPT_PASSWORD = "p";

    private static final String OPT_PASSWORD_L = "password";

    private static final String OPT_SQL = "s";

    private static final String OPT_SQL_L = "sql";

    private static final String OPT_STACK_TRACE = "t";

    private static final String OPT_TIMING = "g";

    private static final String OPT_TRIM_L = "trim";

    private static final String OPT_USER = "u";

    private static final String OPT_WRITE_TO_FILE = "w";

    private static final String PMODE_IN = "IN";

    private static final String PMODE_INOUT = "INOUT";

    private static final String PMODE_OUT = "OUT";

    private static final String PMODE_UNK = "Unknown";

    private static final String PMODES = PMODE_IN + ", " + PMODE_INOUT + ", " + PMODE_OUT + ", " + PMODE_UNK;

    // @formatter:off
    private static final String EXPECTED = "usage: org.apache.commons.cli.bug.BugCLI162Test" + CR +
            " -2,--jdbc2sfmd <arg>        Converts the JDBC file in the first argument" + CR +
            "                             to an SMFD file specified in the second" + CR +
            "                             argument." + CR +
            " -a,--paramNames <arg>       Parameter XML names; default names are" + CR +
            "                             param1, param2, etc. Example: -a \"pname1" + CR +
            "                             pname2\"" + CR +
            " -b,--jdbc <arg>             Writes a JDBC binding node file for the given" + CR +
            "                             SQL" + CR +
            " -c,--url <arg>              Connection URL" + CR +
            " -d,--driver <arg>           JDBC driver class name" + CR +
            " -e,--description <arg>      SFMD description. A default description is" + CR +
            "                             used if omited. Example: -e \"Runs such and" + CR +
            "                             such\"" + CR +
            " -f,--sfmd <arg>             Writes a SFMD file for the given SQL" + CR +
            " -g,--printTiming            Prints timing information" + CR +
            " -h,--help                   Prints help and quits" + CR +
            " -i,--interactive            Runs in interactive mode, reading and writing" + CR +
            "                             from the console, 'go' or '/' sends a" + CR +
            "                             statement" + CR +
            " -j,--node <arg>             Writes a JDBC node file for the given SQL" + CR +
            "                             (internal debugging)" + CR +
            " -l,--columnNames <arg>      Column XML names; default names column" + CR +
            "                             labels. Example: -l \"cname1 cname2\"" + CR +
            " -m,--printMetaData          Prints metadata information" + CR +
            " -n,--info                   Prints driver information and properties. If" + CR +
            "                             -c is not specified, all drivers on the" + CR +
            "                             classpath are displayed." + CR +
            " -o,--paramModes <arg>       Parameters modes (1=IN, 2=INOUT, 4=OUT," + CR +
            "                             0=Unknown). -o and -O are mutually exclusive." + CR +
            "                             Example for 2 parameters, OUT and IN: -o \"4" + CR +
            "                             1\"" + CR +
            " -O,--paramModeNames <arg>   Parameters mode names (IN, INOUT, OUT," + CR +
            "                             Unknown). -o and -O are mutually exclusive." + CR +
            "                             Example for 2 parameters, OUT and IN: -O \"OUT" + CR +
            "                             IN\"" + CR +
            " -p,--password <arg>         The database password for the user specified" + CR +
            "                             with the -u option. You can obfuscate the" + CR +
            "                             password with" + CR +
            "                             org.mortbay.jetty.security.Password, see" + CR +
            "                             https://docs.codehaus.org/display/JETTY/Secur" + CR +
            "                             ing+Passwords" + CR +
            " -s,--sql <arg>              Runs SQL or {call stored_procedure(?, ?)} or" + CR +
            "                             {?=call function(?, ?)}" + CR +
            " -t,--printStack             Prints stack traces on errors" + CR +
            "    --trim <arg>             Trims leading and trailing spaces from all" + CR +
            "                             column values. Column XML names can be" + CR +
            "                             optionally specified to set which columns to" + CR +
            "                             trim." + CR +
            " -u,--user <arg>             A database user name" + CR +
            " -w,--outfile <arg>          Writes the SQL output to the given file" + CR +
            " -y,--paramTypes <arg>       Parameter types from java.sql.Types. -y and" + CR +
            "                             -Y are mutually exclusive. Example: -y \"-10" + CR +
            "                             12\"" + CR +
            " -Y,--paramTypeNames <arg>   Parameter java.sql.Types names. -y and -Y are" + CR +
            "                             mutually exclusive. Example: -Y \"CURSOR" + CR +
            "                             VARCHAR\"" + CR;
    // @formatter:on

    private HelpFormatter formatter;

    private StringWriter sw;

    @BeforeEach
    public void setUp() {
        formatter = new HelpFormatter();
        sw = new StringWriter();
    }

    @Test
    void testInfiniteLoop() {
        final Options options = new Options();
        options.addOption("h", "help", false, "This is a looooong description");
        // used to hang & crash
        formatter.printHelp(new PrintWriter(sw), 20, "app", null, options, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, null);

        //@formatter:off
        final String expected = "usage: app" + CR +
                " -h,--help   This is" + CR +
                "             a" + CR +
                "             looooon" + CR +
                "             g" + CR +
                "             descrip" + CR +
                "             tion" + CR;
        //@formatter:on
        assertEquals(expected, sw.toString());
    }

    @Test
    void testLongLineChunking() {
        final Options options = new Options();
        //@formatter:off
        options.addOption("x", "extralongarg", false,
                          "This description has ReallyLongValuesThatAreLongerThanTheWidthOfTheColumns " +
                          "and also other ReallyLongValuesThatAreHugerAndBiggerThanTheWidthOfTheColumnsBob, " +
                          "yes. ");
        //@formatter:on
        formatter.printHelp(new PrintWriter(sw), 35, this.getClass().getName(), "Header", options, 0, 5, "Footer");
        //@formatter:off
        final String expected = "usage:" + CR +
                          "       org.apache.commons.cli.bug.B" + CR +
                          "       ugCLI162Test" + CR +
                          "Header" + CR +
                          "-x,--extralongarg     This" + CR +
                          "                      description" + CR +
                          "                      has" + CR +
                          "                      ReallyLongVal" + CR +
                          "                      uesThatAreLon" + CR +
                          "                      gerThanTheWid" + CR +
                          "                      thOfTheColumn" + CR +
                          "                      s and also" + CR +
                          "                      other" + CR +
                          "                      ReallyLongVal" + CR +
                          "                      uesThatAreHug" + CR +
                          "                      erAndBiggerTh" + CR +
                          "                      anTheWidthOfT" + CR +
                          "                      heColumnsBob," + CR +
                          "                      yes." + CR +
                          "Footer" + CR;
        //@formatter:on
        assertEquals(expected, sw.toString(), "Long arguments did not split as expected");
    }

    @Test
    void testLongLineChunkingIndentIgnored() {
        final Options options = new Options();
        options.addOption("x", "extralongarg", false, "This description is Long.");
        formatter.printHelp(new PrintWriter(sw), 22, this.getClass().getName(), "Header", options, 0, 5, "Footer");
        //@formatter:off
        final String expected = "usage:" + CR +
                          "       org.apache.comm" + CR +
                          "       ons.cli.bug.Bug" + CR +
                          "       CLI162Test" + CR +
                          "Header" + CR +
                          "-x,--extralongarg" + CR +
                          " This description is" + CR +
                          " Long." + CR +
                          "Footer" + CR;
        //@formatter:on
        assertEquals(expected, sw.toString(), "Long arguments did not split as expected");
    }

    @Test
    void testPrintHelpLongLines() {
        // Options build
        final Options commandLineOptions;
        commandLineOptions = new Options();
        commandLineOptions.addOption(OPT_HELP, OPT_HELP_LONG, false, "Prints help and quits");
        commandLineOptions.addOption(OPT_DRIVER, "driver", true, "JDBC driver class name");
        //@formatter:off
        commandLineOptions.addOption(OPT_DRIVER_INFO, "info", false, "Prints driver information and properties. If "
            + OPT
            + OPT_CONNECTION
            + " is not specified, all drivers on the classpath are displayed.");
        //@formatter:on
        commandLineOptions.addOption(OPT_CONNECTION, "url", true, "Connection URL");
        commandLineOptions.addOption(OPT_USER, "user", true, "A database user name");
        //@formatter:off
        commandLineOptions
                .addOption(
                        OPT_PASSWORD,
                        OPT_PASSWORD_L,
                        true,
                        "The database password for the user specified with the "
                            + OPT
                            + OPT_USER
                            + " option. You can obfuscate the password with org.mortbay.jetty.security.Password,"
                            + " see https://docs.codehaus.org/display/JETTY/Securing+Passwords");
        //@formatter:on
        commandLineOptions.addOption(OPT_SQL, OPT_SQL_L, true, "Runs SQL or {call stored_procedure(?, ?)} or {?=call function(?, ?)}");
        commandLineOptions.addOption(OPT_FILE_SFMD, "sfmd", true, "Writes a SFMD file for the given SQL");
        commandLineOptions.addOption(OPT_FILE_BINDING, "jdbc", true, "Writes a JDBC binding node file for the given SQL");
        commandLineOptions.addOption(OPT_FILE_JDBC, "node", true, "Writes a JDBC node file for the given SQL (internal debugging)");
        commandLineOptions.addOption(OPT_WRITE_TO_FILE, "outfile", true, "Writes the SQL output to the given file");
        commandLineOptions.addOption(OPT_DESCRIPTION, "description", true,
            "SFMD description. A default description is used if omited. Example: " + OPT + OPT_DESCRIPTION + " \"Runs such and such\"");
        commandLineOptions.addOption(OPT_INTERACTIVE, "interactive", false,
            "Runs in interactive mode, reading and writing from the console, 'go' or '/' sends a statement");
        commandLineOptions.addOption(OPT_TIMING, "printTiming", false, "Prints timing information");
        commandLineOptions.addOption(OPT_METADATA, "printMetaData", false, "Prints metadata information");
        commandLineOptions.addOption(OPT_STACK_TRACE, "printStack", false, "Prints stack traces on errors");
        //@formatter:off
        Option option = new Option(OPT_COLUMN_NAMES, "columnNames", true, "Column XML names; default names column labels. Example: "
            + OPT
            + OPT_COLUMN_NAMES
            + " \"cname1 cname2\"");
        //@formatter:on
        commandLineOptions.addOption(option);
        //@formatter:off
        option = new Option(OPT_PARAM_NAMES, "paramNames", true, "Parameter XML names; default names are param1, param2, etc. Example: "
            + OPT
            + OPT_PARAM_NAMES
            + " \"pname1 pname2\"");
        //@formatter:on
        commandLineOptions.addOption(option);
        //
        final OptionGroup outTypesOptionGroup = new OptionGroup();
        final String outTypesOptionGroupDoc = OPT + OPT_PARAM_TYPES_INT + " and " + OPT + OPT_PARAM_TYPES_NAME + " are mutually exclusive.";
        final String typesClassName = Types.class.getName();
        //@formatter:off
        option = new Option(OPT_PARAM_TYPES_INT, "paramTypes", true, "Parameter types from "
            + typesClassName
            + ". "
            + outTypesOptionGroupDoc
            + " Example: "
            + OPT
            + OPT_PARAM_TYPES_INT
            + " \"-10 12\"");
        //@formatter:on
        commandLineOptions.addOption(option);
        //@formatter:off
        option = new Option(OPT_PARAM_TYPES_NAME, "paramTypeNames", true, "Parameter "
            + typesClassName
            + " names. "
            + outTypesOptionGroupDoc
            + " Example: "
            + OPT
            + OPT_PARAM_TYPES_NAME
            + " \"CURSOR VARCHAR\"");
        //@formatter:on
        commandLineOptions.addOption(option);
        commandLineOptions.addOptionGroup(outTypesOptionGroup);
        //
        final OptionGroup modesOptionGroup = new OptionGroup();
        final String modesOptionGroupDoc = OPT + OPT_PARAM_MODES_INT + " and " + OPT + OPT_PARAM_MODES_NAME + " are mutually exclusive.";
        //@formatter:off
        option = new Option(OPT_PARAM_MODES_INT, "paramModes", true, "Parameters modes ("
            + ParameterMetaData.parameterModeIn
            + "=IN, "
            + ParameterMetaData.parameterModeInOut
            + "=INOUT, "
            + ParameterMetaData.parameterModeOut
            + "=OUT, "
            + ParameterMetaData.parameterModeUnknown
            + "=Unknown"
            + "). "
            + modesOptionGroupDoc
            + " Example for 2 parameters, OUT and IN: "
            + OPT
            + OPT_PARAM_MODES_INT
            + " \""
            + ParameterMetaData.parameterModeOut
            + " "
            + ParameterMetaData.parameterModeIn
            + "\"");
        //@formatter:on
        modesOptionGroup.addOption(option);
        //@formatter:off
        option = new Option(OPT_PARAM_MODES_NAME, "paramModeNames", true, "Parameters mode names ("
            + PMODES
            + "). "
            + modesOptionGroupDoc
            + " Example for 2 parameters, OUT and IN: "
            + OPT
            + OPT_PARAM_MODES_NAME
            + " \""
            + PMODE_OUT
            + " "
            + PMODE_IN
            + "\"");
        //@formatter:on
        modesOptionGroup.addOption(option);
        commandLineOptions.addOptionGroup(modesOptionGroup);
        option = new Option(null, OPT_TRIM_L, true,
            "Trims leading and trailing spaces from all column values. Column XML names can be optionally specified to set which columns to trim.");
        option.setOptionalArg(true);
        commandLineOptions.addOption(option);
        option = new Option(OPT_JDBC_TO_SFMD, OPT_JDBC_TO_SFMD_L, true,
            "Converts the JDBC file in the first argument to an SMFD file specified in the second argument.");
        option.setArgs(2);
        commandLineOptions.addOption(option);

        formatter.printHelp(new PrintWriter(sw), HelpFormatter.DEFAULT_WIDTH, this.getClass().getName(), null, commandLineOptions,
            HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, null);
        //@formatter:on
        assertEquals(EXPECTED, sw.toString());
    }

}
