/*
 * Created on Jul 10, 2011
 * Copyright 2010 by Eduard Weissmann (edi.weissmann@gmail.com).
 * 
 * This file is part of the Sejda source code
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.sejda.cli;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.mockito.ArgumentCaptor;
import org.sejda.cli.util.OnceWithMessage;
import org.sejda.cli.util.SystemOutRecordingStream;
import org.sejda.core.notification.context.GlobalNotificationContext;
import org.sejda.core.service.DefaultTaskExecutionService;
import org.sejda.core.service.TaskExecutionService;
import org.sejda.model.exception.SejdaRuntimeException;
import org.sejda.model.notification.EventListener;
import org.sejda.model.notification.event.TaskExecutionCompletedEvent;
import org.sejda.model.parameter.base.TaskParameters;

/**
 * Builder for command line arguments, used in tests
 * 
 * @author Eduard Weissmann
 * 
 */
public class CommandLineTestBuilder {

    private final String taskName;
    private final Map<String, String> optionsAndValues = new HashMap<>();

    public CommandLineTestBuilder(String taskName) {
        this.taskName = taskName;
    }

    /**
     * Populates 2 default input parameters
     * 
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder defaultTwoInputs() {
        with("-f", "inputs/input.pdf inputs/second_input.pdf");
        return this;
    }

    /**
     * Populates multiple input parameters using PDF and non PDf inputs
     * 
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder defaultMultipleNonPdfInputs() {
        with("-f", "inputs/input.pdf inputs/second_input.pdf inputs/file1.txt");
        return this;
    }

    /**
     * Populates default single input parameter
     * 
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder defaultSingleInput() {
        with("-f", "inputs/input.pdf");
        return this;
    }

    /**
     * Populates default multiple input parameter
     *
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder defaultMultiplePdfInputs() {
        with("-f", "inputs/input.pdf inputs/second_input.pdf");
        return this;
    }

    /**
     * Populates default output parameter as folder ./outputs
     * 
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder defaultFolderOutput() {
        with("-o", "./outputs");
        return this;
    }

    /**
     * Populates default output parameter as file fileOutput.pdf
     * 
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder defaultFileOutput() {
        with("-o", "./outputs/fileOutput.pdf");
        return this;
    }

    /**
     * Removes flag/option specified
     * 
     * @param option
     *            option to remove
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder without(String option) {
        optionsAndValues.remove(option);
        return this;
    }

    /**
     * Removes any flags/options already added
     * 
     * @return this builder (for telescopic usage)
     * 
     */
    public CommandLineTestBuilder reset() {
        optionsAndValues.clear();
        return this;
    }

    /**
     * Adds a new option and it's value to the command
     * 
     * @param option
     * @param value
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder with(String option, String value) {
        optionsAndValues.put(option, value);
        return this;
    }

    /**
     * Adds a new boolean flag option
     * 
     * @param option
     * @return this builder (for telescopic usage)
     */
    public CommandLineTestBuilder withFlag(String option) {
        optionsAndValues.put(option, null);
        return this;
    }

    /**
     * Builds a command line string, that calls the task specified as input, using the collected options & values
     * 
     * @return command line as string
     */
    public String toCommandLineString() {
        StringBuilder result = new StringBuilder(taskName);

        for (Map.Entry<String, String> eachOptionAndValue : optionsAndValues.entrySet()) {
            result.append(" ").append(eachOptionAndValue.getKey());
            if (eachOptionAndValue.getValue() != null) {
                result.append(" ").append(eachOptionAndValue.getValue());
            }
        }

        return result.toString();
    }

    public void assertConsoleOutputContains(String... expectedOutputContainedLines) {
        new CommandLineExecuteTestHelper(true).assertConsoleOutputContains(this.toCommandLineString(),
                expectedOutputContainedLines);
    }

    public <T> T invokeSejdaConsole() {
        return new CommandLineExecuteTestHelper(true)
                .invokeConsoleAndReturnTaskParameters(this.toCommandLineString());
    }
}

/**
 * Helper test class for execution of the sejda-console<br/>
 * Contains helper methods such as {@link #assertConsoleOutputContains(String, String...)}, {@link #invokeConsoleAndReturnTaskParameters(String)}<br/>
 * 
 * @author Eduard Weissmann
 * 
 */
class CommandLineExecuteTestHelper {
    protected TaskExecutionService taskExecutionService;
    private SystemOutRecordingStream newSystemOut = new SystemOutRecordingStream(System.out);
    private Map<CustomizableProps, String> customs;

