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

import static org.testng.Assert.assertTrue;

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

import org.apache.brooklyn.api.location.NoMachinesAvailableException;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.location.jclouds.JcloudsLocation;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

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

public abstract class AbstractJcloudsLocationTest {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractJcloudsLocationTest.class);

    private final String provider;

    protected JcloudsLocation loc;
    protected List<SshMachineLocation> machines = MutableList.of();
    
    // NB made private in 090-SNAPSHOT so that non-live tests aren't forced to get a live context
    // (adding 10+ seconds to the build)
    //
    // use mgmt() instead to access, it is populated on demand
    private ManagementContext ctx;

    protected AbstractJcloudsLocationTest(String provider) {
        this.provider = provider;
    }

    protected synchronized ManagementContext mgmt() {
    	if (ctx==null) {
    		useMgmt(newLiveManagementContext());
    	}
    	return ctx;
    }
    
    protected synchronized void useMgmt(ManagementContext mgmt) {
    	if (ctx!=null) {
    		throw new IllegalStateException("Must shutdown old management first");
    	}
    	ctx = mgmt;
    }
    
    protected ManagementContext newLiveManagementContext() {
        BrooklynProperties props = BrooklynProperties.Factory.newDefault().addFromMap(ImmutableMap.of("provider", provider));
        return Entities.newManagementContext(props.asMapWithStringKeys());
	}

    protected ManagementContext newMockManagementContext() {
        BrooklynProperties props = BrooklynProperties.Factory.newDefault().addFromMap(ImmutableMap.of("provider", provider));
        return LocalManagementContextForTests.newInstance(props);
	}

	/**
     * The location and image id tuplets to test.
     */
    @DataProvider(name = "fromImageId")
    public abstract Object[][] cloudAndImageIds();

    /**
     * A single location and image id tuplet to test.
     */
    @DataProvider(name = "fromFirstImageId")
    public Object[][] cloudAndImageFirstId() {
        Object[][] all = cloudAndImageIds();
        return (all != null) ? new Object[][] { all[0] } : new Object[][] { };
    }

    /**
     * The location and image name pattern tuplets to test.
     */
    @DataProvider(name = "fromImageNamePattern")
    public abstract Object[][] cloudAndImageNamePatterns();

    /**
     * The location, image pattern and image owner tuplets to test.
     */
    @DataProvider(name = "fromImageDescriptionPattern")
    public abstract Object[][] cloudAndImageDescriptionPatterns();

    @BeforeMethod(alwaysRun=true)
    public void setUp() {
    }

    @AfterMethod(alwaysRun=true)
    public void tearDown() {
        List<Exception> exceptions = Lists.newArrayList();
        for (SshMachineLocation machine : machines) {
            try {
                loc.release(machine);
            } catch (Exception e) {
                LOG.warn("Error releasing {}: {}; continuing...", machine, e.getMessage());
                exceptions.add(e);
            }
        }
        if (!exceptions.isEmpty()) {
            LOG.info("Exception during tearDown: {}", Exceptions.collapseText(exceptions.get(0)));
        }
        machines.clear();
        
        if (ctx != null) {
        	Entities.destroyAllCatching(ctx);
        	ctx = null;
        }
    }

    @Test(dataProvider="fromImageId")
    public void testTagMapping(String regionName, String imageId, String imageOwner) {
    	useMgmt(newMockManagementContext());
    	
        Map<String, Object> dummy = ImmutableMap.<String, Object>of("identity", "DUMMY", "credential", "DUMMY");
        loc = (JcloudsLocation) mgmt().getLocationRegistry().getLocationManaged(provider + (regionName == null ? "" : ":" + regionName), dummy);
        ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder().put("imageId", imageId);
        if (imageOwner != null) builder.put("imageOwner", imageOwner);
        Map<String, Object> tagMapping = builder.build();
        loc.setTagMapping(ImmutableMap.<String, Map<String, ? extends Object>>of("MyEntityType", tagMapping));

        Map<String, Object> flags = loc.getProvisioningFlags(ImmutableList.of("MyEntityType"));
        assertTrue(Maps.<String, Object>difference(flags, tagMapping).entriesOnlyOnRight().isEmpty(), "flags="+flags);
    }

    @Test(groups = "Live", dataProvider="fromImageId")
    public void testProvisionVmUsingImageId(String regionName, String imageId, String imageOwner) {
        loc = (JcloudsLocation) mgmt().getLocationRegistry().getLocationManaged(provider + (regionName == null ? "" : ":" + regionName));
        SshMachineLocation machine = obtainMachine(MutableMap.of("imageId", imageId, "imageOwner", imageOwner, JcloudsLocation.MACHINE_CREATE_ATTEMPTS, 2));

        LOG.info("Provisioned {} vm {}; checking if ssh'able", provider, machine);
        assertTrue(machine.isSshable());
    }
    
    @Test(groups = "Live", dataProvider="fromImageNamePattern")
    public void testProvisionVmUsingImageNamePattern(String regionName, String imageNamePattern, String imageOwner) {
        loc = (JcloudsLocation) mgmt().getLocationRegistry().getLocationManaged(provider + (regionName == null ? "" : ":" + regionName));
        SshMachineLocation machine = obtainMachine(MutableMap.of("imageNameRegex", imageNamePattern, "imageOwner", imageOwner, JcloudsLocation.MACHINE_CREATE_ATTEMPTS, 2));
        
        LOG.info("Provisioned {} vm {}; checking if ssh'able", provider, machine);
        assertTrue(machine.isSshable());
    }
    
    @Test(groups = "Live", dataProvider="fromImageDescriptionPattern")
    public void testProvisionVmUsingImageDescriptionPattern(String regionName, String imageDescriptionPattern, String imageOwner) {
        loc = (JcloudsLocation) mgmt().getLocationRegistry().getLocationManaged(provider + (regionName == null ? "" : ":" + regionName));
        SshMachineLocation machine = obtainMachine(MutableMap.of("imageDescriptionRegex", imageDescriptionPattern, "imageOwner", imageOwner, JcloudsLocation.MACHINE_CREATE_ATTEMPTS, 2));
        
        LOG.info("Provisioned {} vm {}; checking if ssh'able", provider, machine);
        assertTrue(machine.isSshable());
    }

    // Use this utility method to ensure machines are released on tearDown
    protected SshMachineLocation obtainMachine(Map<?,?> flags) {
        try {
            SshMachineLocation result = (SshMachineLocation)loc.obtain(flags);
            machines.add(result);
            return result;
        } catch (NoMachinesAvailableException nmae) {
            LOG.warn("No machines available", nmae);
            throw Exceptions.propagate(nmae);
        }
    }
    
    protected SshMachineLocation release(SshMachineLocation machine) {
        machines.remove(machine);
        loc.release(machine);
        return machine;
    }
}
