/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     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.apache.brooklyn.camp.brooklyn;

import static org.testng.Assert.fail;

import java.util.List;
import java.util.Map;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.MachineProvisioningLocation;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Dumper;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityAsserts;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.entity.software.base.SoftwareProcess;
import org.apache.brooklyn.entity.software.base.VanillaWindowsProcess;
import org.apache.brooklyn.entity.software.base.test.location.WindowsTestFixture;
import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.core.task.TaskPredicates;
import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes;
import org.apache.brooklyn.util.text.StringEscapes.JavaStringEscapes;
import org.apache.brooklyn.util.text.StringPredicates;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

/**
 * Tests Windows YAML blueprint features.
 */
@Test
public class WindowsYamlLiveTest extends AbstractWindowsYamlTest {
    
    // set EXISTING_WINDOWS_TEST_USER_PASS_HOST_ENV_VAR as per WindowsTestFixture to re-use existing machines
    
    // TODO Remove duplication of assertStreams and VanillaWindowsProcessWinrmStreamsLiveTest.assertStreams
    
    private static final Logger log = LoggerFactory.getLogger(WindowsYamlLiveTest.class);

    /**
     * Maps from the task names that are used to the names used in log/exception messages.
     */
    private static final Map<String, String> TASK_REGEX_TO_COMMAND = ImmutableMap.<String, String>builder()
            .put("winrm: pre-install-command.*", "pre-install-command")
            .put("winrm: install.*", "install-command")
            .put("winrm: post-install-command.*", "post-install-command")
            .put("winrm: customize.*", "customize-command")
            .put("winrm: pre-launch-command.*", "pre-launch-command")
            .put("winrm: launch.*", "launch-command")
            .put("winrm: post-launch-command.*", "post-launch-command")
            .put("winrm: stop-command.*", "stop-command")
            .put("winrm: is-running-command.*", "is-running-command")
            .build();

    protected List<String> yamlLocation;
    protected MachineProvisioningLocation<WinRmMachineLocation> location;
    protected WinRmMachineLocation machine;
    protected Entity app;
    
    protected boolean useDefaultProperties() {
        return true;
    }
    
    @BeforeClass(alwaysRun = true)
    public void setUpClass() throws Exception {
        super.setUp();
        
        location = WindowsTestFixture.setUpWindowsLocation(mgmt());
        machine = location.obtain(ImmutableMap.of());

        yamlLocation = ImmutableList.of(
                "location:",
                "  byon:",
                "    hosts:",
                "    - winrm: "+machine.getAddress().getHostAddress() 
                        // this is the default, probably not necessary but kept for posterity 
                        +":5985",
                "      password: "+JavaStringEscapes.wrapJavaString(machine.config().get(WinRmMachineLocation.PASSWORD)),
                "      user: "+machine.config().get(WinRmMachineLocation.USER),
                "      osFamily: windows");
    }

    @AfterClass(alwaysRun = true)
    public void tearDownClass() throws Exception {
        try {
            if (location != null) location.release(machine);
        } catch (Throwable t) {
            log.error("Caught exception in tearDownClass method", t);
        } finally {
            super.tearDown();
        }
    }

    @BeforeMethod(alwaysRun = true)
    @Override
    public void setUp() {
        // no-op; everything done @BeforeClass
    }

    @AfterMethod(alwaysRun = true)
    @Override
    public void tearDown() {
        try {
            if (app != null) Entities.destroy(app);
        } catch (Throwable t) {
            log.error("Caught exception in tearDown method", t);
        } finally {
            app = null;
        }
    }
    
    @Override
    protected ManagementContextInternal mgmt() {
        return (ManagementContextInternal) super.mgmt();
    }
    
    @Test(groups="Live")
    public void testPowershellMinimalist() throws Exception {
        Map<String, String> cmds = ImmutableMap.<String, String>builder()
                .put("myarg", "myval")
                .put("launch.powershell.command", JavaStringEscapes.wrapJavaString("& \"$Env:INSTALL_DIR\\exit0.ps1\""))
                .put("checkRunning.powershell.command", JavaStringEscapes.wrapJavaString("& \"$Env:INSTALL_DIR\\exit0.bat\""))
                .build();
        
        Map<String, List<String>> stdouts = ImmutableMap.of();
        
        runWindowsApp(cmds, stdouts, true, null);
    }
    
