/*
 * 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 java.util.Collection;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;

import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.MachineLocation;
import org.apache.brooklyn.api.location.MachineLocationCustomizer;
import org.apache.brooklyn.api.location.NoMachinesAvailableException;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.location.LocationConfigKeys;
import org.apache.brooklyn.core.location.cloud.names.CustomMachineNamer;
import org.apache.brooklyn.core.location.geo.HostGeoInfo;
import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.http.HttpAsserts;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.scriptbuilder.domain.OsFamily;
import org.jclouds.scriptbuilder.domain.StatementList;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ContiguousSet;
import com.google.common.collect.DiscreteDomain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Range;
import com.google.common.primitives.Ints;
import com.google.common.reflect.TypeToken;

/**
 * @author Shane Witbeck
 */
public class JcloudsLocationTest implements JcloudsLocationConfig {

    private static final Logger log = LoggerFactory.getLogger(JcloudsLocationTest.class);
    
    public static Predicate<ConfigBag> checkerFor(final String user, final Integer minRam, final Integer minCores) {
        return new Predicate<ConfigBag>() {
            @Override
            public boolean apply(@Nullable ConfigBag input) {
                Assert.assertEquals(input.get(USER), user);
                Assert.assertEquals(input.get(MIN_RAM), minRam);
                Assert.assertEquals(input.get(MIN_CORES), minCores);
                return true;
            }
        };
    }
    
    public static Predicate<ConfigBag> templateCheckerFor(final String ports) {
        return new Predicate<ConfigBag>() {
            @Override
            public boolean apply(@Nullable ConfigBag input) {
                Assert.assertEquals(input.get(INBOUND_PORTS), ports);
                return false;
            }
        };
    }
    
