/*
 * 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 com.google.common.base.Preconditions.checkNotNull;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

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

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.MachineProvisioningLocation;
import org.apache.brooklyn.api.mgmt.HasTaskChildren;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.core.entity.Attributes;
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.util.core.task.TaskPredicates;
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.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

/**
 * Tests Windows YAML blueprint features.
 */
@Test
public class WindowsYamlLiveTest extends AbstractYamlTest {
    
    // 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());
        String ip = machine.getAddress().getHostAddress();
        String password = machine.config().get(WinRmMachineLocation.PASSWORD);

        yamlLocation = ImmutableList.of(
                "location:",
                "  byon:",
                "    hosts:",
                "    - winrm: "+ip+":5985",
                "      password: \""+password.replace("\"", "\\\"") + "\"",
                "      user: Administrator",
                "      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", "\"& c:\\\\exit0.ps1\"")
                .put("checkRunning.powershell.command", "\"& c:\\\\exit0.bat\"")
                .build();
        
        Map<String, List<String>> stdouts = ImmutableMap.of();
        
        runWindowsApp(cmds, stdouts, 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, 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, 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, "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, "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, "winrm: stop-command.*");
    }
    
    protected void runWindowsApp(Map<String, String> commands, Map<String, List<String>> stdouts, String taskRegexFailed) throws Exception {
        String cmdFailed = (taskRegexFailed == null) ? null : TASK_REGEX_TO_COMMAND.get(taskRegexFailed);
        
        List<String> yaml = Lists.newArrayList();
        yaml.addAll(yamlLocation);
        yaml.addAll(ImmutableList.of(
                "services:",
                "- type: org.apache.brooklyn.entity.software.base.VanillaWindowsProcess",
                "  brooklyn.config:",
                "    onbox.base.dir.skipResolution: true",
                "    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: c:\\echoArg.bat",
                "      classpath://org/apache/brooklyn/camp/brooklyn/echoMyArg.ps1: c:\\echoMyArg.ps1",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.bat: c:\\exit0.bat",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.bat: c:\\exit1.bat",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit0.ps1: c:\\exit0.ps1",
                "      classpath://org/apache/brooklyn/camp/brooklyn/exit1.ps1: c:\\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:");
            Entities.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:");
            Entities.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;
            }
        }
    }

    protected void assertStreams(SoftwareProcess entity, Map<String, List<String>> stdouts) {
        Set<Task<?>> tasks = BrooklynTaskTags.getTasksInEntityContext(mgmt().getExecutionManager(), entity);

        for (Map.Entry<String, List<String>> entry : stdouts.entrySet()) {
            String taskNameRegex = entry.getKey();
            List<String> expectedOuts = entry.getValue();

            Task<?> subTask = findTaskOrSubTask(tasks, TaskPredicates.displayNameSatisfies(StringPredicates.matchesRegex(taskNameRegex))).get();

            String stdin = getStreamOrFail(subTask, BrooklynTaskTags.STREAM_STDIN);
            String stdout = getStreamOrFail(subTask, BrooklynTaskTags.STREAM_STDOUT);
            String stderr = getStreamOrFail(subTask, BrooklynTaskTags.STREAM_STDERR);
            String env = getStream(subTask, BrooklynTaskTags.STREAM_ENV);
            String msg = "stdin="+stdin+"; stdout="+stdout+"; stderr="+stderr+"; env="+env;

            for (String expectedOut : expectedOuts) {
                assertTrue(stdout.contains(expectedOut), msg);
            }
        }
    }

    protected void assertSubTaskFailures(SoftwareProcess entity, Map<String, Predicate<CharSequence>> taskErrs) throws Exception {
        Set<Task<?>> tasks = BrooklynTaskTags.getTasksInEntityContext(mgmt().getExecutionManager(), entity);

        for (Map.Entry<String, Predicate<CharSequence>> entry : taskErrs.entrySet()) {
            String taskNameRegex = entry.getKey();
            Predicate<? super String> errChecker = entry.getValue();
            Task<?> subTask = findTaskOrSubTask(tasks, TaskPredicates.displayNameSatisfies(StringPredicates.matchesRegex(taskNameRegex))).get();
            String msg = "regex="+taskNameRegex+"; task="+subTask;
            assertNotNull(subTask, msg);
            assertTrue(subTask.isDone(), msg);
            assertTrue(subTask.isError(), msg);
            try {
                subTask.get();
                fail();
            } catch (Exception e) {
                if (!errChecker.apply(e.toString())) {
                    throw e;
                }
            }
        }
    }

    public static String getStreamOrFail(Task<?> task, String streamType) {
        String msg = "task="+task+"; stream="+streamType;
        BrooklynTaskTags.WrappedStream stream = checkNotNull(BrooklynTaskTags.stream(task, streamType), "Stream null: " + msg);
        return checkNotNull(stream.streamContents.get(), "Contents null: "+msg);
    }

    public static String getStream(Task<?> task, String streamType) {
        BrooklynTaskTags.WrappedStream stream = BrooklynTaskTags.stream(task, streamType);
        return (stream != null) ? stream.streamContents.get() : null;
    }

    protected Optional<Task<?>> findTaskOrSubTask(Iterable<? extends Task<?>> tasks, Predicate<? super Task<?>> matcher) {
        List<String> taskNames = Lists.newArrayList();
        Optional<Task<?>> result = findTaskOrSubTaskImpl(tasks, matcher, taskNames);
        if (!result.isPresent() && log.isDebugEnabled()) {
            log.debug("Task not found matching "+matcher+"; contender names were "+taskNames);
        }
        return result;
    }

    protected Optional<Task<?>> findTaskOrSubTaskImpl(Iterable<? extends Task<?>> tasks, Predicate<? super Task<?>> matcher, List<String> taskNames) {
        for (Task<?> task : tasks) {
            if (matcher.apply(task)) return Optional.<Task<?>>of(task);

            if (task instanceof HasTaskChildren) {
                Optional<Task<?>> subResult = findTaskOrSubTask(((HasTaskChildren) task).getChildren(), matcher);
                if (subResult.isPresent()) return subResult;
            }
        }

        return Optional.<Task<?>>absent();
    }

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