/**
 * Copyright (c) 2013, salesforce.com, inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 *    Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *    following disclaimer.
 *
 *    Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
 *    the following disclaimer in the documentation and/or other materials provided with the distribution.
 *
 *    Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
 *    promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package org.rundeck.plugin.salt;

import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepException;
import com.google.common.collect.ImmutableSet;
import org.apache.http.HttpException;
import org.apache.http.client.HttpClient;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.rundeck.plugin.salt.SaltApiNodeStepPlugin.SaltApiNodeStepFailureReason;
import org.rundeck.plugin.salt.output.SaltReturnResponse;
import org.rundeck.plugin.salt.output.SaltReturnResponseParseException;
import org.rundeck.plugin.salt.validation.SaltStepValidationException;

import java.util.Set;

public class SaltApiNodeStepPlugin_ExecuteTest extends AbstractSaltApiNodeStepPluginTest {

    @Before
    public void setup() throws Exception {
        spyPlugin();
    }

    @Test
    public void testExecuteWithAuthenticationFailure() {
        setupAuthenticate(null);
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[0], new String[0]);

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected authentication failure");
        } catch (NodeStepException e) {
            Assert.assertEquals(SaltApiNodeStepFailureReason.AUTHENTICATION_FAILURE, e.getFailureReason());
        }

        Mockito.verifyZeroInteractions(client, post);
    }

    @Test
    public void testExecuteWithValidationFailure() throws Exception {
        SaltStepValidationException e = new SaltStepValidationException("some property", "Some message",
                SaltApiNodeStepFailureReason.ARGUMENTS_INVALID, node.getNodename());
        Mockito.doThrow(e).when(plugin)
                .validate(Mockito.eq(PARAM_USER), Mockito.eq(PARAM_PASSWORD), Mockito.same(node));
        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected exception");
        } catch (SaltStepValidationException ne) {
            Assert.assertSame("Expected execute to throw mocked exception", e, ne);
        }
    }

    @Test
    public void testExecuteWithDataContextMissing() {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[0], new String[0]);
        dataContext.clear();
        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected exception");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.ARGUMENTS_MISSING, e.getFailureReason());
        }
    }

    @Test
    public void testExecuteInvokesWithCorrectCapability() throws Exception {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[0], new String[0]);

        plugin.executeNodeStep(pluginContext, configuration, node);
        Mockito.verify(plugin, Mockito.times(1)).authenticate(Mockito.same(latestCapability), Mockito.any(HttpClient.class),
                Mockito.anyString(), Mockito.anyString());
    }
    
    @Test
    public void testExecuteInvokesWithCorrectSecureOptions() throws Exception {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[0], new String[0]);

        Set<String> secureOptions = ImmutableSet.of();
        Mockito.doReturn(secureOptions).when(plugin).extractSecureDataFromDataContext(Mockito.same(dataContext));
        
        plugin.executeNodeStep(pluginContext, configuration, node);
        Mockito.verify(plugin, Mockito.times(1)).submitJob(Mockito.same(latestCapability), Mockito.same(client), Mockito.eq(AUTH_TOKEN), Mockito.eq(PARAM_MINION_NAME), Mockito.same(secureOptions));
    }
    
    @Test
    public void testExecuteInvokesLogout() throws Exception {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[0], new String[0]);

        plugin.executeNodeStep(pluginContext, configuration, node);
        Mockito.verify(plugin, Mockito.times(1)).logoutQuietly(Mockito.any(HttpClient.class),
                Mockito.eq(AUTH_TOKEN));
    }

    @Test
    public void testExecuteMakesLogWrapperAvailable() throws Exception {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[0], new String[0]);

        plugin.executeNodeStep(pluginContext, configuration, node);

        Mockito.verify(plugin, Mockito.times(1)).setLogWrapper(Mockito.same(pluginLogger));
    }

    @Test
    public void testSetLogWrapper() {
        plugin.setLogWrapper(pluginLogger);
        Assert.assertNotNull(plugin.logWrapper);
        Assert.assertSame(pluginLogger, plugin.logWrapper.getUnderlyingLogger());
    }

    @Test
    public void testExecuteWithSuccessfulExitCode() throws NodeStepException {
        setupAuthenticate();
        doNothingWhenSetupLogger();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();

        String output1 = "line 1 of output";
        String output2 = "line 2 of output";
        String error1 = "line 1 of error";
        String error2 = "line 2 of error";
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[] { output1, output2 }, new String[] { error1,
                error2 });

        plugin.executeNodeStep(pluginContext, configuration, node);

        Mockito.verify(returnHandlerRegistry, Mockito.times(1)).getHandlerFor(Mockito.eq(PARAM_FUNCTION),
                Mockito.same(plugin.defaultReturnHandler));
        Mockito.verify(returnHandler, Mockito.times(1)).extractResponse(Mockito.eq(HOST_RESPONSE));

        InOrder ordering = Mockito.inOrder(log);
        ordering.verify(log, Mockito.times(1)).info(Mockito.eq(output1));
        ordering.verify(log, Mockito.times(1)).info(Mockito.eq(output2));
        ordering.verify(log, Mockito.times(1)).error(Mockito.eq(error1));
        ordering.verify(log, Mockito.times(1)).error(Mockito.eq(error2));
    }

    @Test
    public void testExecuteWithUnsuccessfulExitCode() {
        setupAuthenticate();
        doNothingWhenSetupLogger();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();

        String output1 = "line 1 of output";
        String output2 = "line 2 of output";
        String error1 = "line 1 of error";
        String error2 = "line 2 of error";
        setupDoReturnSaltResponseWhenExtractResponse(1, new String[] { output1, output2 }, new String[] { error1,
                error2 });

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected failure due to exit code.");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.EXIT_CODE, e.getFailureReason());
        }

        Mockito.verify(returnHandlerRegistry, Mockito.times(1)).getHandlerFor(Mockito.eq(PARAM_FUNCTION),
                Mockito.same(plugin.defaultReturnHandler));
        Mockito.verify(returnHandler, Mockito.times(1)).extractResponse(Mockito.eq(HOST_RESPONSE));

        InOrder ordering = Mockito.inOrder(log);
        ordering.verify(log, Mockito.times(1)).info(Mockito.eq(output1));
        ordering.verify(log, Mockito.times(1)).info(Mockito.eq(output2));
        ordering.verify(log, Mockito.times(1)).error(Mockito.eq(error1));
        ordering.verify(log, Mockito.times(1)).error(Mockito.eq(error2));
    }

    @Test
    public void testExecuteWithSaltResponseParseException() {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();

        SaltReturnResponseParseException pe = new SaltReturnResponseParseException("message");
        Mockito.when(returnHandler.extractResponse(Mockito.anyString())).thenThrow(pe);

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected failure due to response parse exception.");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.SALT_API_FAILURE, e.getFailureReason());
            Assert.assertSame("Expected parse exception to be set as root cause", pe, e.getCause());
        }

        Mockito.verify(returnHandlerRegistry, Mockito.times(1)).getHandlerFor(Mockito.eq(PARAM_FUNCTION),
                Mockito.same(plugin.defaultReturnHandler));
        Mockito.verify(returnHandler, Mockito.times(1)).extractResponse(Mockito.eq(HOST_RESPONSE));
    }

    @Test
    public void testExecuteWithSaltApiException() {
        setupAuthenticate();
        setupDoThrowWhenSubmitJob(new SaltApiException("Some message"));

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected node step failure.");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.SALT_API_FAILURE, e.getFailureReason());
        }
    }

    @Test
    public void testExecuteWithSaltTargettingException() {
        setupAuthenticate();
        setupDoThrowWhenSubmitJob(new SaltTargettingMismatchException("Some message"));

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected node step failure.");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.SALT_TARGET_MISMATCH, e.getFailureReason());
        }
    }

    @Test
    public void testExecuteWithHttpException() {
        setupAuthenticate();
        setupDoThrowWhenSubmitJob(new HttpException("Some message"));

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected node step failure.");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.COMMUNICATION_FAILURE, e.getFailureReason());
        }
    }

    @Test
    public void testExecuteWithUnsupportedEndPointScheme() {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();
        setupDoReturnHostResponseWhenWaitForResponse();
        setupDoReturnSaltResponseWhenExtractResponse(0, new String[0], new String[0]);

        plugin.setEndPointSchemes("http");

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected node step failure.");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.ARGUMENTS_INVALID, e.getFailureReason());
        }
    }

    @Test
    public void testExecuteWithInterruptedException() throws Exception {
        setupAuthenticate();
        setupDoReturnJidWhenSubmitJob();

        Mockito.doThrow(new InterruptedException())
                .when(plugin)
                .waitForJidResponse(Mockito.same(client), Mockito.eq(AUTH_TOKEN), Mockito.eq(OUTPUT_JID),
                        Mockito.eq(PARAM_MINION_NAME));

        try {
            plugin.executeNodeStep(pluginContext, configuration, node);
            Assert.fail("Expected node step failure.");
        } catch (NodeStepException e) {
            Assert.assertEquals("Expected failure reason to be set based on exception type",
                    SaltApiNodeStepFailureReason.INTERRUPTED, e.getFailureReason());
        }
    }

    protected SaltApiNodeStepPlugin_ExecuteTest setupDoReturnSaltResponseWhenExtractResponse(int exitCode,
            String[] stdout, String[] stderr) {
        SaltReturnResponse response = new SaltReturnResponse();
        for (String out : stdout) {
            response.addOutput(out);
        }
        for (String err : stderr) {
            response.addError(err);
        }
        response.setExitCode(exitCode);
        Mockito.when(returnHandler.extractResponse(Mockito.anyString())).thenReturn(response);
        return this;
    }

    protected SaltApiNodeStepPlugin_ExecuteTest setupDoReturnHostResponseWhenWaitForResponse() {
        try {
            Mockito.doReturn(HOST_RESPONSE)
                    .when(plugin)
                    .waitForJidResponse(Mockito.same(client), Mockito.eq(AUTH_TOKEN), Mockito.eq(OUTPUT_JID),
                            Mockito.eq(PARAM_MINION_NAME));
            return this;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected SaltApiNodeStepPlugin_ExecuteTest setupDoReturnJidWhenSubmitJob() {
        try {
            Mockito.doReturn(OUTPUT_JID).when(plugin)
                    .submitJob(Mockito.same(latestCapability), Mockito.same(client), Mockito.eq(AUTH_TOKEN), Mockito.eq(PARAM_MINION_NAME), Mockito.anySet());
            return this;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected SaltApiNodeStepPlugin_ExecuteTest setupDoThrowWhenSubmitJob(Throwable t) {
        try {
            Mockito.doThrow(t).when(plugin)
                    .submitJob(Mockito.same(latestCapability), Mockito.same(client), Mockito.eq(AUTH_TOKEN), Mockito.eq(PARAM_MINION_NAME), Mockito.anySet());
            return this;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected SaltApiNodeStepPlugin_ExecuteTest doNothingWhenSetupLogger() {
        Mockito.doNothing().when(plugin).setLogWrapper(Mockito.same(pluginLogger));
        return this;
    }
}