    private LocalManagementContext managementContext;
    
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        managementContext = LocalManagementContextForTests.newInstance(BrooklynProperties.Factory.newDefault());
    }
    
    @AfterMethod(alwaysRun=true)
    public void tearUp() throws Exception {
        if (managementContext != null) Entities.destroyAll(managementContext);
    }

    @Test
    public void testCreateWithFlagsDirectly() throws Exception {
        BailOutJcloudsLocation jcl = BailOutJcloudsLocation.newBailOutJcloudsLocation(managementContext);
        jcl.tryObtainAndCheck(MutableMap.of(MIN_CORES, 2), checkerFor("fred", 16, 2));
    }

    @Test
    public void testCreateWithFlagsDirectlyAndOverride() throws Exception {
        BailOutJcloudsLocation jcl = BailOutJcloudsLocation.newBailOutJcloudsLocation(managementContext);
        jcl.tryObtainAndCheck(MutableMap.of(MIN_CORES, 2, MIN_RAM, 8), checkerFor("fred", 8, 2));
    }

    @Test
    public void testCreateWithFlagsSubLocation() throws Exception {
        BailOutJcloudsLocation jcl = BailOutJcloudsLocation.newBailOutJcloudsLocation(managementContext);
        jcl = (BailOutJcloudsLocation) jcl.newSubLocation(MutableMap.of(USER, "jon", MIN_CORES, 2));
        jcl.tryObtainAndCheck(MutableMap.of(MIN_CORES, 3), checkerFor("jon", 16, 3));
    }

    @Test
    public void testSingleInttoIntPortArray() {
        int port = 1;
        int[] intArray = new int[] {1};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(port), intArray);
    }

    @Test
    public void testSingleStringtoIntPortArray() {
        String portString = "1";
        int[] intArray = new int[] {1};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(portString), intArray);
    }

    @Test
    public void testStringListWithBracketstoIntPortArray() {
        String listString = "[1, 2, 3, 4]";
        int[] intArray = new int[] {1, 2, 3, 4};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(listString), intArray);
    }

    @Test
    public void testStringListWithoutBracketstoIntPortArray() {
        String listString = "1, 2, 3, 4";
        int[] intArray = new int[] {1, 2, 3, 4};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(listString), intArray);
    }

    @Test
    public void testEmptyStringListtoIntPortArray() {
        String listString = "[]";
        int[] intArray = new int[] {};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(listString), intArray);
    }

    @Test
    public void testIntArraytoIntPortArray() {
        int[] intArray = new int[] {1, 2, 3, 4};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(intArray), intArray);
    }

    @Test
    public void testObjectArrayOfIntegerstoIntPortArray() {
        Object[] integerObjectArray = new Object[] {1, 2, 3, 4};
        int[] intArray = new int[] {1, 2, 3, 4};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(integerObjectArray), intArray);
    }

    @Test
    public void testObjectArrayOfStringstoIntPortArray() {
        Object[] stringObjectArray = new Object[] {"1", "2", "3", "4"};
        int[] intArray = new int[] {1, 2, 3, 4};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(stringObjectArray), intArray);
    }

    @Test
    public void testStringArraytoIntPortArray() {
        String[] stringArray = new String[] {"1", "2", "3"};
        int[] intArray = new int[] {1, 2, 3};
        Assert.assertEquals(JcloudsLocation.toIntPortArray(stringArray), intArray);
    }

    @Test
    public void testStringPortRangetoIntPortArray() {
        String portRange = "1-100";
        int[] intArray = Ints.toArray(ContiguousSet.create(Range.closed(1, 100), DiscreteDomain.integers()));
        Assert.assertEquals(intArray, JcloudsLocation.toIntPortArray(portRange));
    }

    @Test
    public void testStringPortPlustoIntPortArray() {
        String portPlus = "100+";
        int[] intArray = Ints.toArray(ContiguousSet.create(Range.closed(100, 65535), DiscreteDomain.integers()));
        Assert.assertEquals(intArray, JcloudsLocation.toIntPortArray(portPlus));
    }

    @Test
    public void testCombinationOfInputstoIntPortArray() {
        Collection<Object> portInputs = Lists.newLinkedList();
        portInputs.add(1);
        portInputs.add("2");
        portInputs.add("3-100");
        portInputs.add("101,102,103");
        portInputs.add("[104,105,106]");
        portInputs.add(new int[] {107, 108, 109});
        portInputs.add(new String[] {"110", "111", "112"});
        portInputs.add(new Object[] {113, 114, 115});

        int[] intArray = Ints.toArray(ContiguousSet.create(Range.closed(1, 115), DiscreteDomain.integers()));
        Assert.assertEquals(intArray, JcloudsLocation.toIntPortArray(portInputs));
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testMalformedStringNumbertoIntPortArray() {
        String numberStr = "1i";
        JcloudsLocation.toIntPortArray(numberStr);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testMalformedStringRangetoIntPortArray() {
        String rangeString = "1-";
        JcloudsLocation.toIntPortArray(rangeString);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testMalformedStringListWithBracketstoIntPortArray() {
        String listString = "[1,2,e]";
        JcloudsLocation.toIntPortArray(listString);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testMalformedStringListWithoutBracketstoIntPortArray() {
        String listString = "1,2,e";
        JcloudsLocation.toIntPortArray(listString);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testMalformedStringArraytoIntPortArray() {
        String[] stringArray = new String[] {"1", "2", "e"};
        JcloudsLocation.toIntPortArray(stringArray);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    public void testIllegalObjectArrayOfDoublestoIntPortArray() {
        Object[] doubleObjectArray = new Object[] {1.0, 2.0, 3.0};
        JcloudsLocation.toIntPortArray(doubleObjectArray);
    }

    @Test
    public void testVMCreationIsRetriedOnFailure() {
        final AtomicInteger count = new AtomicInteger();
        Function<ConfigBag, Void> countingInterceptor = new Function<ConfigBag, Void>() {
            @Override public Void apply(ConfigBag input) {
                count.incrementAndGet();
                return null;
            }
        };
        BailOutJcloudsLocation loc = BailOutJcloudsLocation.newBailOutJcloudsLocation(managementContext, ImmutableMap.<ConfigKey<?>, Object>of(
                MACHINE_CREATE_ATTEMPTS, 3,
                BailOutJcloudsLocation.BUILD_TEMPLATE_INTERCEPTOR, countingInterceptor));
        loc.tryObtain();
        Assert.assertEquals(count.get(), 3);
    }

    @Test(groups={"Live", "Live-sanity"})
    public void testCreateWithInboundPorts() {
        BailOutJcloudsLocation jcloudsLocation = BailOutJcloudsLocation.newBailOutJcloudsLocationForLiveTest(managementContext);
        jcloudsLocation = (BailOutJcloudsLocation) jcloudsLocation.newSubLocation(MutableMap.of());
        jcloudsLocation.tryObtainAndCheck(MutableMap.of(), templateCheckerFor("[22, 80, 9999]"));
        int[] ports = new int[] {22, 80, 9999};
        Assert.assertEquals(jcloudsLocation.getTemplate().getOptions().getInboundPorts(), ports);
    }
    
    @Test(groups={"Live", "Live-sanity"})
    public void testCreateWithInboundPortsOverride() {
        BailOutJcloudsLocation jcloudsLocation = BailOutJcloudsLocation.newBailOutJcloudsLocationForLiveTest(managementContext);
        jcloudsLocation = (BailOutJcloudsLocation) jcloudsLocation.newSubLocation(MutableMap.of());
        jcloudsLocation.tryObtainAndCheck(MutableMap.of(INBOUND_PORTS, "[23, 81, 9998]"), templateCheckerFor("[23, 81, 9998]"));
        int[] ports = new int[] {23, 81, 9998};
        Assert.assertEquals(jcloudsLocation.getTemplate().getOptions().getInboundPorts(), ports);
    }

    @Test
    public void testCreateWithMaxConcurrentCallsUnboundedByDefault() throws Exception {
        final int numCalls = 20;
        ConcurrencyTracker interceptor = new ConcurrencyTracker();
        ExecutorService executor = Executors.newCachedThreadPool();

        try {
            final BailOutJcloudsLocation jcloudsLocation = BailOutJcloudsLocation.newBailOutJcloudsLocation(
                    managementContext, ImmutableMap.<ConfigKey<?>, Object>of(
                            BailOutJcloudsLocation.BUILD_TEMPLATE_INTERCEPTOR, interceptor));
            for (int i = 0; i < numCalls; i++) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        jcloudsLocation.tryObtain();
                    }
                });
            }
            interceptor.assertCallCountEventually(numCalls);
            interceptor.unblock();
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);
        } finally {
            executor.shutdownNow();
        }
    }

    @Test(groups="Integration") // because takes 1 sec
    public void testCreateWithMaxConcurrentCallsRespectsConfig() throws Exception {
        final int numCalls = 4;
        final int maxConcurrentCreations = 2;
        ConcurrencyTracker interceptor = new ConcurrencyTracker();
        ExecutorService executor = Executors.newCachedThreadPool();
        
        try {
            final BailOutJcloudsLocation jcloudsLocation = BailOutJcloudsLocation.newBailOutJcloudsLocation(
                    managementContext, ImmutableMap.of(
                            BailOutJcloudsLocation.BUILD_TEMPLATE_INTERCEPTOR, interceptor,
                            MAX_CONCURRENT_MACHINE_CREATIONS, maxConcurrentCreations));

            for (int i = 0; i < numCalls; i++) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        jcloudsLocation.tryObtain();
                    }
                });
            }

            interceptor.assertCallCountEventually(maxConcurrentCreations);
            interceptor.assertCallCountContinually(maxConcurrentCreations);

            interceptor.unblock();
            interceptor.assertCallCountEventually(numCalls);
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);

        } finally {
            executor.shutdownNow();
        }
    }

    @Test(groups="Integration") // because takes 1 sec
    public void testCreateWithMaxConcurrentCallsAppliesToSubLocations() throws Exception {
        final int numCalls = 4;
        final int maxConcurrentCreations = 2;
        ConcurrencyTracker interceptor = new ConcurrencyTracker();
        ExecutorService executor = Executors.newCachedThreadPool();

        try {
            final BailOutJcloudsLocation jcloudsLocation = BailOutJcloudsLocation.newBailOutJcloudsLocation(
                    managementContext, ImmutableMap.of(
                            BailOutJcloudsLocation.BUILD_TEMPLATE_INTERCEPTOR, interceptor,
                            MAX_CONCURRENT_MACHINE_CREATIONS, maxConcurrentCreations));

            for (int i = 0; i < numCalls; i++) {
                final BailOutJcloudsLocation subLocation = (BailOutJcloudsLocation) jcloudsLocation.newSubLocation(MutableMap.of());
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        subLocation.tryObtain();
                    }
                });
            }

            interceptor.assertCallCountEventually(maxConcurrentCreations);
            interceptor.assertCallCountContinually(maxConcurrentCreations);

            interceptor.unblock();
            interceptor.assertCallCountEventually(numCalls);
            executor.shutdown();
            executor.awaitTermination(10, TimeUnit.SECONDS);

        } finally {
            executor.shutdownNow();
        }
    }
    
    @Test
    public void testCreateWithCustomMachineNamer() {
        final String machineNamerClass = CustomMachineNamer.class.getName();
        BailOutJcloudsLocation jcloudsLocation = BailOutJcloudsLocation.newBailOutJcloudsLocation(
                managementContext, ImmutableMap.<ConfigKey<?>, Object>of(
                        LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS, machineNamerClass));
        jcloudsLocation.tryObtainAndCheck(ImmutableMap.of(CustomMachineNamer.MACHINE_NAME_TEMPLATE, "ignored"), new Predicate<ConfigBag>() {
            @Override
            public boolean apply(ConfigBag input) {
                Assert.assertEquals(input.get(LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS), machineNamerClass);
                return true;
            }
        });
    }
    
    @Test
    public void testCreateWithCustomMachineNamerOnObtain() {
        final String machineNamerClass = CustomMachineNamer.class.getName();
        BailOutJcloudsLocation jcloudsLocation = BailOutJcloudsLocation.newBailOutJcloudsLocation(managementContext);
        ImmutableMap<ConfigKey<String>, String> flags = ImmutableMap.of(
                CustomMachineNamer.MACHINE_NAME_TEMPLATE, "ignored",
                LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS, machineNamerClass);
        jcloudsLocation.tryObtainAndCheck(flags, new Predicate<ConfigBag>() {
            @Override
            public boolean apply(ConfigBag input) {
                Assert.assertEquals(input.get(LocationConfigKeys.CLOUD_MACHINE_NAMER_CLASS), machineNamerClass);
                return true;
            }
        });
    }

    public static class ConcurrencyTracker implements Function<ConfigBag,Void> {
        final AtomicInteger concurrentCallsCounter = new AtomicInteger();
        final CountDownLatch continuationLatch = new CountDownLatch(1);
        
        @Override public Void apply(ConfigBag input) {
            concurrentCallsCounter.incrementAndGet();
            try {
                continuationLatch.await();
            } catch (InterruptedException e) {
                throw Exceptions.propagate(e);
            }
            return null;
        }
        
        public void unblock() {
            continuationLatch.countDown();
        }

        public void assertCallCountEventually(final int expected) {
            Asserts.succeedsEventually(new Runnable() {
                @Override public void run() {
                    Assert.assertEquals(concurrentCallsCounter.get(), expected);
                }
            });
        }
        
        public void assertCallCountContinually(final int expected) {
            Asserts.succeedsContinually(new Runnable() {
                @Override public void run() {
                    Assert.assertEquals(concurrentCallsCounter.get(), expected);
                }
            });
        }
    }

    
    @SuppressWarnings("serial")
    public static class FakeLocalhostWithParentJcloudsLocation extends JcloudsLocation {
        public static final ConfigKey<Function<ConfigBag,Void>> BUILD_TEMPLATE_INTERCEPTOR = ConfigKeys.newConfigKey(new TypeToken<Function<ConfigBag,Void>>() {}, "buildtemplateinterceptor");
        
        ConfigBag lastConfigBag;

        public FakeLocalhostWithParentJcloudsLocation() {
            super();
        }

        public FakeLocalhostWithParentJcloudsLocation(Map<?, ?> conf) {
            super(conf);
        }

        @Override
        public JcloudsSshMachineLocation obtain(Map<?, ?> flags) throws NoMachinesAvailableException {
            JcloudsSshMachineLocation result = getManagementContext().getLocationManager().createLocation(LocationSpec.create(JcloudsSshMachineLocation.class)
                .configure("address", "127.0.0.1") 
                .configure("port", 22) 
                .configure("user", "bob")
                .configure("jcloudsParent", this)
                .configure("nodeId", "myNodeId")
                .configure("imageId", "myImageId")
                .configure("privateAddresses", ImmutableSet.of("10.0.0.1"))
                .configure("privateHostname", "10.0.0.1")
                .configure("publicAddresses", ImmutableSet.of("56.0.0.1"))
                );
            registerJcloudsMachineLocation("bogus", result);
            
            // explicitly invoke this customizer, to comply with tests
            for (JcloudsLocationCustomizer customizer : getCustomizers(config().getBag())) {
                customizer.customize(this, null, result);
            }
            for (MachineLocationCustomizer customizer : getMachineCustomizers(config().getBag())) {
                customizer.customize(result);
            }

            return result;
        }
        
        @Override
        protected void releaseNode(String instanceId) {
            // no-op
        }
    }

    @Test
    public void testInheritsGeo() throws Exception {
        ConfigBag allConfig = ConfigBag.newInstance()
            .configure(IMAGE_ID, "bogus")
            .configure(CLOUD_PROVIDER, "aws-ec2")
            .configure(CLOUD_REGION_ID, "bogus")
            .configure(ACCESS_IDENTITY, "bogus")
            .configure(ACCESS_CREDENTIAL, "bogus")
            .configure(LocationConfigKeys.LATITUDE, 42d)
            .configure(LocationConfigKeys.LONGITUDE, -20d)
            .configure(MACHINE_CREATE_ATTEMPTS, 1);
        FakeLocalhostWithParentJcloudsLocation ll = managementContext.getLocationManager().createLocation(LocationSpec.create(FakeLocalhostWithParentJcloudsLocation.class).configure(allConfig.getAllConfig()));
        MachineLocation l = ll.obtain();
        log.info("loc:" +l);
        HostGeoInfo geo = HostGeoInfo.fromLocation(l);
        log.info("geo: "+geo);
        Assert.assertEquals(geo.latitude, 42d, 0.00001);
        Assert.assertEquals(geo.longitude, -20d, 0.00001);
    }

    @Test
    public void testInheritsGeoFromLocationMetadataProperties() throws Exception {
        // in location-metadata.properties:
//        brooklyn.location.jclouds.softlayer@wdc01.latitude=38.909202
//        brooklyn.location.jclouds.softlayer@wdc01.longitude=-77.47314
        ConfigBag allConfig = ConfigBag.newInstance()
            .configure(IMAGE_ID, "bogus")
            .configure(CLOUD_PROVIDER, "softlayer")
            .configure(CLOUD_REGION_ID, "wdc01")
            .configure(ACCESS_IDENTITY, "bogus")
            .configure(ACCESS_CREDENTIAL, "bogus")
            .configure(MACHINE_CREATE_ATTEMPTS, 1);
        Map<String, Object> brooklynProperties = managementContext.getBrooklynProperties().asMapWithStringKeys();
		FakeLocalhostWithParentJcloudsLocation ll = managementContext.getLocationManager().createLocation(LocationSpec.create(FakeLocalhostWithParentJcloudsLocation.class)
            .configure(new JcloudsPropertiesFromBrooklynProperties().getJcloudsProperties("softlayer", "wdc01", null, brooklynProperties))
            .configure(allConfig.getAllConfig()));
        MachineLocation l = ll.obtain();
        log.info("loc:" +l);
        HostGeoInfo geo = HostGeoInfo.fromLocation(l);
        log.info("geo: "+geo);
        Assert.assertEquals(geo.latitude, 38.909202d, 0.00001);
        Assert.assertEquals(geo.longitude, -77.47314d, 0.00001);
    }

    @Test
    public void testInvokesCustomizerCallbacks() throws Exception {
        JcloudsLocationCustomizer customizer = Mockito.mock(JcloudsLocationCustomizer.class);
        MachineLocationCustomizer machineCustomizer = Mockito.mock(MachineLocationCustomizer.class);
//        Mockito.when(customizer.customize(Mockito.any(JcloudsLocation.class), Mockito.any(ComputeService.class), Mockito.any(JcloudsSshMachineLocation.class)));
        ConfigBag allConfig = ConfigBag.newInstance()
            .configure(CLOUD_PROVIDER, "aws-ec2")
            .configure(ACCESS_IDENTITY, "bogus")
            .configure(ACCESS_CREDENTIAL, "bogus")
            .configure(JcloudsLocationConfig.JCLOUDS_LOCATION_CUSTOMIZERS, ImmutableList.of(customizer))
            .configure(JcloudsLocation.MACHINE_LOCATION_CUSTOMIZERS, ImmutableList.of(machineCustomizer))
            .configure(MACHINE_CREATE_ATTEMPTS, 1);
        FakeLocalhostWithParentJcloudsLocation ll = managementContext.getLocationManager().createLocation(LocationSpec.create(FakeLocalhostWithParentJcloudsLocation.class).configure(allConfig.getAllConfig()));
        JcloudsMachineLocation l = (JcloudsMachineLocation)ll.obtain();
        Mockito.verify(customizer, Mockito.times(1)).customize(ll, null, l);
        Mockito.verify(customizer, Mockito.never()).preRelease(l);
        Mockito.verify(customizer, Mockito.never()).postRelease(l);
        Mockito.verify(machineCustomizer, Mockito.times(1)).customize(l);
        Mockito.verify(machineCustomizer, Mockito.never()).preRelease(l);
        
        ll.release(l);
        Mockito.verify(customizer, Mockito.times(1)).preRelease(l);
        Mockito.verify(customizer, Mockito.times(1)).postRelease(l);
        Mockito.verify(machineCustomizer, Mockito.times(1)).preRelease(l);
    }

    // now test creating users
    
    protected String getCreateUserStatementsFor(Map<ConfigKey<?>,?> config) {
        BailOutJcloudsLocation jl = BailOutJcloudsLocation.newBailOutJcloudsLocation(
                managementContext, MutableMap.<ConfigKey<?>, Object>builder()
                        .put(JcloudsLocationConfig.LOGIN_USER, "root").put(JcloudsLocationConfig.LOGIN_USER_PASSWORD, "m0ck")
                        .put(JcloudsLocationConfig.USER, "bob").put(JcloudsLocationConfig.LOGIN_USER_PASSWORD, "b0b")
                        .putAll(config).build());

        CreateUserStatements creation = CreateUserStatements.get(jl, null, jl.config().getBag());
        return new StatementList(creation.statements()).render(OsFamily.UNIX);
    }
    
    @Test
    public void testDisablesRoot() {
        String statements = getCreateUserStatementsFor(ImmutableMap.<ConfigKey<?>, Object>of());
        Assert.assertTrue(statements.contains("PermitRootLogin"), "Error:\n"+statements);
        Assert.assertTrue(statements.matches("(?s).*sudoers.*useradd.*bob.*wheel.*"), "Error:\n"+statements);
    }

    @Test
    public void testDisableRootFalse() {
        String statements = getCreateUserStatementsFor(ImmutableMap.<ConfigKey<?>, Object>of(
                JcloudsLocationConfig.DISABLE_ROOT_AND_PASSWORD_SSH, false));
        Assert.assertFalse(statements.contains("PermitRootLogin"), "Error:\n"+statements);
        Assert.assertTrue(statements.matches("(?s).*sudoers.*useradd.*bob.*wheel.*"), "Error:\n"+statements);
    }
    
    @Test
    public void testDisableRootAndSudoFalse() {
        String statements = getCreateUserStatementsFor(ImmutableMap.<ConfigKey<?>, Object>of(
            JcloudsLocationConfig.DISABLE_ROOT_AND_PASSWORD_SSH, false,
            JcloudsLocationConfig.GRANT_USER_SUDO, false));
        Assert.assertFalse(statements.contains("PermitRootLogin"), "Error:\n"+statements);
        Assert.assertFalse(statements.matches("(?s).*sudoers.*useradd.*bob.*wheel.*"), "Error:\n"+statements);
    }

    // TODO more tests, where flags come in from resolver, named locations, etc

    @Test(groups="Integration")
    public void testAwsVpcFailureDocLink() {
        HttpAsserts.assertContentContainsText(JcloudsLocation.AWS_VPC_HELP_URL, "VPC", "Classic");
    }

    @Test
    public void testGetLoginPortOrDefaultReturnsDefaultWhenLoginPortNve() {
        NodeMetadata mock = Mockito.mock(NodeMetadata.class);
        Mockito.when(mock.getLoginPort()).thenReturn(-1);
        Assert.assertEquals(JcloudsLocation.getLoginPortOrDefault(mock,22), 22);
    }

    @Test
    public void testGetLoginPortOrDefaultReturnsPortWhenLoginPortPve() {
        NodeMetadata mock = Mockito.mock(NodeMetadata.class);
        Mockito.when(mock.getLoginPort()).thenReturn(666);
        Assert.assertEquals(JcloudsLocation.getLoginPortOrDefault(mock,22), 666);
    }
}
