/*
   Copyright 2017 Remko Popma

   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 picocli;

import org.hamcrest.MatcherAssert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.ProvideSystemProperty;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.contrib.java.lang.system.SystemErrRule;
import org.junit.contrib.java.lang.system.SystemOutRule;
import org.junit.rules.TestRule;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.OptionSpec;
import picocli.CommandLine.Model.PositionalParamSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

import static java.lang.String.format;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;

/**
 * Tests the scripts generated by AutoComplete.
 * @see AutoCompleteSystemExitTest
 */
// http://hayne.net/MacDev/Notes/unixFAQ.html#shellStartup
// https://apple.stackexchange.com/a/13019
public class AutoCompleteTest {

    @Rule
    public final ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false");
    // allows tests to set any kind of properties they like, without having to individually roll them back
    @Rule
    public final TestRule restoreSystemProperties = new RestoreSystemProperties();

    @Rule
    public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests();

    @Rule
    public final SystemOutRule systemOutRule = new SystemOutRule().enableLog().muteForSuccessfulTests();

    public static class BasicExample implements Runnable {
        @Option(names = {"-u", "--timeUnit"}) private TimeUnit timeUnit;
        @Option(names = {"-t", "--timeout"}) private long timeout;
        public void run() {
            System.out.printf("BasicExample was invoked with %d %s.%n", timeout, timeUnit);
        }
        public static void main(String[] args) { new CommandLine(new BasicExample()).execute(args); }
    }
    @Test
    public void basic() throws Exception {
        String script = AutoComplete.bash("basicExample", new CommandLine(new BasicExample()));
        String expected = format(loadTextFromClasspath("/basic.bash"),
                CommandLine.VERSION, concat("\" \"", TimeUnit.values()));
        assertEquals(expected, script);
    }

    public static class TopLevel {
        @Option(names = {"-V", "--version"}, help = true) boolean versionRequested;
        @Option(names = {"-h", "--help"}, help = true) boolean helpRequested;

        @SuppressWarnings("deprecation")
        public static void main(String[] args) {
            CommandLine hierarchy = new CommandLine(new TopLevel())
                    .addSubcommand("sub1", new Sub1())
                    .addSubcommand("sub2", new CommandLine(new Sub2())
                            .addSubcommand("subsub1", new Sub2Child1())
                            .addSubcommand("subsub2", new Sub2Child2())
                    );
            List<CommandLine> commandLines = hierarchy.parse(args);
            //Collections.reverse(commandLines);
            for (CommandLine cmdLine : commandLines) {
                Object command = cmdLine.getCommand();
                System.out.printf("Parsed command %s%n", AutoCompleteTest.toString(command));
            }
        }
    }
    static class Candidates extends ArrayList<String> {
        Candidates() {super(Arrays.asList("aaa", "bbb", "ccc"));}
    }
    @Command(description = "First level subcommand 1", aliases = {"sub1-alias"})
    public static class Sub1 {
        @Option(names = "--num", description = "a number") double number;
        @Option(names = "--str", description = "a String") String str;
        @Option(names = "--candidates", completionCandidates = Candidates.class, description = "with candidates") String[] str2;
    }
    @Command(description = "First level subcommand 2", aliases = {"sub2-alias"})
    public static class Sub2 {
        @Option(names = "--num2", description = "another number") int number2;
        @Option(names = {"--directory", "-d"}, description = "a directory") File[] directory;
        @Parameters(arity = "0..1") Possibilities possibilities;
    }
    @Command(description = "Second level sub-subcommand 1", aliases = {"sub2child1-alias"})
    public static class Sub2Child1 {
        @Option(names = {"-h", "--host"}, description = "a host") List<InetAddress> host;
    }
    @Command(description = "Second level sub-subcommand 2", aliases = {"sub2child2-alias"})
    public static class Sub2Child2 {
        @Option(names = {"-u", "--timeUnit"}) private TimeUnit timeUnit;
        @Option(names = {"-t", "--timeout"}) private long timeout;
        @Parameters(completionCandidates = Candidates.class, description = "with candidates") String str2;
    }

    @Command(description = "Second level sub-subcommand 3", aliases = {"sub2child3-alias"})
    public static class Sub2Child3 {
        @Parameters(index = "1..2") File[] files;
        @Parameters(index = "3..*") List<InetAddress> other;
        @Parameters(completionCandidates = Candidates.class, index = "0") String[] cands;
    }

    // TopLevel
    //    @Option(names = {"-V", "--version"}, help = true) boolean versionRequested;
    //    @Option(names = {"-h", "--help"}, help = true) boolean helpRequested;
    //    Sub1 {
    //      @Option(names = "--num") double number;
    //      @Option(names = "--str") String str;
    //      @Option(names = "--candidates", completionCandidates = Candidates.class) String[] str2;//"aaa", "bbb", "ccc"
    //    }
    //    Sub2 {
    //      @Option(names = "--num2") int number2;
    //      @Option(names = {"--directory", "-d"}) File[] directory;
    //      @Parameters(arity = "0..1") Possibilities possibilities; // Aaa, Bbb, Ccc
    //    -----
    //      Sub2Child1 {
    //        @Option(names = {"-h", "--host"}) List<InetAddress> host;
    //      }
    //      Sub2Child2 {
    //        @Option(names = {"-u", "--timeUnit"}) private TimeUnit timeUnit;
    //        @Option(names = {"-t", "--timeout"}) private long timeout;
    //        @Parameters(completionCandidates = Candidates.class) String str2;//"aaa", "bbb", "ccc"
    //      }
    //      Sub2Child3 {
    //        @Parameters(completionCandidates = Candidates.class, index = "0") String[] cands;//"aaa", "bbb", "ccc"
    //        @Parameters(index = "1..2") File[] files;
    //        @Parameters(index = "3..*") List<InetAddress> other;
    //      }
    //    }
    @Test
    public void nestedSubcommands() throws Exception {
        CommandLine hierarchy = new CommandLine(new TopLevel())
                .addSubcommand("sub1", new Sub1())
                .addSubcommand("sub2", new CommandLine(new Sub2())
                        .addSubcommand("subsub1", new Sub2Child1())
                        .addSubcommand("subsub2", new Sub2Child2())
                        .addSubcommand("subsub3", new Sub2Child3())
                );
        String script = AutoComplete.bash("picocompletion-demo", hierarchy);
        String expected = format(loadTextFromClasspath("/picocompletion-demo_completion.bash"),
                CommandLine.VERSION, concat("\" \"", TimeUnit.values()));
        assertEquals(expected, script);
    }

    @Test
    public void helpCommand() {
        CommandLine hierarchy = new CommandLine(new AutoCompleteTest.TopLevel())
                .addSubcommand("sub1", new AutoCompleteTest.Sub1())
                .addSubcommand("sub2", new CommandLine(new AutoCompleteTest.Sub2())
                        .addSubcommand("subsub1", new AutoCompleteTest.Sub2Child1())
                        .addSubcommand("subsub2", new AutoCompleteTest.Sub2Child2())
                        .addSubcommand("subsub3", new AutoCompleteTest.Sub2Child3())
                )
                .addSubcommand(new CommandLine.HelpCommand());
        String script = AutoComplete.bash("picocompletion-demo-help", hierarchy);
        String expected = format(loadTextFromClasspath("/picocompletion-demo-help_completion.bash"),
                CommandLine.VERSION, concat("\" \"", TimeUnit.values()));
        assertEquals(expected, script);
    }

