/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.elasticsearch.common.cli;

import com.google.common.collect.ImmutableMap;
import org.apache.commons.cli.CommandLine;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.junit.Test;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
import static org.hamcrest.Matchers.*;

/**
 *
 */
public class CliToolTests extends CliToolTestCase {

    @Test
    public void testOK() throws Exception {
        Terminal terminal = new MockTerminal();
        final AtomicReference<Boolean> executed = new AtomicReference<>(false);
        final NamedCommand cmd = new NamedCommand("cmd", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) {
                executed.set(true);
                return CliTool.ExitStatus.OK;
            }
        };
        SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
        int status = tool.execute();
        assertStatus(status, CliTool.ExitStatus.OK);
        assertCommandHasBeenExecuted(executed);
    }

    @Test
    public void testUsageError() throws Exception {
        Terminal terminal = new MockTerminal();
        final AtomicReference<Boolean> executed = new AtomicReference<>(false);
        final NamedCommand cmd = new NamedCommand("cmd", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) {
                executed.set(true);
                return CliTool.ExitStatus.USAGE;
            }
        };
        SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
        int status = tool.execute();
        assertStatus(status, CliTool.ExitStatus.USAGE);
        assertCommandHasBeenExecuted(executed);
    }

    @Test
    public void testIOError() throws Exception {
        Terminal terminal = new MockTerminal();
        final AtomicReference<Boolean> executed = new AtomicReference<>(false);
        final NamedCommand cmd = new NamedCommand("cmd", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                executed.set(true);
                throw new IOException("io error");
            }
        };
        SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
        int status = tool.execute();
        assertStatus(status, CliTool.ExitStatus.IO_ERROR);
        assertCommandHasBeenExecuted(executed);
    }

    @Test
    public void testCodeError() throws Exception {
        Terminal terminal = new MockTerminal();
        final AtomicReference<Boolean> executed = new AtomicReference<>(false);
        final NamedCommand cmd = new NamedCommand("cmd", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                executed.set(true);
                throw new Exception("random error");
            }
        };
        SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
        int status = tool.execute();
        assertStatus(status, CliTool.ExitStatus.CODE_ERROR);
        assertCommandHasBeenExecuted(executed);
    }

    @Test
    public void testMultiCommand() {
        Terminal terminal = new MockTerminal();
        int count = randomIntBetween(2, 7);
        final AtomicReference<Boolean>[] executed = new AtomicReference[count];
        for (int i = 0; i < executed.length; i++) {
            executed[i] = new AtomicReference<>(false);
        }
        NamedCommand[] cmds = new NamedCommand[count];
        for (int i = 0; i < count; i++) {
            final int index = i;
            cmds[i] = new NamedCommand("cmd" + index, terminal) {
                @Override
                public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                    executed[index].set(true);
                    return CliTool.ExitStatus.OK;
                }
            };
        }
        MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
        int cmdIndex = randomIntBetween(0, count-1);
        int status = tool.execute("cmd" + cmdIndex);
        assertThat(status, is(CliTool.ExitStatus.OK.status()));
        for (int i = 0; i < executed.length; i++) {
            assertThat(executed[i].get(), is(i == cmdIndex));
        }
    }

    @Test
    public void testMultiCommand_UnknownCommand() {
        Terminal terminal = new MockTerminal();
        int count = randomIntBetween(2, 7);
        final AtomicReference<Boolean>[] executed = new AtomicReference[count];
        for (int i = 0; i < executed.length; i++) {
            executed[i] = new AtomicReference<>(false);
        }
        NamedCommand[] cmds = new NamedCommand[count];
        for (int i = 0; i < count; i++) {
            final int index = i;
            cmds[i] = new NamedCommand("cmd" + index, terminal) {
                @Override
                public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                    executed[index].set(true);
                    return CliTool.ExitStatus.OK;
                }
            };
        }
        MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
        int status = tool.execute("cmd" + count); // "cmd" + count doesn't exist
        assertThat(status, is(CliTool.ExitStatus.USAGE.status()));
        for (int i = 0; i < executed.length; i++) {
            assertThat(executed[i].get(), is(false));
        }
    }

    @Test
    public void testSingleCommand_ToolHelp() throws Exception {
        CaptureOutputTerminal terminal = new CaptureOutputTerminal();
        final AtomicReference<Boolean> executed = new AtomicReference<>(false);
        final NamedCommand cmd = new NamedCommand("cmd1", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                executed.set(true);
                throw new IOException("io error");
            }
        };
        SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
        int status = tool.execute(args("-h"));
        assertStatus(status, CliTool.ExitStatus.OK);
        assertThat(terminal.getTerminalOutput(), hasSize(3));
        assertThat(terminal.getTerminalOutput(), hasItem(containsString("cmd1 help")));
    }

    @Test
    public void testMultiCommand_ToolHelp() {
        CaptureOutputTerminal terminal = new CaptureOutputTerminal();
        NamedCommand[] cmds = new NamedCommand[2];
        cmds[0] = new NamedCommand("cmd0", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                return CliTool.ExitStatus.OK;
            }
        };
        cmds[1] = new NamedCommand("cmd1", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                return CliTool.ExitStatus.OK;
            }
        };
        MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
        int status = tool.execute(args("-h"));
        assertStatus(status, CliTool.ExitStatus.OK);
        assertThat(terminal.getTerminalOutput(), hasSize(3));
        assertThat(terminal.getTerminalOutput(), hasItem(containsString("tool help")));
    }

    @Test
    public void testMultiCommand_CmdHelp() {
        CaptureOutputTerminal terminal = new CaptureOutputTerminal();
        NamedCommand[] cmds = new NamedCommand[2];
        cmds[0] = new NamedCommand("cmd0", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                return CliTool.ExitStatus.OK;
            }
        };
        cmds[1] = new NamedCommand("cmd1", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                return CliTool.ExitStatus.OK;
            }
        };
        MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds);
        int status = tool.execute(args("cmd1 -h"));
        assertStatus(status, CliTool.ExitStatus.OK);
        assertThat(terminal.getTerminalOutput(), hasSize(3));
        assertThat(terminal.getTerminalOutput(), hasItem(containsString("cmd1 help")));
    }

    @Test
    public void testThatThrowExceptionCanBeLogged() throws Exception {
        CaptureOutputTerminal terminal = new CaptureOutputTerminal();
        NamedCommand cmd = new NamedCommand("cmd", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
                throw new ElasticsearchException("error message");
            }
        };
        SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
        assertStatus(tool.execute(), CliTool.ExitStatus.CODE_ERROR);
        assertThat(terminal.getTerminalOutput(), hasSize(1));
        assertThat(terminal.getTerminalOutput(), hasItem(containsString("error message")));

        // set env... and log stack trace
        try {
            System.setProperty(Terminal.DEBUG_SYSTEM_PROPERTY, "true");
            terminal = new CaptureOutputTerminal();
            assertStatus(new SingleCmdTool("tool", terminal, cmd).execute(), CliTool.ExitStatus.CODE_ERROR);
            assertThat(terminal.getTerminalOutput(), hasSize(2));
            assertThat(terminal.getTerminalOutput(), hasItem(containsString("error message")));
            // This class must be part of the stack strace
            assertThat(terminal.getTerminalOutput(), hasItem(containsString(getClass().getName())));
        } finally {
            System.clearProperty(Terminal.DEBUG_SYSTEM_PROPERTY);
        }
    }

    @Test
    public void testMultipleLaunch() throws Exception {
        Terminal terminal = new MockTerminal();
        final AtomicReference<Boolean> executed = new AtomicReference<>(false);
        final NamedCommand cmd = new NamedCommand("cmd", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) {
                executed.set(true);
                return CliTool.ExitStatus.OK;
            }
        };
        SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd);
        tool.parse("cmd", Strings.splitStringByCommaToArray("--verbose"));
        tool.parse("cmd", Strings.splitStringByCommaToArray("--silent"));
        tool.parse("cmd", Strings.splitStringByCommaToArray("--help"));
    }

    @Test
    public void testPromptForSetting() throws Exception {
        final AtomicInteger counter = new AtomicInteger();
        final AtomicReference<String> promptedSecretValue = new AtomicReference<>(null);
        final AtomicReference<String> promptedTextValue = new AtomicReference<>(null);
        final Terminal terminal = new MockTerminal() {
            @Override
            public char[] readSecret(String text, Object... args) {
                counter.incrementAndGet();
                assertThat(args, arrayContaining((Object) "foo.password"));
                return "changeit".toCharArray();
            }

            @Override
            public String readText(String text, Object... args) {
                counter.incrementAndGet();
                assertThat(args, arrayContaining((Object) "replace"));
                return "replaced";
            }
        };
        final NamedCommand cmd = new NamedCommand("noop", terminal) {
            @Override
            public CliTool.ExitStatus execute(Settings settings, Environment env) {
                promptedSecretValue.set(settings.get("foo.password"));
                promptedTextValue.set(settings.get("replace"));
                return CliTool.ExitStatus.OK;
            }
        };

        System.setProperty("es.foo.password", InternalSettingsPreparer.SECRET_PROMPT_VALUE);
        System.setProperty("es.replace", InternalSettingsPreparer.TEXT_PROMPT_VALUE);
        try {
            new SingleCmdTool("tool", terminal, cmd).execute();
        } finally {
            System.clearProperty("es.foo.password");
            System.clearProperty("es.replace");
        }

        assertThat(counter.intValue(), is(2));
        assertThat(promptedSecretValue.get(), is("changeit"));
        assertThat(promptedTextValue.get(), is("replaced"));
    }

    private void assertStatus(int status, CliTool.ExitStatus expectedStatus) {
        assertThat(status, is(expectedStatus.status()));
    }

    private void assertCommandHasBeenExecuted(AtomicReference<Boolean> executed) {
        assertThat("Expected command atomic reference counter to be set to true", executed.get(), is(Boolean.TRUE));
    }

    private static class SingleCmdTool extends CliTool {

        private final Command command;

        private SingleCmdTool(String name, Terminal terminal, NamedCommand command) {
            super(CliToolConfig.config(name, SingleCmdTool.class)
                    .cmds(cmd(command.name, command.getClass()))
                    .build(), terminal);
            this.command = command;
        }

        @Override
        protected Command parse(String cmdName, CommandLine cli) throws Exception {
            return command;
        }
    }

    private static class MultiCmdTool extends CliTool {

        private final Map<String, Command> commands;

        private MultiCmdTool(String name, Terminal terminal, NamedCommand... commands) {
            super(CliToolConfig.config(name, MultiCmdTool.class)
                    .cmds(cmds(commands))
                    .build(), terminal);
            ImmutableMap.Builder<String, Command> commandByName = ImmutableMap.builder();
            for (int i = 0; i < commands.length; i++) {
                commandByName.put(commands[i].name, commands[i]);
            }
            this.commands = commandByName.build();
        }

        @Override
        protected Command parse(String cmdName, CommandLine cli) throws Exception {
            return commands.get(cmdName);
        }

        private static CliToolConfig.Cmd[] cmds(NamedCommand... commands) {
            CliToolConfig.Cmd[] cmds = new CliToolConfig.Cmd[commands.length];
            for (int i = 0; i < commands.length; i++) {
                cmds[i] = cmd(commands[i].name, commands[i].getClass()).build();
            }
            return cmds;
        }
    }

    private static abstract class NamedCommand extends CliTool.Command {

        private final String name;

        private NamedCommand(String name, Terminal terminal) {
            super(terminal);
            this.name = name;
        }
    }
}
