/*
 * 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 org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;

import java.io.File;

import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.OsDetails;
import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
import org.apache.brooklyn.core.mgmt.rebind.RebindOptions;
import org.apache.brooklyn.core.mgmt.rebind.RebindTestUtils;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.util.core.ResourceUtils;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.commons.io.FileUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import com.google.common.net.HostAndPort;

public class RebindJcloudsLocationLiveTest extends AbstractJcloudsLiveTest {

    public static final String AWS_EC2_REGION_NAME = AWS_EC2_USEAST_REGION_NAME;
    public static final String AWS_EC2_LOCATION_SPEC = "jclouds:" + AWS_EC2_PROVIDER + ":" + AWS_EC2_REGION_NAME;

    private ClassLoader classLoader = getClass().getClassLoader();
    private File mementoDir;
    private TestApplication origApp;
    
    @BeforeMethod(alwaysRun=true)
    @Override
    public void setUp() throws Exception {
        super.setUp();

        jcloudsLocation = (JcloudsLocation) managementContext.getLocationRegistry().getLocationManaged(AWS_EC2_LOCATION_SPEC);
        jcloudsLocation.config().set(JcloudsLocation.HARDWARE_ID, AWS_EC2_SMALL_HARDWARE_ID);
        
        origApp = TestApplication.Factory.newManagedInstanceForTests(managementContext);
    }

    @AfterMethod(alwaysRun = true)
    @Override
    public void tearDown() throws Exception {
        try {
            super.tearDown();
        } finally {
            if (mementoDir != null) RebindTestUtils.deleteMementoDir(mementoDir);
        }
    }
    
    @Override
    protected LocalManagementContext newManagementContext() {
        mementoDir = Files.createTempDir();
        return RebindTestUtils.newPersistingManagementContext(mementoDir, classLoader, 1);
    }
    
    @Test(groups="Live")
    public void testRebindsToJcloudsMachine() throws Exception {
        LiveTestEntity origEntity = origApp.addChild(EntitySpec.create(LiveTestEntity.class));

        origApp.start(ImmutableList.of(jcloudsLocation));
        JcloudsLocation origJcloudsLocation = jcloudsLocation;
        System.out.println("orig locations: " + origEntity.getLocations());
        JcloudsSshMachineLocation origMachine = (JcloudsSshMachineLocation) Iterables.find(origEntity.getLocations(), Predicates.instanceOf(JcloudsSshMachineLocation.class));

        TestApplication newApp = rebind();
        LiveTestEntity newEntity = (LiveTestEntity) Iterables.find(newApp.getChildren(), Predicates.instanceOf(LiveTestEntity.class));
        JcloudsSshMachineLocation newMachine = (JcloudsSshMachineLocation) Iterables.find(newEntity.getLocations(), Predicates.instanceOf(JcloudsSshMachineLocation.class));

        assertMachineEquals(newMachine, origMachine, true); // Don't expect OsDetails, because that is not persisted.
        assertTrue(newMachine.isSshable());

        JcloudsLocation newJcloudsLoction = newMachine.getParent();
        assertJcloudsLocationEquals(newJcloudsLoction, origJcloudsLocation);
    }

    // TODO In jclouds-azure, the AzureComputeTemplateOptions fields changed, which meant old 
    // persisted state could not be deserialized. These files are examples of the old format.
    @Test(groups={"Live", "WIP"}, enabled=false)
    public void testRebindsToJcloudsMachineWithInvalidTemplate() throws Exception {
        ResourceUtils resourceUtils = ResourceUtils.create(this);
        FileUtils.write(
                new File(mementoDir, "locations/briByOel"),
                resourceUtils.getResourceAsString("classpath://org/apache/brooklyn/location/jclouds/persisted-azure-parent-briByOel"));
        FileUtils.write(
                new File(mementoDir, "locations/VNapYjwp"),
                resourceUtils.getResourceAsString("classpath://org/apache/brooklyn/location/jclouds/persisted-azure-machine-VNapYjwp"));
        
        TestApplication newApp = rebind();
        
        JcloudsLocation loc = (JcloudsLocation) newApp.getManagementContext().getLocationManager().getLocation("briByOel");
        JcloudsSshMachineLocation machine = (JcloudsSshMachineLocation) newApp.getManagementContext().getLocationManager().getLocation("VNapYjwp");
        assertEquals(ImmutableSet.of(loc.getChildren()), ImmutableSet.of(machine));
    }

    @Test(groups={"Live", "Live-sanity"})
    public void testRebindsToJcloudsSshMachineWithTemplateAndNode() throws Exception {
        // Populate the mementoDir with some old-style peristed state
        ResourceUtils resourceUtils = ResourceUtils.create(this);
        String origParentXml = resourceUtils.getResourceAsString("classpath://org/apache/brooklyn/location/jclouds/persisted-aws-parent-lCYB3mTb");
        String origMachineXml = resourceUtils.getResourceAsString("classpath://org/apache/brooklyn/location/jclouds/persisted-aws-machine-aKEcbxKN");
        File persistedParentFile = new File(mementoDir, "locations/lCYB3mTb");
        File persistedMachineFile = new File(mementoDir, "locations/aKEcbxKN");
        FileUtils.write(
                persistedParentFile,
                origParentXml);
        FileUtils.write(
                persistedMachineFile,
                origMachineXml);

        assertTrue(origMachineXml.contains("AWSEC2TemplateOptions"), origMachineXml);
        assertTrue(origMachineXml.contains("NodeMetadataImpl"), origMachineXml);

        // Rebind to the old-style persisted state, which includes the NodeMetadata and the Template objects.
        // Expect to parse that ok.
        TestApplication app2 = rebind();
        
        JcloudsLocation loc2 = (JcloudsLocation) app2.getManagementContext().getLocationManager().getLocation("lCYB3mTb");
        JcloudsSshMachineLocation machine2 = (JcloudsSshMachineLocation) app2.getManagementContext().getLocationManager().getLocation("aKEcbxKN");
        assertEquals(ImmutableSet.copyOf(loc2.getChildren()), ImmutableSet.of(machine2));
        
        String errmsg = "loc="+loc2.toVerboseString()+"; machine="+machine2.toVerboseString();
        assertNull(machine2.config().getLocalBag().getAllConfig().get("node"), errmsg);
        assertNull(machine2.config().getLocalBag().getAllConfig().get("template"), errmsg);
        assertEquals(machine2.getId(), "aKEcbxKN", errmsg);
        assertEquals(machine2.getJcloudsId(), "ap-southeast-1/i-56fd53f2", errmsg);
        assertEquals(machine2.getSshHostAndPort(), HostAndPort.fromParts("ec2-54-254-23-53.ap-southeast-1.compute.amazonaws.com", 22), errmsg);
        assertEquals(machine2.getPrivateAddresses(), ImmutableSet.of("10.144.66.5"), errmsg);
        assertEquals(machine2.getPublicAddresses(), ImmutableSet.of("54.254.23.53"), errmsg);
        assertEquals(machine2.getPrivateAddress(), Optional.of("10.144.66.5"), errmsg);
        assertEquals(machine2.getSubnetHostname(), "10.144.66.5", errmsg);
        assertEquals(machine2.getHostname(), "54.254.23.53", errmsg);
        assertEquals(machine2.getOsDetails().isWindows(), false, errmsg);
        assertEquals(machine2.getOsDetails().isLinux(), true, errmsg);
        assertEquals(machine2.getOsDetails().isMac(), false, errmsg);
        assertEquals(machine2.getOsDetails().getName(), "centos", errmsg);
        assertEquals(machine2.getOsDetails().getArch(), "x86_64", errmsg);
        assertEquals(machine2.getOsDetails().getVersion(), "6.5", errmsg);
        assertEquals(machine2.getOsDetails().is64bit(), true, errmsg);
        
        // Re-populates the @SetFromFlag fields from config
        machine2.configure(ImmutableMap.of());

        // Force it to be persisted again. Expect to pesist without the NodeMetadata and Template.
        app2.getManagementContext().getRebindManager().getChangeListener().onChanged(loc2);
        app2.getManagementContext().getRebindManager().getChangeListener().onChanged(machine2);
        RebindTestUtils.stopPersistence(app2);
        
        String newMachineXml = new String(java.nio.file.Files.readAllBytes(persistedMachineFile.toPath()));
        assertFalse(newMachineXml.contains("AWSEC2TemplateOptions"), newMachineXml);
        assertFalse(newMachineXml.contains("NodeMetadataImpl"), newMachineXml);
        
        // Rebind again, with the re-written persisted state.
        TestApplication app3 = rebind();
        
        JcloudsLocation loc3 = (JcloudsLocation) app3.getManagementContext().getLocationManager().getLocation("lCYB3mTb");
        JcloudsSshMachineLocation machine3 = (JcloudsSshMachineLocation) app3.getManagementContext().getLocationManager().getLocation("aKEcbxKN");
        assertEquals(ImmutableSet.copyOf(loc3.getChildren()), ImmutableSet.of(machine3));
        
        errmsg = "loc="+loc3.toVerboseString()+"; machine="+machine3.toVerboseString();
        assertEquals(machine3.getId(), "aKEcbxKN", errmsg);
        assertEquals(machine3.getJcloudsId(), "ap-southeast-1/i-56fd53f2", errmsg);
        assertEquals(machine3.getSshHostAndPort(), HostAndPort.fromParts("ec2-54-254-23-53.ap-southeast-1.compute.amazonaws.com", 22), errmsg);
        assertEquals(machine3.getPrivateAddresses(), ImmutableSet.of("10.144.66.5"), errmsg);
        assertEquals(machine3.getPublicAddresses(), ImmutableSet.of("54.254.23.53"), errmsg);
        assertEquals(machine3.getPrivateAddress(), Optional.of("10.144.66.5"), errmsg);
        assertEquals(machine3.getHostname(), "54.254.23.53", errmsg);
        
        // The VM is no longer running, so won't be able to infer OS Details.
        assertFalse(machine3.getOptionalOsDetails().isPresent(), errmsg);
    }

    @Test(groups={"Live", "Live-sanity"})
    public void testRebindsToJcloudsWinrmMachineWithTemplateAndNode() throws Exception {
        // Populate the mementoDir with some old-style peristed state
        ResourceUtils resourceUtils = ResourceUtils.create(this);
        String origParentXml = resourceUtils.getResourceAsString("classpath://org/apache/brooklyn/location/jclouds/persisted-aws-winrm-parent-fKc0Ofyn");
        String origMachineXml = resourceUtils.getResourceAsString("classpath://org/apache/brooklyn/location/jclouds/persisted-aws-winrm-machine-KYSryzW8");
        File persistedParentFile = new File(mementoDir, "locations/fKc0Ofyn");
        File persistedMachineFile = new File(mementoDir, "locations/KYSryzW8");
        FileUtils.write(
                persistedParentFile,
                origParentXml);
        FileUtils.write(
                persistedMachineFile,
                origMachineXml);

        assertTrue(origMachineXml.contains("NodeMetadataImpl"), origMachineXml);

        // Rebind to the old-style persisted state, which includes the NodeMetadata and the Template objects.
        // Expect to parse that ok.
        TestApplication app2 = rebind();
        
        JcloudsLocation loc2 = (JcloudsLocation) app2.getManagementContext().getLocationManager().getLocation("fKc0Ofyn");
        JcloudsWinRmMachineLocation machine2 = (JcloudsWinRmMachineLocation) app2.getManagementContext().getLocationManager().getLocation("KYSryzW8");
        assertEquals(ImmutableSet.copyOf(loc2.getChildren()), ImmutableSet.of(machine2));
        
        String errmsg = "loc="+loc2.toVerboseString()+"; machine="+machine2.toVerboseString();
        assertEquals(machine2.getId(), "KYSryzW8", errmsg);
        assertEquals(machine2.getJcloudsId(), "eu-central-1/i-372eda8a", errmsg);
        assertEquals(machine2.getAddress().getHostAddress(), "52.28.153.46", errmsg);
        assertEquals(machine2.getPort(), 5985, errmsg);
        // FIXME assertEquals(machine2.getAddress().getHostAddress(), HostAndPort.fromParts("ec2-52-28-153-46.eu-central-1.compute.amazonaws.com", 22), errmsg);
        assertEquals(machine2.getPrivateAddresses(), ImmutableSet.of("172.31.18.175"), errmsg);
        assertEquals(machine2.getPublicAddresses(), ImmutableSet.of("52.28.153.46"), errmsg);
        assertEquals(machine2.getPrivateAddress(), Optional.of("172.31.18.175"), errmsg);
        assertEquals(machine2.getHostname(), "ip-172-31-18-175", errmsg); // TODO would prefer the hostname that works inside and out
        assertNull(machine2.getOsDetails(), errmsg); // JcloudsWinRmMachineLocation never had OsDetails

        // Force it to be persisted again. Expect to pesist without the NodeMetadata and Template.
        app2.getManagementContext().getRebindManager().getChangeListener().onChanged(loc2);
        app2.getManagementContext().getRebindManager().getChangeListener().onChanged(machine2);
        RebindTestUtils.stopPersistence(app2);
        
        String newMachineXml = new String(java.nio.file.Files.readAllBytes(persistedMachineFile.toPath()));
        assertFalse(newMachineXml.contains("NodeMetadataImpl"), newMachineXml);
        
        // Rebind again, with the re-written persisted state.
        TestApplication app3 = rebind();
        
        JcloudsLocation loc3 = (JcloudsLocation) app3.getManagementContext().getLocationManager().getLocation("fKc0Ofyn");
        JcloudsWinRmMachineLocation machine3 = (JcloudsWinRmMachineLocation) app3.getManagementContext().getLocationManager().getLocation("KYSryzW8");
        assertEquals(ImmutableSet.copyOf(loc3.getChildren()), ImmutableSet.of(machine3));
        
        errmsg = "loc="+loc3.toVerboseString()+"; machine="+machine3.toVerboseString();
        assertEquals(machine3.getId(), "KYSryzW8", errmsg);
        assertEquals(machine3.getJcloudsId(), "eu-central-1/i-372eda8a", errmsg);
        assertEquals(machine3.getAddress().getHostAddress(), "52.28.153.46", errmsg);
        assertEquals(machine3.getPort(), 5985, errmsg);
        assertEquals(machine3.getPrivateAddresses(), ImmutableSet.of("172.31.18.175"), errmsg);
        assertEquals(machine3.getPublicAddresses(), ImmutableSet.of("52.28.153.46"), errmsg);
        assertEquals(machine3.getPrivateAddress(), Optional.of("172.31.18.175"), errmsg);
        assertEquals(machine3.getHostname(), "ip-172-31-18-175", errmsg); // TODO would prefer the hostname that works inside and out
        assertNull(machine2.getOsDetails(), errmsg); // JcloudsWinRmMachineLocation never had OsDetails
    }

    private void assertMachineEquals(JcloudsSshMachineLocation actual, JcloudsSshMachineLocation expected, boolean expectNoOsDetails) {
        String errmsg = "actual="+actual.toVerboseString()+"; expected="+expected.toVerboseString();
        assertEquals(actual.getId(), expected.getId(), errmsg);
        assertEquals(actual.getJcloudsId(), expected.getJcloudsId(), errmsg);
        if (expectNoOsDetails) {
            assertOsDetailEquals(actual.getOptionalOsDetails(), Optional.<OsDetails>absent());
        } else {
            assertOsDetailEquals(actual.getOptionalOsDetails(), expected.getOptionalOsDetails());
        }
        assertEquals(actual.getSshHostAndPort(), expected.getSshHostAndPort());
        assertEquals(actual.getPrivateAddress(), expected.getPrivateAddress());
        assertConfigBagEquals(actual.config().getBag(), expected.config().getBag(), errmsg);
    }

    private void assertOsDetailEquals(Optional<OsDetails> actual, Optional<OsDetails> expected) {
        String errmsg = "actual="+actual+"; expected="+expected;
        if (actual.isPresent()) {
            assertEquals(actual.get().isWindows(), expected.get().isWindows());
            assertEquals(actual.get().isLinux(), expected.get().isLinux());
            assertEquals(actual.get().isMac(), expected.get().isMac());
            assertEquals(actual.get().getName(), expected.get().getName());
            assertEquals(actual.get().getArch(), expected.get().getArch());
            assertEquals(actual.get().getVersion(), expected.get().getVersion());
            assertEquals(actual.get().is64bit(), expected.get().is64bit());
        } else {
            assertFalse(expected.isPresent(), errmsg);
        }
    }

    private void assertJcloudsLocationEquals(JcloudsLocation actual, JcloudsLocation expected) {
        String errmsg = "actual="+actual.toVerboseString()+"; expected="+expected.toVerboseString();
        assertEquals(actual.getId(), expected.getId(), errmsg);
        assertEquals(actual.getProvider(), expected.getProvider(), errmsg);
        assertEquals(actual.getRegion(), expected.getRegion(), errmsg);
        assertEquals(actual.getIdentity(), expected.getIdentity(), errmsg);
        assertEquals(actual.getCredential(), expected.getCredential(), errmsg);
        assertEquals(actual.getHostGeoInfo(), expected.getHostGeoInfo(), errmsg);
        assertConfigBagEquals(actual.config().getBag(), expected.config().getBag(), errmsg);
    }

    private void assertConfigBagEquals(ConfigBag actual, ConfigBag expected, String errmsg) {
        // TODO revisit the strong assertion that configBags are equal
        
//        // TODO Can we include all of these things (e.g. when locations are entities, so flagged fields not treated special)?
//        List<String> configToIgnore = ImmutableList.of("id", "template", "usedPorts", "machineCreationSemaphore", "config");
//        MutableMap<Object, Object> actualMap = MutableMap.builder().putAll(actual.getAllConfig())
//                .removeAll(configToIgnore)
//                .build();
//        MutableMap<Object, Object> expectedMap = MutableMap.builder().putAll(expected.getAllConfig())
//                .removeAll(configToIgnore)
//                .build();
//        
//        assertEquals(actualMap, expectedMap, errmsg+"; actualBag="+actualMap+"; expectedBag="+expectedMap);
    }
    
    private TestApplication rebind() throws Exception {
        return rebind(RebindOptions.create()
                .mementoDir(mementoDir)
                .classLoader(classLoader));
    }
    
    private TestApplication rebind(RebindOptions options) throws Exception {
        RebindTestUtils.stopPersistence(origApp);
        return (TestApplication) RebindTestUtils.rebind(options);
    }
}