    private static String concat(String infix, Object[] values) {
        StringBuilder result = new StringBuilder();
        for (Object value : values) {
            result.append(value).append(infix);
        }
        return result.toString().substring(0, result.length() - infix.length());
    }

    static String loadTextFromClasspath(String path) {
        URL url = AutoCompleteTest.class.getResource(path);
        if (url == null) { throw new IllegalArgumentException("Could not find '" + path + "' in classpath."); }
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(url.openStream()));
            StringBuilder result = new StringBuilder(512);
            char[] buff = new char[4096];
            int read = 0;
            do {
                result.append(buff, 0, read);
                read = reader.read(buff);
            } while (read >= 0);
            return result.toString();
        } catch (IOException ex) {
            throw new IllegalStateException("Could not read " + url + " for '" + path + "':", ex);
        } finally {
            if (reader != null) { try { reader.close(); } catch (IOException e) { /* ignore */ } }
        }
    }

    private static String toString(Object obj) {
        StringBuilder sb = new StringBuilder(256);
        Class<?> cls = obj.getClass();
        sb.append(cls.getSimpleName()).append("[");
        String sep = "";
        for (Field f : cls.getDeclaredFields()) {
            f.setAccessible(true);
            sb.append(sep).append(f.getName()).append("=");
            try { sb.append(f.get(obj)); } catch (Exception ex) { sb.append(ex); }
            sep = ", ";
        }
        return sb.append("]").toString();
    }

    public static class NonDefaultCommand {
        @Option(names = {"-t", "--timeout"}) private long timeout;
        public NonDefaultCommand(int i) {}
    }
    public static class MyFactory implements CommandLine.IFactory {
        @SuppressWarnings("unchecked")
        public <K> K create(Class<K> cls) {
            return (K) new NonDefaultCommand(123);
        }
    }

    @Test
    public void testBashRejectsNullScript() {
        try {
            AutoComplete.bash(null, new CommandLine(new TopLevel()));
            fail("Expected NPE");
        } catch (NullPointerException ok) {
            assertEquals("scriptName", ok.getMessage());
        }
    }

    @Test
    public void testBashRejectsNullCommandLine() {
        try {
            AutoComplete.bash("script", null);
            fail("Expected NPE");
        } catch (NullPointerException ok) {
            assertEquals("commandLine", ok.getMessage());
        }
    }

    @Test
    public void testBashAcceptsNullCommand() throws Exception {
        File temp = File.createTempFile("abc", "b");
        temp.deleteOnExit();
        AutoComplete.bash("script", temp, null, new CommandLine(new TopLevel()));
        assertTrue(temp.length() > 0);
    }

    @Test
    public void testBashRejectsNullOut() throws Exception {
        File commandFile = File.createTempFile("abc", "b");
        commandFile.deleteOnExit();
        try {
            AutoComplete.bash("script", null, commandFile,  new CommandLine(new TopLevel()));
            fail("Expected NPE");
        } catch (NullPointerException ok) {
            assertEquals(null, ok.getMessage());
        }
    }

    @Test
    public void testComplete() {
        CommandLine hierarchy = new CommandLine(new TopLevel())
                .addSubcommand("sub1", new Sub1())
                .addSubcommand("sub2", new CommandLine(new Sub2())
                        .addSubcommand("subsub1", new Sub2Child1())
                        .addSubcommand("subsub2", new Sub2Child2())
                );

        CommandSpec spec = hierarchy.getCommandSpec();
        spec.parser().collectErrors(true);
        int cur = 500;

        test(spec, a(),                                       0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub1-alias", "sub2", "sub2-alias"));
        test(spec, a("-"),                                    0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub1-alias", "sub2", "sub2-alias"));
        test(spec, a("-"),                                    0, 1, cur, l("-help", "-version", "V", "h"));
        test(spec, a("-h"),                                   0, 1, cur, l("-help", "-version", "V", "h"));
        test(spec, a("-h"),                                   0, 2, cur, l(""));
        test(spec, a("s"),                                    0, 1, cur, l("ub1", "ub1-alias", "ub2", "ub2-alias"));
        test(spec, a("sub1"),                                 0, 0, cur, l("--help", "--version", "-V", "-h", "sub1", "sub1-alias", "sub2", "sub2-alias"));
        test(spec, a("sub1"),                                 1, 0, cur, l("--candidates", "--num", "--str"));
        test(spec, a("sub1", "-"),                            1, 0, cur, l("--candidates", "--num", "--str"));
        test(spec, a("sub1", "-"),                            1, 1, cur, l("-candidates", "-num", "-str"));
        test(spec, a("sub1", "--"),                           1, 1, cur, l("-candidates", "-num", "-str"));
        test(spec, a("sub1", "--"),                           1, 2, cur, l("candidates", "num", "str"));
        test(spec, a("sub1", "--c"),                          1, 2, cur, l("candidates", "num", "str"));
        test(spec, a("sub1", "--c"),                          1, 3, cur, l("andidates"));
        test(spec, a("sub1", "--candidates"),                 2, 0, cur, l("aaa", "bbb", "ccc"));
        test(spec, a("sub1", "--candidates"),                 1, 12, cur, l(""));
        test(spec, a("sub1", "--candidates="),                1, 11, cur, l("s")); // cursor before 's'
        test(spec, a("sub1", "--candidates="),                1, 12, cur, l("=aaa", "=bbb", "=ccc"));
        test(spec, a("sub1", "--candidates="),                1, 13, cur, l("aaa", "bbb", "ccc"));
        test(spec, a("sub1", "--candidates=a"),               1, 13, cur, l("aaa", "bbb", "ccc"));
        test(spec, a("sub1", "--candidates=a"),               1, 14, cur, l("aa"));
        test(spec, a("sub1", "--candidates", "a"),            2, 1, cur, l("aa"));
        test(spec, a("sub1", "--candidates", "a"),            3, 0, cur, l("--candidates", "--num", "--str"));
        test(spec, a("sub1", "--candidates", "a", "-"),       3, 1, cur, l("-candidates", "-num", "-str"));
        test(spec, a("sub1", "--candidates", "a", "--"),      3, 2, cur, l("candidates", "num", "str"));
        test(spec, a("sub1", "--num"),                        2, 0, cur, l());
        test(spec, a("sub1", "--str"),                        2, 0, cur, l());
        test(spec, a("sub2"),                                 1, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
        test(spec, a("sub2", "-"),                            1, 1, cur, l("-directory", "-num2", "d"));
        test(spec, a("sub2", "-d"),                           2, 0, cur, l());
        test(spec, a("sub2", "-d", "/"),                      3, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
        test(spec, a("sub2", "-d", "/", "-"),                 3, 1, cur, l("-directory", "-num2", "d"));
        test(spec, a("sub2", "-d", "/", "--"),                3, 2, cur, l("directory", "num2"));
        test(spec, a("sub2", "-d", "/", "--n"),               3, 3, cur, l("um2"));
        test(spec, a("sub2", "-d", "/", "--num2"),            3, 6, cur, l(""));
        test(spec, a("sub2", "-d", "/", "--num2"),            4, 0, cur, l());
        test(spec, a("sub2", "-d", "/", "--num2", "0"),       4, 1, cur, l());
        test(spec, a("sub2", "-d", "/", "--num2", "0"),       5, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
        test(spec, a("sub2", "-d", "/", "--num2", "0", "s"),  5, 1, cur, l("ub2child1-alias", "ub2child2-alias", "ubsub1", "ubsub2"));
        test(spec, a("sub2", "A"),                            1, 1, cur, l("aa"));
        test(spec, a("sub2", "Aaa"),                          1, 3, cur, l(""));
        test(spec, a("sub2", "Aaa"),                          2, 0, cur, l("--directory", "--num2", "-d", "Aaa", "Bbb", "Ccc", "sub2child1-alias", "sub2child2-alias", "subsub1", "subsub2"));
        test(spec, a("sub2", "Aaa", "s"),                     2, 1, cur, l("ub2child1-alias", "ub2child2-alias", "ubsub1", "ubsub2"));
        test(spec, a("sub2", "Aaa", "subsub1"),               3, 0, cur, l("--host", "-h"));
        test(spec, a("sub2", "subsub1"),                      2, 0, cur, l("--host", "-h"));
        test(spec, a("sub2", "subsub2"),                      2, 0, cur, l("--timeUnit", "--timeout", "-t", "-u", "aaa", "bbb", "ccc"));
        test(spec, a("sub2", "subsub2", "-"),                 2, 1, cur, l("-timeUnit", "-timeout", "t", "u"));
        test(spec, a("sub2", "subsub2", "-t"),                2, 2, cur, l(""));
        test(spec, a("sub2", "subsub2", "-t"),                3, 0, cur, l());
        test(spec, a("sub2", "subsub2", "-t", "0"),           3, 1, cur, l());
        test(spec, a("sub2", "subsub2", "-t", "0"),           4, 0, cur, l("--timeUnit", "--timeout", "-t", "-u", "aaa", "bbb", "ccc"));
        test(spec, a("sub2", "subsub2", "-t", "0", "-"),      4, 1, cur, l("-timeUnit", "-timeout", "t", "u"));
        test(spec, a("sub2", "subsub2", "-t", "0", "--"),     4, 2, cur, l("timeUnit", "timeout"));
        test(spec, a("sub2", "subsub2", "-t", "0", "--t"),    4, 3, cur, l("imeUnit", "imeout"));
        test(spec, a("sub2", "subsub2", "-t", "0", "-u"),     4, 2, cur, l(""));
        test(spec, a("sub2", "subsub2", "-t", "0", "-u"),     5, 0, cur, timeUnitValues());
        test(spec, a("sub2", "subsub2", "-t", "0", "-u", "S"),5, 1, cur, l("ECONDS"));
        test(spec, a("sub2", "subsub2", "a"),                 2, 1, cur, l("aa"));
        test(spec, a("sub2", "subsub2", "a"),                 3, 0, cur, l("--timeUnit", "--timeout", "-t", "-u", "aaa", "bbb", "ccc"));
    }

    private static void test(CommandSpec spec, String[] args, int argIndex, int positionInArg, int cursor, List<CharSequence> expected) {
        List<CharSequence> actual = new ArrayList<CharSequence>();
        AutoComplete.complete(spec, args, argIndex, positionInArg, cursor, actual);
        Collections.sort(actual, new CharSequenceSort());
        Collections.sort(expected, new CharSequenceSort());
        assertEquals(expected, actual);
    }

    private static String[] a(String... args) {
        return args;
    }

    private static List<CharSequence> l(CharSequence... args) {
        return Arrays.asList(args);
    }

    private static List<CharSequence> timeUnitValues() {
        List<CharSequence> result = new ArrayList<CharSequence>();
        for (TimeUnit tu : TimeUnit.values()) { result.add(tu.toString()); }
        return result;
    }

    static class CharSequenceSort implements Comparator<CharSequence> {
        public int compare(CharSequence left, CharSequence right) {
            return left.toString().compareTo(right.toString());
        }
    }

    @Test(expected = NullPointerException.class)
    public void testCompleteDisallowsNullSpec() {
        AutoComplete.complete(null, new String[] {"-x"}, 0, 0, 0, new ArrayList<CharSequence>());
    }

    @Test(expected = NullPointerException.class)
    public void testCompleteDisallowsNullArgs() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), null, 0, 0, 0, new ArrayList<CharSequence>());
    }

    @Test(expected = NullPointerException.class)
    public void testCompleteDisallowsNullCandidates() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, 0, 0, null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCompleteDisallowsNegativeArgIndex() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, -1, 0, 0, new ArrayList<CharSequence>());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCompleteDisallowsTooLargeArgIndex() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 2, 0, 0, new ArrayList<CharSequence>());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCompleteDisallowsNegativePositionInArg() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, -1, 0, new ArrayList<CharSequence>());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCompleteDisallowsTooLargePositionInArg() {
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, 3, 0, new ArrayList<CharSequence>());
    }

    @Test
    public void testCompleteAllowsNormalValues() {
        List<CharSequence> candidates = new ArrayList<CharSequence>();
        AutoComplete.complete(CommandSpec.create().addOption(OptionSpec.builder("-x").build()), new String[] {"-x"}, 0, 0, 0, candidates);
        assertFalse(candidates.isEmpty());
    }

    enum Possibilities { Aaa, Bbb, Ccc };

    @Test
    public void testCompleteFindCompletionStartPoint() {
        class App {
            @Option(names = "-x", arity = "2") List<Possibilities> poss;
        }
        CommandSpec spec = CommandSpec.forAnnotatedObject(new App());
        int cur = 500;
        test(spec, a("-x"),               1, 0, cur, l("Aaa", "Bbb", "Ccc"));
        test(spec, a("-x", "A"),          1, 0, cur, l("Aaa", "Bbb", "Ccc")); // suggest 1st arg of same type
        test(spec, a("-x", "A"),          1, 1, cur, l("aa"));
        test(spec, a("-x", "Aaa"),        2, 0, cur, l("Aaa", "Bbb", "Ccc")); // suggest 2nd arg of same type
        test(spec, a("-x", "Aaa", "Bbb"), 3, 0, cur, l("-x")); // we have 2 args for first -x. Suggest -x again.
    }

    @Test
    public void testCompleteFindPositionalForTopLevelCommand() {
        class App {
            @Parameters() List<Possibilities> poss;
        }
        CommandSpec spec = CommandSpec.forAnnotatedObject(new App());
        int cur = 500;
        test(spec, a(),                    0, 0, cur, l("Aaa", "Bbb", "Ccc"));
        test(spec, a("A"),          0, 0, cur, l("Aaa", "Bbb", "Ccc"));
        test(spec, a("A"),          0, 1, cur, l("aa"));
        test(spec, a("Aaa"),        1, 0, cur, l("Aaa", "Bbb", "Ccc"));
        test(spec, a("Aaa", "Bbb"), 2, 0, cur, l("Aaa", "Bbb", "Ccc"));
    }

    @Test
    public void testBashify() {
        CommandSpec cmd = CommandSpec.create().addOption(
                OptionSpec.builder("-x")
                        .type(String.class)
                        .paramLabel("_A\tB C")
                        .completionCandidates(Arrays.asList("1")).build());
        String actual = AutoComplete.bash("bashify", new CommandLine(cmd));
        String expected = format(loadTextFromClasspath("/bashify_completion.bash"), CommandLine.VERSION);
        assertEquals(expected, actual);
    }

    @Test
    public void testBashifyWithExtras() {
        CommandSpec cmd = CommandSpec.create().addOption(
                OptionSpec.builder("-x")
                        .type(String.class)
                        .paramLabel("_A\tB C")
                        .completionCandidates(Arrays.asList("1")).build());
        String actual = AutoComplete.bash("./bashify.sh", new CommandLine(cmd));
        String expected = format(loadTextFromClasspath("/bashify_completion.bash"), CommandLine.VERSION);
        assertEquals(expected, actual);
    }

    @Test
    public void testBooleanArgFilter() {
        @Command(name = "booltest")
        class App {
            @Option(names = "-b") boolean primitive;
            @Option(names = "-B") Boolean object;
        }
        String actual = AutoComplete.bash("booltest", new CommandLine(new App()));
        MatcherAssert.assertThat(actual, containsString("local flag_opts=\"-b -B\""));
    }

    @Test
    public void testIsPicocliModelObject() throws Exception {
        Method m = AutoComplete.class.getDeclaredMethod("isPicocliModelObject", Object.class);
        m.setAccessible(true);
        assertFalse((Boolean) m.invoke(null, "blah"));
        assertTrue((Boolean) m.invoke(null, CommandSpec.create()));
        assertTrue((Boolean) m.invoke(null, OptionSpec.builder("-x").build()));
        assertTrue((Boolean) m.invoke(null, PositionalParamSpec.builder().build()));
    }

    @Test
    public void testAddCandidatesForArgsFollowingObject() throws Exception {
        Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", Object.class, List.class);
        m.setAccessible(true);
        List<String> candidates = new ArrayList<String>();
        m.invoke(null, null, candidates);
        assertTrue("null Object adds no candidates", candidates.isEmpty());

        m.invoke(null, new Object(), candidates);
        assertTrue("non-PicocliModelObject Object adds no candidates", candidates.isEmpty());

        List<String> completions = Arrays.asList("x", "y", "z");
        PositionalParamSpec positional = PositionalParamSpec.builder().completionCandidates(completions).build();
        m.invoke(null, positional, candidates);
        assertEquals("PositionalParamSpec adds completion candidates", completions, candidates);
    }

    @Test
    public void testAddCandidatesForArgsFollowingNullCommandAddsNoCandidates() throws Exception {
        Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", CommandSpec.class, List.class);
        m.setAccessible(true);
        List<String> candidates = new ArrayList<String>();
        m.invoke(null, null, candidates);
        assertTrue("null CommandSpec adds no candidates", candidates.isEmpty());
    }

    @Test
    public void testAddCandidatesForArgsFollowingNullOptionAddsNoCandidates() throws Exception {
        Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", OptionSpec.class, List.class);
        m.setAccessible(true);
        List<String> candidates = new ArrayList<String>();
        m.invoke(null, null, candidates);
        assertTrue("null OptionSpec adds no candidates", candidates.isEmpty());
    }

    @Test
    public void testAddCandidatesForArgsFollowingNullPositionalParamAddsNoCandidates() throws Exception {
        Method m = AutoComplete.class.getDeclaredMethod("addCandidatesForArgsFollowing", PositionalParamSpec.class, List.class);
        m.setAccessible(true);
        List<String> candidates = new ArrayList<String>();
        m.invoke(null, null, candidates);
        assertTrue("null PositionalParamSpec adds no candidates", candidates.isEmpty());
    }

    @Command(name = "myapp", mixinStandardHelpOptions = true,
            subcommands = AutoComplete.GenerateCompletion.class)
    static class MyApp implements Runnable {

        @Parameters(index = "0", description = "Required positional param")
        String value;

        public void run() { }
    }

    @Test
    public void testGenerateCompletionParentUsageMessage() {
        CommandLine cmd = new CommandLine(new MyApp());
        String expected = String.format("" +
                "Usage: myapp [-hV] <value> [COMMAND]%n" +
                "      <value>     Required positional param%n" +
                "  -h, --help      Show this help message and exit.%n" +
                "  -V, --version   Print version information and exit.%n" +
                "Commands:%n" +
                "  generate-completion  Generate bash/zsh completion script for myapp.%n");
        assertEquals(expected, cmd.getUsageMessage(CommandLine.Help.Ansi.OFF));
    }

    @Test
    public void testGenerateCompletionCanBeHiddenFromParentUsageMessage() {
        CommandLine cmd = new CommandLine(new MyApp());
        CommandLine gen = cmd.getSubcommands().get("generate-completion");
        gen.getCommandSpec().usageMessage().hidden(true);
        String expected = String.format("" +
                "Usage: myapp [-hV] <value> [COMMAND]%n" +
                "      <value>     Required positional param%n" +
                "  -h, --help      Show this help message and exit.%n" +
                "  -V, --version   Print version information and exit.%n");
        assertEquals(expected, cmd.getUsageMessage(CommandLine.Help.Ansi.OFF));
    }

    @Test
    public void testGenerateCompletionUsageMessage() {
        CommandLine cmd = new CommandLine(new MyApp());
        String expected = String.format("" +
                "Usage: myapp generate-completion [-hV]%n" +
                "Generate bash/zsh completion script for myapp.%n" +
                "Run the following command to give `myapp` TAB completion in the current shell:%n" +
                "%n" +
                "  source <(myapp generate-completion)%n" +
                "%n" +
                "Options:%n" +
                "  -h, --help      Show this help message and exit.%n" +
                "  -V, --version   Print version information and exit.%n");
        CommandLine gen = cmd.getSubcommands().get("generate-completion");
        assertEquals(expected, gen.getUsageMessage(CommandLine.Help.Ansi.OFF));
    }

    @Test
    public void testGenerateCompletionScriptCustomOut() {
        CommandLine cmd = new CommandLine(new MyApp());
        StringWriter sw = new StringWriter();
        cmd.setOut(new PrintWriter(sw));
        String expected = getCompletionScriptText("myapp");
        cmd.execute("generate-completion");
        assertEquals(expected, sw.toString());
    }

    @Test
    public void testGenerateCompletionScriptStandardOut() {
        int exitCode = new CommandLine(new MyApp()).execute("generate-completion");
        assertEquals(CommandLine.ExitCode.OK, exitCode);
        assertEquals("", systemErrRule.getLog());
        assertEquals(getCompletionScriptText("myapp"), systemOutRule.getLog());
    }

    private String getCompletionScriptText(String cmdName) {
        return String.format("" +
                    "#!/usr/bin/env bash\n" +
                    "#\n" +
                    "# %1$s Bash Completion\n" +
                    "# =======================\n" +
                    "#\n" +
                    "# Bash completion support for the `%1$s` command,\n" +
                    "# generated by [picocli](https://picocli.info/) version %2$s.\n" +
                    "#\n" +
                    "# Installation\n" +
                    "# ------------\n" +
                    "#\n" +
                    "# 1. Source all completion scripts in your .bash_profile\n" +
                    "#\n" +
                    "#   cd $YOUR_APP_HOME/bin\n" +
                    "#   for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" +
                    "#\n" +
                    "# 2. Open a new bash console, and type `%1$s [TAB][TAB]`\n" +
                    "#\n" +
                    "# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" +
                    "#     Place this file in a `bash-completion.d` folder:\n" +
                    "#\n" +
                    "#   * /etc/bash-completion.d\n" +
                    "#   * /usr/local/etc/bash-completion.d\n" +
                    "#   * ~/bash-completion.d\n" +
                    "#\n" +
                    "# Documentation\n" +
                    "# -------------\n" +
                    "# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" +
                    "# '%1$s (..)'. By reading entered command line parameters,\n" +
                    "# it determines possible bash completions and writes them to the COMPREPLY variable.\n" +
                    "# Bash then completes the user input if only one entry is listed in the variable or\n" +
                    "# shows the options if more than one is listed in COMPREPLY.\n" +
                    "#\n" +
                    "# References\n" +
                    "# ----------\n" +
                    "# [1] http://stackoverflow.com/a/12495480/1440785\n" +
                    "# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" +
                    "# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" +
                    "# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" +
                    "# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" +
                    "# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" +
                    "# [7] https://stackoverflow.com/questions/3249432/can-a-bash-tab-completion-script-be-used-in-zsh/27853970#27853970\n" +
                    "#\n" +
                    "\n" +
                    "if [ -n \"$BASH_VERSION\" ]; then\n" +
                    "  # Enable programmable completion facilities when using bash (see [3])\n" +
                    "  shopt -s progcomp\n" +
                    "elif [ -n \"$ZSH_VERSION\" ]; then\n" +
                    "  # Make alias a distinct command for completion purposes when using zsh (see [4])\n" +
                    "  setopt COMPLETE_ALIASES\n" +
                    "  alias compopt=complete\n" +
                    "\n" +
                    "  # Enable bash completion in zsh (see [7])\n" +
                    "  # Only initialize completions module once to avoid unregistering existing completions.\n" +
                    "  if ! type compdef > /dev/null; then\n" +
                    "    autoload -U +X compinit && compinit\n" +
                    "  fi\n" +
                    "  autoload -U +X bashcompinit && bashcompinit\n" +
                    "fi\n" +
                    "\n" +
                    "# CompWordsContainsArray takes an array and then checks\n" +
                    "# if all elements of this array are in the global COMP_WORDS array.\n" +
                    "#\n" +
                    "# Returns zero (no error) if all elements of the array are in the COMP_WORDS array,\n" +
                    "# otherwise returns 1 (error).\n" +
                    "function CompWordsContainsArray() {\n" +
                    "  declare -a localArray\n" +
                    "  localArray=(\"$@\")\n" +
                    "  local findme\n" +
                    "  for findme in \"${localArray[@]}\"; do\n" +
                    "    if ElementNotInCompWords \"$findme\"; then return 1; fi\n" +
                    "  done\n" +
                    "  return 0\n" +
                    "}\n" +
                    "function ElementNotInCompWords() {\n" +
                    "  local findme=\"$1\"\n" +
                    "  local element\n" +
                    "  for element in \"${COMP_WORDS[@]}\"; do\n" +
                    "    if [[ \"$findme\" = \"$element\" ]]; then return 1; fi\n" +
                    "  done\n" +
                    "  return 0\n" +
                    "}\n" +
                    "\n" +
                    "# The `currentPositionalIndex` function calculates the index of the current positional parameter.\n" +
                    "#\n" +
                    "# currentPositionalIndex takes three parameters:\n" +
                    "# the command name,\n" +
                    "# a space-separated string with the names of options that take a parameter, and\n" +
                    "# a space-separated string with the names of boolean options (that don't take any params).\n" +
                    "# When done, this function echos the current positional index to std_out.\n" +
                    "#\n" +
                    "# Example usage:\n" +
                    "# local currIndex=$(currentPositionalIndex \"mysubcommand\" \"$ARG_OPTS\" \"$FLAG_OPTS\")\n" +
                    "function currentPositionalIndex() {\n" +
                    "  local commandName=\"$1\"\n" +
                    "  local optionsWithArgs=\"$2\"\n" +
                    "  local booleanOptions=\"$3\"\n" +
                    "  local previousWord\n" +
                    "  local result=0\n" +
                    "\n" +
                    "  for i in $(seq $((COMP_CWORD - 1)) -1 0); do\n" +
                    "    previousWord=${COMP_WORDS[i]}\n" +
                    "    if [ \"${previousWord}\" = \"$commandName\" ]; then\n" +
                    "      break\n" +
                    "    fi\n" +
                    "    if [[ \"${optionsWithArgs}\" =~ ${previousWord} ]]; then\n" +
                    "      ((result-=2)) # Arg option and its value not counted as positional param\n" +
                    "    elif [[ \"${booleanOptions}\" =~ ${previousWord} ]]; then\n" +
                    "      ((result-=1)) # Flag option itself not counted as positional param\n" +
                    "    fi\n" +
                    "    ((result++))\n" +
                    "  done\n" +
                    "  echo \"$result\"\n" +
                    "}\n" +
                    "\n" +
                    "# compReplyArray generates a list of completion suggestions based on an array, ensuring all values are properly escaped.\n" +
                    "#\n" +
                    "# compReplyArray takes a single parameter: the array of options to be displayed\n" +
                    "#\n" +
                    "# The output is echoed to std_out, one option per line.\n" +
                    "#\n" +
                    "# Example usage:\n" +
                    "# local options=(\"foo\", \"bar\", \"baz\")\n" +
                    "# local IFS=$'\\n'\n" +
                    "# COMPREPLY=($(compReplyArray \"${options[@]}\"))\n" +
                    "function compReplyArray() {\n" +
                    "  declare -a options\n" +
                    "  options=(\"$@\")\n" +
                    "  local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
                    "  local i\n" +
                    "  local quoted\n" +
                    "  local optionList=()\n" +
                    "\n" +
                    "  for (( i=0; i<${#options[@]}; i++ )); do\n" +
                    "    # Double escape, since we want escaped values, but compgen -W expands the argument\n" +
                    "    printf -v quoted %%q \"${options[i]}\"\n" +
                    "    quoted=\\'${quoted//\\'/\\'\\\\\\'\\'}\\'\n" +
                    "\n" +
                    "    optionList[i]=$quoted\n" +
                    "  done\n" +
                    "\n" +
                    "  # We also have to add another round of escaping to $curr_word.\n" +
                    "  curr_word=${curr_word//\\\\/\\\\\\\\}\n" +
                    "  curr_word=${curr_word//\\'/\\\\\\'}\n" +
                    "\n" +
                    "  # Actually generate completions.\n" +
                    "  local IFS=$'\\n'\n" +
                    "  echo -e \"$(compgen -W \"${optionList[*]}\" -- \"$curr_word\")\"\n" +
                    "}\n" +
                    "\n" +
                    "# Bash completion entry point function.\n" +
                    "# _complete_%1$s finds which commands and subcommands have been specified\n" +
                    "# on the command line and delegates to the appropriate function\n" +
                    "# to generate possible options and subcommands for the last specified subcommand.\n" +
                    "function _complete_%1$s() {\n" +
                    "  # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
                    "  if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} generate-completion\" ];    then _picocli_myapp; return $?; fi\n" +
                    "\n" +
                    "  # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
                    "  local cmds0=(generate-completion)\n" +
                    "\n" +
                    "  if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_myapp_generatecompletion; return $?; fi\n" +
                    "\n" +
                    "  # No subcommands were specified; generate completions for the top-level command.\n" +
                    "  _picocli_%1$s; return $?;\n" +
                    "}\n" +
                    "\n" +
                    "# Generates completions for the options and subcommands of the `%1$s` command.\n" +
                    "function _picocli_%1$s() {\n" +
                    "  # Get completion data\n" +
                    "  local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
                    "\n" +
                    "  local commands=\"generate-completion\"\n" +
                    "  local flag_opts=\"-h --help -V --version\"\n" +
                    "  local arg_opts=\"\"\n" +
                    "\n" +
                    "  if [[ \"${curr_word}\" == -* ]]; then\n" +
                    "    COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" +
                    "  else\n" +
                    "    local positionals=\"\"\n" +
                    "    local IFS=$'\\n'\n" +
                    "    COMPREPLY=( $(compgen -W \"${commands// /$'\\n'}${IFS}${positionals}\" -- \"${curr_word}\") )\n" +
                    "  fi\n" +
                    "}\n" +
                    "\n" +
                    "# Generates completions for the options and subcommands of the `generate-completion` subcommand.\n" +
                    "function _picocli_%1$s_generatecompletion() {\n" +
                    "  # Get completion data\n" +
                    "  local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
                    "\n" +
                    "  local commands=\"\"\n" +
                    "  local flag_opts=\"-h --help -V --version\"\n" +
                    "  local arg_opts=\"\"\n" +
                    "\n" +
                    "  if [[ \"${curr_word}\" == -* ]]; then\n" +
                    "    COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" +
                    "  else\n" +
                    "    local positionals=\"\"\n" +
                    "    local IFS=$'\\n'\n" +
                    "    COMPREPLY=( $(compgen -W \"${commands// /$'\\n'}${IFS}${positionals}\" -- \"${curr_word}\") )\n" +
                    "  fi\n" +
                    "}\n" +
                    "\n" +
                    "# Define a completion specification (a compspec) for the\n" +
                    "# `%1$s`, `%1$s.sh`, and `%1$s.bash` commands.\n" +
                    "# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" +
                    "# `_complete_%1$s` is responsible for generating possible completions for the\n" +
                    "# current word on the command line.\n" +
                    "# The `-o default` option means that if the function generated no matches, the\n" +
                    "# default Bash completions and the Readline default filename completions are performed.\n" +
                    "complete -F _complete_%1$s -o default %1$s %1$s.sh %1$s.bash\n" +
                    "\n", cmdName, CommandLine.VERSION);
    }

    //https://github.com/remkop/picocli/issues/887
    @Test
    public void testHiddenOptionsAndSubcommandsNotSuggested() {

        @Command(name="CompletionDemo", subcommands = { picocli.AutoComplete.GenerateCompletion.class, CommandLine.HelpCommand.class } )
        class CompletionSubcommandDemo implements Runnable {
            @Option(names = "--aaa", hidden = true) int a;
            @Option(names = "--apples", hidden = false) int apples;
            @Option(names = "--bbb", hidden = false) int b;

            public void run() { }
        }
        CommandLine cmd = new CommandLine(new CompletionSubcommandDemo());
        CommandLine gen = cmd.getSubcommands().get("generate-completion");
        gen.getCommandSpec().usageMessage().hidden(true);

        String expectedUsage = String.format("" +
                "Usage: CompletionDemo [--apples=<apples>] [--bbb=<b>] [COMMAND]%n" +
                "      --apples=<apples>%n" +
                "      --bbb=<b>%n" +
                "Commands:%n" +
                "  help  Display help information about the specified command.%n");
        assertEquals(expectedUsage, cmd.getUsageMessage(CommandLine.Help.Ansi.OFF));

        StringWriter sw = new StringWriter();
        cmd.setOut(new PrintWriter(sw));
        String expected = getCompletionScriptTextWithHidden("CompletionDemo");
        cmd.execute("generate-completion");
        assertEquals(expected, sw.toString());
    }

    private String getCompletionScriptTextWithHidden(String commandName) {
        return String.format("" +
                "#!/usr/bin/env bash\n" +
                "#\n" +
                "# %1$s Bash Completion\n" +
                "# =======================\n" +
                "#\n" +
                "# Bash completion support for the `%1$s` command,\n" +
                "# generated by [picocli](https://picocli.info/) version %2$s.\n" +
                "#\n" +
                "# Installation\n" +
                "# ------------\n" +
                "#\n" +
                "# 1. Source all completion scripts in your .bash_profile\n" +
                "#\n" +
                "#   cd $YOUR_APP_HOME/bin\n" +
                "#   for f in $(find . -name \"*_completion\"); do line=\". $(pwd)/$f\"; grep \"$line\" ~/.bash_profile || echo \"$line\" >> ~/.bash_profile; done\n" +
                "#\n" +
                "# 2. Open a new bash console, and type `%1$s [TAB][TAB]`\n" +
                "#\n" +
                "# 1a. Alternatively, if you have [bash-completion](https://github.com/scop/bash-completion) installed:\n" +
                "#     Place this file in a `bash-completion.d` folder:\n" +
                "#\n" +
                "#   * /etc/bash-completion.d\n" +
                "#   * /usr/local/etc/bash-completion.d\n" +
                "#   * ~/bash-completion.d\n" +
                "#\n" +
                "# Documentation\n" +
                "# -------------\n" +
                "# The script is called by bash whenever [TAB] or [TAB][TAB] is pressed after\n" +
                "# '%1$s (..)'. By reading entered command line parameters,\n" +
                "# it determines possible bash completions and writes them to the COMPREPLY variable.\n" +
                "# Bash then completes the user input if only one entry is listed in the variable or\n" +
                "# shows the options if more than one is listed in COMPREPLY.\n" +
                "#\n" +
                "# References\n" +
                "# ----------\n" +
                "# [1] http://stackoverflow.com/a/12495480/1440785\n" +
                "# [2] http://tiswww.case.edu/php/chet/bash/FAQ\n" +
                "# [3] https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html\n" +
                "# [4] http://zsh.sourceforge.net/Doc/Release/Options.html#index-COMPLETE_005fALIASES\n" +
                "# [5] https://stackoverflow.com/questions/17042057/bash-check-element-in-array-for-elements-in-another-array/17042655#17042655\n" +
                "# [6] https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html#Programmable-Completion\n" +
                "# [7] https://stackoverflow.com/questions/3249432/can-a-bash-tab-completion-script-be-used-in-zsh/27853970#27853970\n" +
                "#\n" +
                "\n" +
                "if [ -n \"$BASH_VERSION\" ]; then\n" +
                "  # Enable programmable completion facilities when using bash (see [3])\n" +
                "  shopt -s progcomp\n" +
                "elif [ -n \"$ZSH_VERSION\" ]; then\n" +
                "  # Make alias a distinct command for completion purposes when using zsh (see [4])\n" +
                "  setopt COMPLETE_ALIASES\n" +
                "  alias compopt=complete\n" +
                "\n" +
                "  # Enable bash completion in zsh (see [7])\n" +
                "  # Only initialize completions module once to avoid unregistering existing completions.\n" +
                "  if ! type compdef > /dev/null; then\n" +
                "    autoload -U +X compinit && compinit\n" +
                "  fi\n" +
                "  autoload -U +X bashcompinit && bashcompinit\n" +
                "fi\n" +
                "\n" +
                "# CompWordsContainsArray takes an array and then checks\n" +
                "# if all elements of this array are in the global COMP_WORDS array.\n" +
                "#\n" +
                "# Returns zero (no error) if all elements of the array are in the COMP_WORDS array,\n" +
                "# otherwise returns 1 (error).\n" +
                "function CompWordsContainsArray() {\n" +
                "  declare -a localArray\n" +
                "  localArray=(\"$@\")\n" +
                "  local findme\n" +
                "  for findme in \"${localArray[@]}\"; do\n" +
                "    if ElementNotInCompWords \"$findme\"; then return 1; fi\n" +
                "  done\n" +
                "  return 0\n" +
                "}\n" +
                "function ElementNotInCompWords() {\n" +
                "  local findme=\"$1\"\n" +
                "  local element\n" +
                "  for element in \"${COMP_WORDS[@]}\"; do\n" +
                "    if [[ \"$findme\" = \"$element\" ]]; then return 1; fi\n" +
                "  done\n" +
                "  return 0\n" +
                "}\n" +
                "\n" +
                "# The `currentPositionalIndex` function calculates the index of the current positional parameter.\n" +
                "#\n" +
                "# currentPositionalIndex takes three parameters:\n" +
                "# the command name,\n" +
                "# a space-separated string with the names of options that take a parameter, and\n" +
                "# a space-separated string with the names of boolean options (that don't take any params).\n" +
                "# When done, this function echos the current positional index to std_out.\n" +
                "#\n" +
                "# Example usage:\n" +
                "# local currIndex=$(currentPositionalIndex \"mysubcommand\" \"$ARG_OPTS\" \"$FLAG_OPTS\")\n" +
                "function currentPositionalIndex() {\n" +
                "  local commandName=\"$1\"\n" +
                "  local optionsWithArgs=\"$2\"\n" +
                "  local booleanOptions=\"$3\"\n" +
                "  local previousWord\n" +
                "  local result=0\n" +
                "\n" +
                "  for i in $(seq $((COMP_CWORD - 1)) -1 0); do\n" +
                "    previousWord=${COMP_WORDS[i]}\n" +
                "    if [ \"${previousWord}\" = \"$commandName\" ]; then\n" +
                "      break\n" +
                "    fi\n" +
                "    if [[ \"${optionsWithArgs}\" =~ ${previousWord} ]]; then\n" +
                "      ((result-=2)) # Arg option and its value not counted as positional param\n" +
                "    elif [[ \"${booleanOptions}\" =~ ${previousWord} ]]; then\n" +
                "      ((result-=1)) # Flag option itself not counted as positional param\n" +
                "    fi\n" +
                "    ((result++))\n" +
                "  done\n" +
                "  echo \"$result\"\n" +
                "}\n" +
                "\n" +
                "# compReplyArray generates a list of completion suggestions based on an array, ensuring all values are properly escaped.\n" +
                "#\n" +
                "# compReplyArray takes a single parameter: the array of options to be displayed\n" +
                "#\n" +
                "# The output is echoed to std_out, one option per line.\n" +
                "#\n" +
                "# Example usage:\n" +
                "# local options=(\"foo\", \"bar\", \"baz\")\n" +
                "# local IFS=$'\\n'\n" +
                "# COMPREPLY=($(compReplyArray \"${options[@]}\"))\n" +
                "function compReplyArray() {\n" +
                "  declare -a options\n" +
                "  options=(\"$@\")\n" +
                "  local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
                "  local i\n" +
                "  local quoted\n" +
                "  local optionList=()\n" +
                "\n" +
                "  for (( i=0; i<${#options[@]}; i++ )); do\n" +
                "    # Double escape, since we want escaped values, but compgen -W expands the argument\n" +
                "    printf -v quoted %%q \"${options[i]}\"\n" +
                "    quoted=\\'${quoted//\\'/\\'\\\\\\'\\'}\\'\n" +
                "\n" +
                "    optionList[i]=$quoted\n" +
                "  done\n" +
                "\n" +
                "  # We also have to add another round of escaping to $curr_word.\n" +
                "  curr_word=${curr_word//\\\\/\\\\\\\\}\n" +
                "  curr_word=${curr_word//\\'/\\\\\\'}\n" +
                "\n" +
                "  # Actually generate completions.\n" +
                "  local IFS=$'\\n'\n" +
                "  echo -e \"$(compgen -W \"${optionList[*]}\" -- \"$curr_word\")\"\n" +
                "}\n" +
                "\n" +
                "# Bash completion entry point function.\n" +
                "# _complete_%1$s finds which commands and subcommands have been specified\n" +
                "# on the command line and delegates to the appropriate function\n" +
                "# to generate possible options and subcommands for the last specified subcommand.\n" +
                "function _complete_%1$s() {\n" +
                "  # Edge case: if command line has no space after subcommand, then don't assume this subcommand is selected (remkop/picocli#1468).\n" +
                "  if [ \"${COMP_LINE}\" = \"${COMP_WORDS[0]} help\" ];    then _picocli_CompletionDemo; return $?; fi\n" +
                "\n" +
                "  # Find the longest sequence of subcommands and call the bash function for that subcommand.\n" +
                "  local cmds0=(help)\n" +
                "\n" +
                "  if CompWordsContainsArray \"${cmds0[@]}\"; then _picocli_%1$s_help; return $?; fi\n" +
                "\n" +
                "  # No subcommands were specified; generate completions for the top-level command.\n" +
                "  _picocli_%1$s; return $?;\n" +
                "}\n" +
                "\n" +
                "# Generates completions for the options and subcommands of the `%1$s` command.\n" +
                "function _picocli_%1$s() {\n" +
                "  # Get completion data\n" +
                "  local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
                "  local prev_word=${COMP_WORDS[COMP_CWORD-1]}\n" +
                "\n" +
                "  local commands=\"help\"\n" + // NOTE: no generate-completion: this command is hidden
                "  local flag_opts=\"\"\n" +
                "  local arg_opts=\"--apples --bbb\"\n" + // NOTE: no --aaa: this option is hidden
                "\n" +
                "  type compopt &>/dev/null && compopt +o default\n" +
                "\n" +
                "  case ${prev_word} in\n" +
                "    --apples)\n" +
                "      return\n" +
                "      ;;\n" +
                "    --bbb)\n" +
                "      return\n" +
                "      ;;\n" +
                "  esac\n" +
                "\n" +
                "  if [[ \"${curr_word}\" == -* ]]; then\n" +
                "    COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" +
                "  else\n" +
                "    local positionals=\"\"\n" +
                "    local IFS=$'\\n'\n" +
                "    COMPREPLY=( $(compgen -W \"${commands// /$'\\n'}${IFS}${positionals}\" -- \"${curr_word}\") )\n" +
                "  fi\n" +
                "}\n" +
                "\n" +
                "# Generates completions for the options and subcommands of the `help` subcommand.\n" +
                "function _picocli_%1$s_help() {\n" +
                "  # Get completion data\n" +
                "  local curr_word=${COMP_WORDS[COMP_CWORD]}\n" +
                "\n" +
                "  local commands=\"\"\n" +
                "  local flag_opts=\"-h --help\"\n" +
                "  local arg_opts=\"\"\n" +
                "\n" +
                "  if [[ \"${curr_word}\" == -* ]]; then\n" +
                "    COMPREPLY=( $(compgen -W \"${flag_opts} ${arg_opts}\" -- \"${curr_word}\") )\n" +
                "  else\n" +
                "    local positionals=\"\"\n" +
                "    local IFS=$'\\n'\n" +
                "    COMPREPLY=( $(compgen -W \"${commands// /$'\\n'}${IFS}${positionals}\" -- \"${curr_word}\") )\n" +
                "  fi\n" +
                "}\n" +
                "\n" +
                "# Define a completion specification (a compspec) for the\n" +
                "# `%1$s`, `%1$s.sh`, and `%1$s.bash` commands.\n" +
                "# Uses the bash `complete` builtin (see [6]) to specify that shell function\n" +
                "# `_complete_%1$s` is responsible for generating possible completions for the\n" +
                "# current word on the command line.\n" +
                "# The `-o default` option means that if the function generated no matches, the\n" +
                "# default Bash completions and the Readline default filename completions are performed.\n" +
                "complete -F _complete_%1$s -o default %1$s %1$s.sh %1$s.bash\n" +
                "\n", commandName, CommandLine.VERSION);
    }


    @Test
    public void testNestedCompletion() {
        @Command(name="Demo", subcommands = { NestedLevel1.class } )
        class NestedCompletionDemo implements Runnable {
            public void run() { }
        }

        CommandLine root = new CommandLine(new NestedCompletionDemo());
        String expectedRoot = String.format("" +
                "Usage: Demo [COMMAND]%n" +
                "Commands:%n" +
                "  Level1%n");
        assertEquals(expectedRoot, root.getUsageMessage(CommandLine.Help.Ansi.OFF));

        CommandLine level2 = root
                .getSubcommands().get("Level1")
                .getSubcommands().get("Level2");
        String expectedLevel2 = String.format("" +
                "Usage: Demo Level1 Level2 [COMMAND]%n" +
                "Commands:%n" +
                "  generate-completion  Generate bash/zsh completion script for Demo.%n");
        assertEquals(expectedLevel2, level2.getUsageMessage(CommandLine.Help.Ansi.OFF));

        CommandLine gen = level2
                .getSubcommands().get("generate-completion");
        gen.getCommandSpec().usageMessage().hidden(true);
        String expectedGen = String.format("" +
                "Usage: Demo Level1 Level2 generate-completion [-hV]%n" +
                "Generate bash/zsh completion script for Demo.%n" +
                "Run the following command to give `Demo` TAB completion in the current shell:%n" +
                "%n" +
                "  source <(Demo Level1 Level2 generate-completion)%n" +
                "%n" +
                "Options:%n" +
                "  -h, --help      Show this help message and exit.%n" +
                "  -V, --version   Print version information and exit.%n");
        assertEquals(expectedGen, gen.getUsageMessage(CommandLine.Help.Ansi.OFF));
    }

    @Command(name = "Level2", subcommands = {picocli.AutoComplete.GenerateCompletion.class})
    static class NestedLevel2 implements Runnable {
        public void run() {
        }
    }

    @Command(name = "Level1", subcommands = {NestedLevel2.class})
    static class NestedLevel1 implements Runnable {
        public void run() {
        }
    }

    @Command(name = "${project.cli.command}", resourceBundle = "picocli.Issue1352ResourceBundle",
            mixinStandardHelpOptions = true)
    static class Issue1352CommandWithResourceBundle {}

    @Test
    public void testIssue1352_CommandNameResourceBundle() throws FileNotFoundException {
        // AutoComplete.bash("scriptname", new CommandLine(new Issue1352CommandWithResourceBundle()));
        File existingScript = new File("mycommandfromresourcebundle_completion");
        if (existingScript.exists()) {
            assertTrue(existingScript.delete());
        }
        try {
            AutoComplete.main(Issue1352CommandWithResourceBundle.class.getName());

            assertEquals("", systemErrRule.getLog());
            assertEquals("", systemOutRule.getLog());

            assertTrue("Expected file '" + existingScript.getAbsolutePath() + "' to exist",
                    existingScript.exists());

            Scanner scanner = new Scanner(existingScript);
            scanner.useDelimiter("\\Z"); // end of file
            String script = scanner.next();
            scanner.close();
            assertThat(script, containsString("mycommandfromresourcebundle"));
            assertThat(script, not(containsString("${project.cli.command}")));
        } finally {
            existingScript.delete();
        }
    }

    @Command(name = "parent", resourceBundle = "picocli.Issue1352ResourceBundle",
            subcommands = Issue1352CommandWithResourceBundle.class,
            mixinStandardHelpOptions = true)
    static class Issue1352ParentCommand {}

    @Test
    public void testIssue1352_SubcommandNameResourceBundle() throws FileNotFoundException {
        File existingScript = new File("parent_completion");
        if (existingScript.exists()) {
            assertTrue(existingScript.delete());
        }
        try {
            AutoComplete.main(Issue1352ParentCommand.class.getName());

            assertEquals("", systemErrRule.getLog());
            assertEquals("", systemOutRule.getLog());

            assertTrue("Expected file '" + existingScript.getAbsolutePath() + "' to exist",
                    existingScript.exists());

            Scanner scanner = new Scanner(existingScript);
            scanner.useDelimiter("\\Z"); // end of file
            String script = scanner.next();
            scanner.close();
            assertThat(script, containsString("_picocli_parent"));
            assertThat(script, containsString("mycommandfromresourcebundle"));

            // FIXME: the below assertion fails with picocli 4.6.1...
            //assertThat(script, not(containsString("${project.cli.command}")));

        } finally {
            existingScript.delete();
        }
    }

    @CommandLine.Command(name = "aliases", aliases = {"a"})
    static class Issue1388AliasesCommand {}

    @CommandLine.Command(name = "aliases-parent", subcommands = {Issue1388AliasesCommand.class})
    static class Issue1388AliasesParentCommand {}

    @Test
    public void testIssue1388_AliasesCommand() throws FileNotFoundException {
        File existingScript = new File("aliases-parent_completion");
        if (existingScript.exists()) {
            assertTrue(existingScript.delete());
        }
        try {
            AutoComplete.main(Issue1388AliasesParentCommand.class.getName());

            assertEquals("", systemErrRule.getLog());
            assertEquals("", systemOutRule.getLog());

            assertTrue("Expected file '" + existingScript.getAbsolutePath() + "' to exist",
                    existingScript.exists());

            Scanner scanner = new Scanner(existingScript);
            scanner.useDelimiter("\\Z"); // end of file
            String script = scanner.next();
            scanner.close();
            assertThat(script, containsString("local cmds0=(aliases)"));
            assertThat(script, containsString("local cmds1=(a)"));
        } finally {
            existingScript.delete();
        }
    }
}
