/*
 * 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.software.base.test.core.mgmt.usage;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.fail;

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

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.NoMachinesAvailableException;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.location.Machines;
import org.apache.brooklyn.core.mgmt.usage.LocationUsage;
import org.apache.brooklyn.core.mgmt.usage.LocationUsage.LocationEvent;
import org.apache.brooklyn.core.mgmt.usage.RecordingUsageListener;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest;
import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.core.internal.ssh.RecordingSshTool;
import org.apache.brooklyn.util.time.Time;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

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

public class LocationUsageTrackingTest extends BrooklynAppUnitTestSupport {

    private DynamicLocalhostMachineProvisioningLocation loc;

    @BeforeMethod(alwaysRun = true)
    @Override
    public void setUp() throws Exception {
        super.setUp();
        loc = mgmt.getLocationManager().createLocation(LocationSpec.create(DynamicLocalhostMachineProvisioningLocation.class));
        RecordingSshTool.clear();
    }

    @Test
    public void testUsageInitiallyEmpty() {
        Set<LocationUsage> usage = mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
        assertEquals(usage, ImmutableSet.of());
    }

    @Test
    public void testAddAndRemoveUsageListener() throws Exception {
        final RecordingUsageListener listener = new RecordingUsageListener();
        mgmt.getUsageManager().addUsageListener(listener);
        
        app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
        app.start(ImmutableList.of(loc));
        final SshMachineLocation machine = Iterables.getOnlyElement(loc.getAllMachines());
        
        Asserts.succeedsEventually(new Runnable() {
            @Override public void run() {
                List<List<?>> events = listener.getLocationEvents();
                assertEquals(events.size(), 1, "events="+events);
                listener.assertLocEvent(0, machine, app, Lifecycle.CREATED, "events="+events);
            }});

        // Remove the listener; will get no more notifications
        listener.clearEvents();
        mgmt.getUsageManager().removeUsageListener(listener);
        
        app.stop();
        Asserts.succeedsContinually(new Runnable() {
            @Override public void run() {
                List<List<?>> events = listener.getLocationEvents();
                assertEquals(events.size(), 0, "events="+events);
            }});
    }

    @Test
    public void testUsageIncludesStartAndStopEvents() {
        SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));

        // Start the app; expect record of location in use
        long preStart = System.currentTimeMillis();
        app.start(ImmutableList.of(loc));
        long postStart = System.currentTimeMillis();
        SshMachineLocation machine = Iterables.getOnlyElement(loc.getAllMachines());

        Set<LocationUsage> usages1 = mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
        LocationUsage usage1 = Iterables.getOnlyElement(usages1);
        assertLocationUsage(usage1, machine);
        assertLocationEvent(usage1.getEvents().get(0), entity, Lifecycle.CREATED, preStart, postStart);

        // Stop the app; expect record of location no longer in use
        long preStop = System.currentTimeMillis();
        app.stop();
        long postStop = System.currentTimeMillis();

        Set<LocationUsage> usages2 = mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
        LocationUsage usage2 = Iterables.getOnlyElement(usages2);
        assertLocationUsage(usage2, machine);
        assertLocationEvent(usage2.getEvents().get(1), app.getApplicationId(), entity.getId(), entity.getEntityType().getName(), Lifecycle.DESTROYED, preStop, postStop);
        
        assertEquals(usage2.getEvents().size(), 2, "usage="+usage2);
    }

    @Test
    public void testFoo() throws Exception {
        DynamicLocalhostMachineProvisioningLocation recordingLoc = mgmt.getLocationManager().createLocation(LocationSpec.create(DynamicLocalhostMachineProvisioningLocation.class)
                .configure("sshToolClass", RecordingSshTool.class.getName()));

        SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(EntitySpec.create(SoftwareProcessEntityTest.MyService.class));

        // Start the app; expect record of location in use
        long preStart = System.currentTimeMillis();
        app.start(ImmutableList.of(recordingLoc));
        long postStart = System.currentTimeMillis();
        SshMachineLocation machine = Machines.findUniqueMachineLocation(entity.getLocations(), SshMachineLocation.class).get();

        Set<LocationUsage> usages1 = mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
        LocationUsage usage1 = Iterables.getOnlyElement(usages1);
        
        assertEquals(usage1.getLocationId(), machine.getId(), "usage="+usage1);
        assertNotNull(usage1.getMetadata(), "usage="+usage1);

        assertLocationEvent(usage1.getEvents().get(0), entity, Lifecycle.CREATED, preStart, postStart);

//        assertEquals(event.getApplicationId(), expectedAppId);
//        assertEquals(event.getEntityId(), expectedEntityId);
//        assertEquals(event.getEntityType(), expectedEntityType);
//        assertEquals(event.getState(), expectedState);
//        long eventTime = event.getDate().getTime();
//        if (eventTime < (preEvent - TIMING_GRACE) || eventTime > (postEvent + TIMING_GRACE)) {
//            fail("for "+expectedState+": event=" + Time.makeDateString(eventTime) + "("+eventTime + "); "
//                    + "pre=" + Time.makeDateString(preEvent) + " ("+preEvent+ "); "
//                    + "post=" + Time.makeDateString(postEvent) + " ("+postEvent + ")");
//        }

        // Stop the app; expect record of location no longer in use
        long preStop = System.currentTimeMillis();
        app.stop();
        long postStop = System.currentTimeMillis();

        Set<LocationUsage> usages2 = mgmt.getUsageManager().getLocationUsage(Predicates.alwaysTrue());
        LocationUsage usage2 = Iterables.getOnlyElement(usages2);
        assertLocationUsage(usage2, machine);
        assertLocationEvent(usage2.getEvents().get(1), app.getApplicationId(), entity.getId(), entity.getEntityType().getName(), Lifecycle.DESTROYED, preStop, postStop);
        
        assertEquals(usage2.getEvents().size(), 2, "usage="+usage2);
        
        System.out.println(RecordingSshTool.execScriptCmds);
    }

    public static class DynamicLocalhostMachineProvisioningLocation extends LocalhostMachineProvisioningLocation {
        @Override
        public SshMachineLocation obtain(Map<?, ?> flags) throws NoMachinesAvailableException {
            System.out.println("called DynamicLocalhostMachineProvisioningLocation.obtain");
            return super.obtain(flags);
        }

        @Override
        public void release(SshMachineLocation machine) {
            System.out.println("called DynamicLocalhostMachineProvisioningLocation.release");
            super.release(machine);
            super.machines.remove(machine);
            super.removeChild(machine);
        }
    }
    
    private void assertLocationUsage(LocationUsage usage, Location expectedLoc) {
        assertEquals(usage.getLocationId(), expectedLoc.getId(), "usage="+usage);
        assertNotNull(usage.getMetadata(), "usage="+usage);
    }

    private void assertLocationEvent(LocationEvent event, Entity expectedEntity, Lifecycle expectedState, long preEvent, long postEvent) {
        assertLocationEvent(event, expectedEntity.getApplicationId(), expectedEntity.getId(), expectedEntity.getEntityType().getName(), expectedState, preEvent, postEvent);
    }
    
    private void assertLocationEvent(LocationEvent event, String expectedAppId, String expectedEntityId, String expectedEntityType, Lifecycle expectedState, long preEvent, long postEvent) {
        // Saw times differ by 1ms - perhaps different threads calling currentTimeMillis() can get out-of-order times?!
        final int TIMING_GRACE = 5;
        
        assertEquals(event.getApplicationId(), expectedAppId);
        assertEquals(event.getEntityId(), expectedEntityId);
        assertEquals(event.getEntityType(), expectedEntityType);
        assertEquals(event.getState(), expectedState);
        long eventTime = event.getDate().getTime();
        if (eventTime < (preEvent - TIMING_GRACE) || eventTime > (postEvent + TIMING_GRACE)) {
            fail("for "+expectedState+": event=" + Time.makeDateString(eventTime) + "("+eventTime + "); "
                    + "pre=" + Time.makeDateString(preEvent) + " ("+preEvent+ "); "
                    + "post=" + Time.makeDateString(postEvent) + " ("+postEvent + ")");
        }
    }
}