    CommandLineExecuteTestHelper(boolean useMockTaskExecutionService) {
        this(useMockTaskExecutionService, new HashMap<>());
        customs.put(CustomizableProps.APP_NAME, "Sejda Console");
        customs.put(CustomizableProps.LICENSE_PATH, "/sejda-console/SEJDA_LICENSE.txt");
    }

    CommandLineExecuteTestHelper(boolean useMockTaskExecutionService, Map<CustomizableProps, String> customs) {
        if (useMockTaskExecutionService) {
            taskExecutionService = mock(TaskExecutionService.class);
        } else {
            taskExecutionService = new DefaultTaskExecutionService();
        }
        this.customs = customs;
    }

    private SejdaConsole getConsole(String commandLine) {
        String[] args = parseCommandLineArgs(commandLine);
        return new SejdaConsole(args, new DefaultTaskExecutionAdapter(taskExecutionService), customs);
    }

    /**
     * Simulate's java's cli argument parsing. That means {@code java MyProgram 1234 www.caltech.edu "olive festival"} has 3 arguments: <br/>
     * <ul>
     * <li>args[0] = "1234"</li>
     * <li>args[1] = "www.caltech.edu"</li>
     * <li>args[2] = "olive festival"</li>
     * <ul/>
     * 
     */
    static String[] parseCommandLineArgs(String commandLine) {
        List<String> result = new ArrayList<String>();

        Matcher m = Pattern.compile("\"([^\"]*)\"|(\\S+)").matcher(commandLine);
        while (m.find()) {
            if (m.group(1) != null) {
                result.add(m.group(1));
            } else {
                result.add(m.group(2));
            }
        }
        return result.toArray(new String[] {});
    }

    public void assertConsoleOutputContains(String commandLine, String... expectedOutputContainedLines) {
        String consoleOutput = invokeConsoleAndReturnSystemOut(commandLine);
        for (String eachExpected : expectedOutputContainedLines) {
            assertThat(consoleOutput, containsString(eachExpected));
        }
    }

    public void assertTaskCompletes(String commandLine) {
        final MutableBoolean taskCompleted = new MutableBoolean(false);
        GlobalNotificationContext.getContext().addListener(new EventListener<TaskExecutionCompletedEvent>() {

            @Override
            public void onEvent(TaskExecutionCompletedEvent event) {
                taskCompleted.setValue(true);
            }

        });

        String consoleOutput = invokeConsoleAndReturnSystemOut(commandLine);
        assertThat("Task did not complete. Console output was:\n" + consoleOutput, taskCompleted.toBoolean(), is(true));
    }

    private String invokeConsoleAndReturnSystemOut(String commandLine) {
        invokeConsoleIgnoringExpectedExceptions(commandLine);

        return getCapturedSystemOut();
    }

    private void invokeConsoleIgnoringExpectedExceptions(String commandLine) {
        prepareSystemOutCapture();
        try {
            getConsole(commandLine).execute();
            // fail("Console execution should have failed, no? " + commandLine);
        } catch (SejdaRuntimeException e) {
            // no-op
        } catch (Exception e) {
            throw new SejdaRuntimeException("An unexpected exception occurred while executing the console", e);
        }
    }

    private void prepareSystemOutCapture() {
        //newSystemOut = new SystemOutRecordingStream(System.out);
        newSystemOut = new SystemOutRecordingStream(new NullOutputStream());
        System.setOut(new PrintStream(newSystemOut));
    }

    private String getCapturedSystemOut() {
        return newSystemOut.getCapturedSystemOut();
    }

    @SuppressWarnings("unchecked")
    public <T extends TaskParameters> T invokeConsoleAndReturnTaskParameters(String commandLine) {
        ArgumentCaptor<TaskParameters> taskPrametersCaptor = ArgumentCaptor.forClass(TaskParameters.class);

        invokeConsoleIgnoringExpectedExceptions(commandLine);

        // now Mockito can provide some context to verification failures, yay
        verify(taskExecutionService,
                once("Command '" + commandLine
                        + "' did not reach task execution, as was expected. Console output was: \n"
                        + getCapturedSystemOut())).execute(taskPrametersCaptor.capture());
        return (T) taskPrametersCaptor.getValue();
    }

    private static OnceWithMessage once(String failureDescribingMessage) {
        return new OnceWithMessage(failureDescribingMessage);
    }
}
