/*
 * 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.entity.dns.geoscaling;

import static org.testng.Assert.assertEquals;

import java.net.InetAddress;

import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.location.geo.HostGeoInfo;
import org.apache.brooklyn.core.location.geo.HostGeoLookup;
import org.apache.brooklyn.core.location.geo.MaxMind2HostGeoLookup;
import org.apache.brooklyn.core.location.geo.UtraceHostGeoLookup;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.entity.group.DynamicGroup;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.internal.BrooklynSystemProperties;
import org.apache.brooklyn.util.net.Networking;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.SkipException;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.apache.brooklyn.location.ssh.SshMachineLocation;

import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;

/**
 * {@link GeoscalingScriptGenerator} unit tests.
 */
public class GeoscalingIntegrationTest extends BrooklynAppUnitTestSupport {

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

    private final String primaryDomain = "geopaas.org";//"domain"+((int)(Math.random()*10000))+".test.org";
    private final String subDomain = "subdomain"+((int)(Math.random()*10000));
    private final InetAddress addrWithGeo = Networking.getReachableLocalHost();
    private final InetAddress addrWithoutGeo = Networking.getInetAddressWithFixedName(StubHostGeoLookup.HOMELESS_IP);
    
    private TestEntity target;
    private DynamicGroup group;
    private GeoscalingDnsService geoDns;
    private String origGeoLookupImpl;

    private SshMachineLocation locWithGeo;
    private SshMachineLocation locWithoutGeo;

    @Override
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        // Want to load username and password from user's properties.
        mgmt = LocalManagementContextForTests.newInstance(BrooklynProperties.Factory.newDefault());
        super.setUp();

        origGeoLookupImpl = BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getValue();
        HostGeoInfo.clearCachedLookup();

        target = app.createAndManageChild(EntitySpec.create(TestEntity.class));
        group = app.createAndManageChild(EntitySpec.create(DynamicGroup.class)
                .configure(DynamicGroup.ENTITY_FILTER, Predicates.instanceOf(TestEntity.class)));

        String username = getBrooklynProperty(mgmt, "brooklyn.geoscaling.username");
        String password = getBrooklynProperty(mgmt, "brooklyn.geoscaling.password");
        if (username == null || password == null) {
            throw new SkipException("Set brooklyn.geoscaling.username and brooklyn.geoscaling.password in brooklyn.properties to enable test");
        }

        geoDns = app.createAndManageChild(EntitySpec.create(GeoscalingDnsService.class)
                .displayName("Geo-DNS")
                .configure(GeoscalingDnsService.GEOSCALING_USERNAME, username)
                .configure(GeoscalingDnsService.GEOSCALING_PASSWORD, password)
                .configure(GeoscalingDnsService.GEOSCALING_PRIMARY_DOMAIN_NAME, primaryDomain)
                .configure(GeoscalingDnsService.GEOSCALING_SMART_SUBDOMAIN_NAME, subDomain)
                .configure(GeoscalingDnsService.ENTITY_PROVIDER, group));

