/*
 * 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.location.jclouds;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

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

import org.apache.brooklyn.util.core.internal.winrm.WinRmToolResponse;
import org.apache.brooklyn.util.exceptions.CompoundRuntimeException;
import org.apache.brooklyn.api.location.MachineLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.location.winrm.WinRmMachineLocation;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

public class AbstractJcloudsLiveTest {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractJcloudsLiveTest.class);

    public static final String BROOKLYN_PROPERTIES_PREFIX = "brooklyn.location.jclouds.";
    public static final String BROOKLYN_PROPERTIES_LEGACY_PREFIX = "brooklyn.jclouds.";
    
    public static final String AWS_EC2_PROVIDER = "aws-ec2";
    public static final String AWS_EC2_MICRO_HARDWARE_ID = "t1.micro";
    public static final String AWS_EC2_SMALL_HARDWARE_ID = "m1.small";
    public static final String AWS_EC2_MEDIUM_HARDWARE_ID = "m3.medium";
    public static final String AWS_EC2_EUWEST_REGION_NAME = "eu-west-1";
    public static final String AWS_EC2_USEAST_REGION_NAME = "us-east-1";
    public static final String AWS_EC2_SINGAPORE_REGION_NAME = "ap-southeast-1";

    public static final String RACKSPACE_PROVIDER = "rackspace-cloudservers-uk";
    
    public static final String SOFTLAYER_PROVIDER = "softlayer";
    public static final String SOFTLAYER_AMS01_REGION_NAME = "ams01";
    
    public static final String GCE_PROVIDER = "google-compute-engine";
    public static final String GCE_USCENTRAL_REGION_NAME = "us-central1-a";
    
    protected BrooklynProperties brooklynProperties;
    protected LocalManagementContext managementContext;
    
    protected List<JcloudsMachineLocation> machines;
    protected JcloudsLocation jcloudsLocation;
    
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        machines = Lists.newCopyOnWriteArrayList();
        managementContext = newManagementContext();
        
        // Don't let any defaults from brooklyn.properties (except credentials) interfere with test
        brooklynProperties = managementContext.getBrooklynProperties();
        stripBrooklynProperties(brooklynProperties);
    }

    @AfterMethod(alwaysRun=true)
    public void tearDown() throws Exception {
        List<Exception> exceptions = Lists.newArrayList();
        try {
            if (machines != null) {
                exceptions.addAll(releaseMachineSafely(machines));
                machines.clear();
            }
        } finally {
            try {
                if (managementContext != null) Entities.destroyAll(managementContext);
            } catch (Exception e) {
                LOG.warn("Error destroying management context", e);
                exceptions.add(e);
            }
            managementContext = null;
        }
        
        // TODO Debate about whether to:
        //  - use destroyAllCatching (i.e. not propagating exception)
        //    Benefit is that other tests in class will subsequently be run, rather than bailing out.
        //  - propagate exceptions from tearDown
        //    Benefit is that we don't hide errors; release(...) etc should not be throwing exceptions.
        if (exceptions.size() > 0) {
            throw new CompoundRuntimeException("Error in tearDown of "+getClass(), exceptions);
        }
    }

    protected LocalManagementContext mgmt() {
        return managementContext;
    }
    
    protected LocalManagementContext newManagementContext() {
        // loads properties, by default, but not OSGi or anything else
        return LocalManagementContextForTests.builder(true).useDefaultProperties().build();
    }
    
    public static void stripBrooklynProperties(BrooklynProperties props) {
        // remove all location properties except for identity and credential
        // (so key, scripts, etc settings don't interfere with tests) 
        for (String key : ImmutableSet.copyOf(props.asMapWithStringKeys().keySet())) {
            if (key.startsWith(BROOKLYN_PROPERTIES_PREFIX) && !(key.endsWith("identity") || key.endsWith("credential"))) {
                props.remove(key);
            }
            if (key.startsWith(BROOKLYN_PROPERTIES_LEGACY_PREFIX) && !(key.endsWith("identity") || key.endsWith("credential"))) {
                props.remove(key);
            }
            
            // Also removes scriptHeader (e.g. if doing `. ~/.bashrc` and `. ~/.profile`, then that can cause "stdin: is not a tty")
            if (key.startsWith("brooklyn.ssh")) {
                props.remove(key);
            }
        }
    }
    
    protected void assertSshable(SshMachineLocation machine) {
        int result = machine.execScript("simplecommand", ImmutableList.of("true"));
        assertEquals(result, 0);
    }

    protected void assertWinrmable(WinRmMachineLocation machine) {
        // Assumes that tests are letting Brooklyn or the cloud auto-generate the password, so ok to log it.
        // If the assertion fails, we want to know that it had a plausible-looking password etc.
        WinRmToolResponse result;
        try {
            result = machine.executeCommand(ImmutableList.of("echo mySimpleWinrmCmd"));
        } catch (Exception e) {
            throw new RuntimeException("Error executing WinRM command on " + machine + " with config " + machine.config().getAllLocalRaw(), e);
        }
        assertEquals(result.getStatusCode(), 0, "stdout="+result.getStdOut()+"; stderr="+result.getStdErr());
    }

    // Use this utility method to ensure machines are released on tearDown
    protected JcloudsSshMachineLocation obtainMachine(Map<?, ?> conf) throws Exception {
        assertNotNull(jcloudsLocation);
        JcloudsSshMachineLocation result = (JcloudsSshMachineLocation)jcloudsLocation.obtain(conf);
        machines.add(checkNotNull(result, "result"));
        return result;
    }

    protected JcloudsSshMachineLocation obtainMachine() throws Exception {
        return obtainMachine(ImmutableMap.of());
    }
    
    protected JcloudsWinRmMachineLocation obtainWinrmMachine(Map<?, ?> conf) throws Exception {
        assertNotNull(jcloudsLocation);
        JcloudsWinRmMachineLocation result = (JcloudsWinRmMachineLocation)jcloudsLocation.obtain(conf);
        machines.add(checkNotNull(result, "result"));
        return result;
    }

    // Use this utility method to ensure machines are released on tearDown
    protected void releaseMachine(JcloudsMachineLocation machine) {
        assertNotNull(jcloudsLocation);
        machines.remove(machine);
        jcloudsLocation.release(machine);
    }
    
    protected List<Exception> releaseMachineSafely(Iterable<? extends JcloudsMachineLocation> machines) {
        List<Exception> exceptions = Lists.newArrayList();
        
        for (JcloudsMachineLocation machine : machines) {
            try {
                releaseMachine(machine);
            } catch (Exception e) {
                LOG.warn("Error releasing machine "+machine+"; continuing...", e);
                exceptions.add(e);
            }
        }
        return exceptions;
    }

    protected void suspendMachine(MachineLocation machine) {
        assertNotNull(jcloudsLocation);
        machines.remove(machine);
        jcloudsLocation.suspendMachine(machine);
    }

    protected MachineLocation resumeMachine(Map<?, ?> flags) {
        assertNotNull(jcloudsLocation);
        JcloudsMachineLocation location = jcloudsLocation.resumeMachine(flags);
        machines.add(location);
        return location;
    }

}