    @Test(groups="Live")
    public void testPowershell() throws Exception {
        Map<String, String> cmds = ImmutableMap.<String, String>builder()
                .put("myarg", "myval")
                .put("pre.install.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("install.powershell.command", "\"& c:\\\\echoMyArg.ps1 -myarg myInstall\"")
                .put("post.install.powershell.command", "\"& c:\\\\echoArg.bat myPostInstall\"")
                .put("customize.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.bat\"")
                .put("pre.launch.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.ps1\"")
                .put("launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("post.launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("checkRunning.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("stop.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .build();
        
        Map<String, List<String>> stdouts = ImmutableMap.<String, List<String>>builder()
                .put("winrm: install.*", ImmutableList.of("myInstall"))
                .put("winrm: post-install-command.*", ImmutableList.of("myPostInstall"))
                .put("winrm: customize.*", ImmutableList.of("myval"))
                .put("winrm: pre-launch-command.*", ImmutableList.of("myval"))
                .build();
        
        runWindowsApp(cmds, stdouts, false, null);
    }
    
    @Test(groups="Live")
    public void testBatch() throws Exception {
        Map<String, String> cmds = ImmutableMap.<String, String>builder()
                .put("myarg", "myval")
                .put("pre.install.command", "\"PowerShell -NonInteractive -NoProfile -Command c:\\\\exit0.ps1\"")
                .put("install.command", "\"PowerShell -NonInteractive -NoProfile -Command c:\\\\echoMyArg.ps1 -myarg myInstall\"")
                .put("post.install.command", "\"c:\\\\echoArg.bat myPostInstall\"")
                .put("customize.command", "\"c:\\\\echoFreemarkerMyarg.bat\"")
                .put("pre.launch.command", "\"PowerShell -NonInteractive -NoProfile -Command c:\\\\echoFreemarkerMyarg.ps1\"")
                .put("launch.command", "\"PowerShell -NonInteractive -NoProfile -Command c:\\\\exit0.ps1\"")
                .put("post.launch.command", "\"PowerShell -NonInteractive -NoProfile -Command c:\\\\exit0.ps1\"")
                .put("checkRunning.command", "\"PowerShell -NonInteractive -NoProfile -Command c:\\\\exit0.ps1\"")
                .put("stop.command", "\"PowerShell -NonInteractive -NoProfile -Command c:\\\\exit0.ps1\"")
                .build();

        Map<String, List<String>> stdouts = ImmutableMap.<String, List<String>>builder()
                .put("winrm: install.*", ImmutableList.of("myInstall"))
                .put("winrm: post-install-command.*", ImmutableList.of("myPostInstall"))
                .put("winrm: customize.*", ImmutableList.of("myval"))
                .put("winrm: pre-launch-command.*", ImmutableList.of("myval"))
                .build();
        
        runWindowsApp(cmds, stdouts, false, null);
    }
    
    @Test(groups="Live")
    public void testPowershellExit1() throws Exception {
        Map<String, String> cmds = ImmutableMap.<String, String>builder()
                .put("myarg", "myval")
                .put("pre.install.powershell.command", "\"& c:\\\\exit1.ps1\"")
                .put("install.powershell.command", "\"& c:\\\\echoMyArg.ps1 -myarg myInstall\"")
                .put("post.install.powershell.command", "\"& c:\\\\echoArg.bat myPostInstall\"")
                .put("customize.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.bat\"")
                .put("pre.launch.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.ps1\"")
                .put("launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("post.launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("checkRunning.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("stop.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .build();
        
        Map<String, List<String>> stdouts = ImmutableMap.of();
        
        runWindowsApp(cmds, stdouts, false, "winrm: pre-install-command.*");
    }
    
    // FIXME Failing to match the expected exception, but looks fine! Needs more investigation.
    @Test(groups="Live")
    public void testPowershellCheckRunningExit1() throws Exception {
        Map<String, String> cmds = ImmutableMap.<String, String>builder()
                .put("myarg", "myval")
                .put("pre.install.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("install.powershell.command", "\"& c:\\\\echoMyArg.ps1 -myarg myInstall\"")
                .put("post.install.powershell.command", "\"& c:\\\\echoArg.bat myPostInstall\"")
                .put("customize.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.bat\"")
                .put("pre.launch.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.ps1\"")
                .put("launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("post.launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("checkRunning.powershell.command", "\"& c:\\\\exit1.ps1\"")
                .put("stop.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .build();
        
        Map<String, List<String>> stdouts = ImmutableMap.of();
        
        runWindowsApp(cmds, stdouts, false, "winrm: is-running-command.*");
    }
    
    // FIXME Needs more work to get the stop's task that failed, so can assert got the right error message
    @Test(groups="Live")
    public void testPowershellStopExit1() throws Exception {
        Map<String, String> cmds = ImmutableMap.<String, String>builder()
                .put("myarg", "myval")
                .put("pre.install.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("install.powershell.command", "\"& c:\\\\echoMyArg.ps1 -myarg myInstall\"")
                .put("post.install.powershell.command", "\"& c:\\\\echoArg.bat myPostInstall\"")
                .put("customize.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.bat\"")
                .put("pre.launch.powershell.command", "\"& c:\\\\echoFreemarkerMyarg.ps1\"")
                .put("launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("post.launch.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("checkRunning.powershell.command", "\"& c:\\\\exit0.ps1\"")
                .put("stop.powershell.command", "\"& c:\\\\exit1.ps1\"")
                .build();
        
        Map<String, List<String>> stdouts = ImmutableMap.of();
        
        runWindowsApp(cmds, stdouts, false, "winrm: stop-command.*");
    }
    
    protected void runWindowsApp(Map<String, String> commands, Map<String, List<String>> stdouts, boolean useInstallDir, String taskRegexFailed) throws Exception {
        String cmdFailed = (taskRegexFailed == null) ? null : TASK_REGEX_TO_COMMAND.get(taskRegexFailed);
        
        List<String> yaml = Lists.newArrayList();
        yaml.addAll(yamlLocation);
        String prefix = useInstallDir ? "" : "c:\\";
        yaml.addAll(ImmutableList.of(
                "services:",
                "- type: org.apache.brooklyn.entity.software.base.VanillaWindowsProcess",
                "  brooklyn.config:",
                "    templates.preinstall:",
                "      classpath://org/apache/brooklyn/camp/brooklyn/echoFreemarkerMyarg.bat: c:\\echoFreemarkerMyarg.bat",
                "      classpath://org/apache/brooklyn/camp/brooklyn/echoFreemarkerMyarg.ps1: c:\\echoFreemarkerMyarg.ps1",
                "    files.preinstall:",
                "      classpath://org/apache/brooklyn/camp/brooklyn/echoArg.bat: "+prefix+"echoArg.bat",
                "      classpath://org/apache/brooklyn/camp/brooklyn/echoMyArg.ps1: "+prefix+"echoMyArg.ps1",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.bat: "+prefix+"exit0.bat",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.bat: "+prefix+"exit1.bat",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.ps1: "+prefix+"exit0.ps1",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.ps1: "+prefix+"exit1.ps1",
                ""));
        
        for (Map.Entry<String, String> entry : commands.entrySet()) {
            yaml.add("    "+entry.getKey()+": "+entry.getValue());
        }

        if (Strings.isBlank(cmdFailed)) {
            app = createAndStartApplication(Joiner.on("\n").join(yaml));
            waitForApplicationTasks(app);
            log.info("App started:");
            Dumper.dumpInfo(app);
            
            VanillaWindowsProcess entity = (VanillaWindowsProcess) app.getChildren().iterator().next();
            
            EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, true);
            assertStreams(entity, stdouts);
            
        } else if (cmdFailed.equals("stop-command")) {
            app = createAndStartApplication(Joiner.on("\n").join(yaml));
            waitForApplicationTasks(app);
            log.info("App started:");
            Dumper.dumpInfo(app);
            VanillaWindowsProcess entity = (VanillaWindowsProcess) app.getChildren().iterator().next();
            EntityAsserts.assertAttributeEqualsEventually(entity, Attributes.SERVICE_UP, true);
            
            entity.stop();
            assertSubTaskFailures(entity, ImmutableMap.of(taskRegexFailed, StringPredicates.containsLiteral("for "+cmdFailed)));
            
        } else {
            try {
                app = createAndStartApplication(Joiner.on("\n").join(yaml));
                fail("start should have failed for app="+app);
            } catch (Exception e) {
                if (!e.toString().contains("invalid result") || !e.toString().contains("for "+cmdFailed)) throw e;
            }
        }
    }

    @Test(groups="Live")
    public void testEnvVarResolution() throws Exception {
        List<String> yaml = Lists.newArrayList();
        yaml.addAll(yamlLocation);
        String in = "%KEY1%: %ADDR_RESOLVED%";
        yaml.addAll(ImmutableList.of(
            "services:",
            "  - type: org.apache.brooklyn.entity.software.base.VanillaWindowsProcess", 
            "    brooklyn.config:", 
            "      install.command: "+JavaStringEscapes.wrapJavaString("echo "+in), 
            "      customize.command: "+JavaStringEscapes.wrapJavaString("echo "+in), 
            "      launch.command: "+JavaStringEscapes.wrapJavaString("echo "+in), 
            "      stop.command: echo true", 
            "      checkRunning.command: echo true", 
            "      shell.env:", 
            "        KEY1: Address", 
            "        ADDR_RESOLVED: $brooklyn:attributeWhenReady(\"host.address\")"));

        app = createAndStartApplication(Joiner.on("\n").join(yaml));
        waitForApplicationTasks(app);
        log.info("App started:");
        Dumper.dumpInfo(app);
        
        
        Entity win = Iterables.getOnlyElement(app.getChildren());
        String out = "Address: "+win.sensors().get(SoftwareProcess.ADDRESS);
        assertPhaseStreamEquals(win, "install", "stdout", Predicates.equalTo(in));
        assertPhaseStreamEquals(win, "customize", "stdout", Predicates.equalTo(out));
        assertPhaseStreamEquals(win, "launch", "stdout", Predicates.equalTo(out));
    }
    
    private void assertPhaseStreamEquals(Entity entity, String phase, String stream, Predicate<String> check) {
        Optional<Task<?>> t = findTaskOrSubTask(entity, TaskPredicates.displayNameSatisfies(StringPredicates.startsWith("winrm: "+phase)));
        Asserts.assertThat(BrooklynTaskTags.stream(t.get(), stream).streamContents.get().trim(), check);
    }

    @Override
    protected Logger getLogger() {
        return log;
    }
}