        locWithGeo = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
                .configure("address", addrWithGeo)
                .configure("name", "Edinburgh")
                .configure("latitude", 55.94944)
                .configure("longitude", -3.16028)
                .configure("iso3166", ImmutableList.of("GB-EDH")));

        locWithoutGeo = mgmt.getLocationManager().createLocation(LocationSpec.create(SshMachineLocation.class)
                .configure("address", addrWithoutGeo)
                .configure("name", "Nowhere"));
    }

    @Override
    @AfterMethod(alwaysRun=true)
    public void tearDown() throws Exception {
        super.tearDown();
        if (origGeoLookupImpl != null) {
            System.setProperty(BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getPropertyName(), origGeoLookupImpl);
        } else {
            System.clearProperty(BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getPropertyName());
        }
        HostGeoInfo.clearCachedLookup();
    }

    private String getBrooklynProperty(ManagementContext mgmt, String property) {
        return ((ManagementContextInternal) mgmt).getBrooklynProperties().getFirst(property);
    }

    @Test(groups={"Integration"})
    public void testRoutesToExpectedLocation() {
        // Without this config, running on a home network (i.e. no public IP) the entity will have a private IP and will be ignored
        geoDns.config().set(GeoscalingDnsService.INCLUDE_HOMELESS_ENTITIES, true);
        
        target.sensors().set(Attributes.HOSTNAME,addrWithGeo.getHostName());
        
        app.start(ImmutableList.of(locWithGeo));
        
        LOG.info("geo-scaling test, using {}.{}; expect to be wired to {}", new Object[] {subDomain, primaryDomain, addrWithGeo});
        
        assertTargetHostsEventually(geoDns, 1);
    }
    
    @Test(groups={"Integration"})
    public void testIgnoresAddressWithoutGeography() throws Exception {
        System.setProperty(BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getPropertyName(), StubHostGeoLookup.class.getName());
        geoDns.config().set(GeoscalingDnsService.INCLUDE_HOMELESS_ENTITIES, false); // false is default
        
        app.start(ImmutableList.of(locWithoutGeo));
        target.sensors().set(Attributes.HOSTNAME, StubHostGeoLookup.HOMELESS_IP);
        
        LOG.info("geo-scaling test, using {}.{}; expect not to be wired to {}", new Object[] {subDomain, primaryDomain, addrWithoutGeo});
        
        Asserts.succeedsContinually(MutableMap.of("timeout", 10*1000), new Runnable() {
            @Override public void run() {
                assertEquals(geoDns.getTargetHosts().size(), 0, "targets="+geoDns.getTargetHosts());
            }
        });
    }

    @Test(groups={"Integration"})
    public void testIncludesAddressWithoutGeography() {
        System.setProperty(BrooklynSystemProperties.HOST_GEO_LOOKUP_IMPL.getPropertyName(), StubHostGeoLookup.class.getName());
        geoDns.config().set(GeoscalingDnsService.INCLUDE_HOMELESS_ENTITIES, true);
        
        app.start(ImmutableList.of(locWithoutGeo));
        target.sensors().set(Attributes.HOSTNAME, StubHostGeoLookup.HOMELESS_IP);
        
        LOG.info("geo-scaling test, using {}.{}; expect to be wired to {}", new Object[] {subDomain, primaryDomain, addrWithoutGeo});
        
        assertTargetHostsEventually(geoDns, 1);
    }

    private void assertTargetHostsEventually(final GeoscalingDnsService geoDns, final int numExpected) {
        Asserts.succeedsEventually(new Runnable() {
            @Override public void run() {
                assertEquals(geoDns.getTargetHosts().size(), 1, "targets="+geoDns.getTargetHosts());
            }
        });
    }
    
    public static class StubHostGeoLookup implements HostGeoLookup {
        public static final String HOMELESS_IP = "1.2.3.4";
        private final HostGeoLookup delegate;

        public StubHostGeoLookup() throws Exception {
            this(null);
        }
        
        public StubHostGeoLookup(String delegateImpl) throws Exception {
            if (delegateImpl == null) {
                // don't just call HostGeoInfo.getDefaultLookup; this is the default lookup!
                if (MaxMind2HostGeoLookup.getDatabaseReader()!=null) {
                    delegate = new MaxMind2HostGeoLookup();
                } else {
                    delegate = new UtraceHostGeoLookup();
                }
            } else {
                delegate = (HostGeoLookup) Class.forName(delegateImpl).newInstance();
            }
        }

        @Override
        public HostGeoInfo getHostGeoInfo(InetAddress address) throws Exception {
            // Saw strange test failure on jenkins: hence paranoid logging, just in case exception is swallowed somehow.
            try {
                HostGeoInfo result;
                if (HOMELESS_IP.equals(address.getHostAddress())) {
                    result = null;
                } else {
                    result = delegate.getHostGeoInfo(address);
                }
                LOG.info("StubHostGeoLookup.getHostGeoInfo queried: address="+address+"; hostAddress="+address.getHostAddress()+"; result="+result);
                return result;
            } catch (Throwable t) {
                LOG.error("StubHostGeoLookup.getHostGeoInfo encountered problem (rethrowing): address="+address+"; hostAddress="+address.getHostAddress(), t);
                throw Exceptions.propagate(t);
            }
        }
    }
}
