package com.atlassian.stash.sal.api.lifecycle;

import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
import com.atlassian.sal.api.lifecycle.LifecycleAware;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.osgi.framework.Bundle;
import org.osgi.framework.ServiceReference;

import java.util.Collection;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class TestDefaultLifecycleManager extends TestDefaultLifecycleManagerBase {
    @Test
    public void onStartCalledForServicesRegisteredBeforeDefaultLifecycleManagerIsInitialized() throws Exception {
        final String pluginKey1 = "plugin-key-1";
        final String pluginKey2 = "plugin-key-2";
        final Bundle bundle1 = mockBundle(pluginKey1);
        final Bundle bundle2 = mockBundle(pluginKey2);
        final LifecycleAware lifecycleAware1 = mock(LifecycleAware.class);
        final LifecycleAware lifecycleAware2 = mock(LifecycleAware.class);
        final ServiceReference<LifecycleAware> service1 = registerService(bundle1, lifecycleAware1);
        final ServiceReference<LifecycleAware> service2 = registerService(bundle2, lifecycleAware2);
        when(bundleContext.getServiceReferences(LifecycleAware.class, null)).thenReturn(ImmutableList.of(service1, service2));

        defaultLifecycleManager.afterPropertiesSet();

        isApplicationSetup = true;
        enablePlugin(pluginKey2);
        enablePlugin(pluginKey1);

        // Should not be called yet, as things haven't started
        verify(lifecycleAware1, never()).onStart();
        verify(lifecycleAware2, never()).onStart();

        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));

        // And now it should have started
        InOrder inOrder = inOrder(lifecycleAware1, lifecycleAware2);
        // plugin 2 was enabled before plugin 1, so it's lifecycleAwares should be initialized first
        inOrder.verify(lifecycleAware2).onStart();
        inOrder.verify(lifecycleAware1).onStart();

        // And this is a convenient test to check resource management. This isn't perfect (we should check calls are balanced),
        // but it's not bad and picks up the likely broken code cases.
        verify(bundleContext).ungetService(service1);
        verify(bundleContext).ungetService(service2);
    }

    @Test
    public void onStartCalledWhenPluginFrameworkStartedIfApplicationIsSetup() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
        enablePlugin(pluginKey);

        // Should not be called yet, as things haven't started
        verify(lifecycleAware, never()).onStart();

        isApplicationSetup = true;
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));

        // And now it should have started
        verify(lifecycleAware).onStart();
    }

    @Test
    public void onStartCalledWhenStartCalledIfApplicationIsSetupAfterPluginFrameworkStarted() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
        enablePlugin(pluginKey);

        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));

        // Should not be called yet, as things haven't started
        verify(lifecycleAware, never()).onStart();

        isApplicationSetup = true;
        defaultLifecycleManager.start();

        // And now it should have started
        verify(lifecycleAware).onStart();
    }

    @Test
    public void onStartCalledWhenPluginEnabledAfterStarted() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
        isApplicationSetup = true;
        defaultLifecycleManager.start();

        // Should not be called yet, as plugin is not enabled
        verify(lifecycleAware, never()).onStart();

        enablePlugin(pluginKey);

        // Should have been called now, in response to plugin being enabled
        verify(lifecycleAware).onStart();
    }

    @Test
    public void onStartCalledOnlyForRelevantPluginWhenPluginEnabledAfterStarted() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
        isApplicationSetup = true;
        defaultLifecycleManager.start();

        // Should not be called yet, as plugin is not enabled
        verify(lifecycleAware, never()).onStart();

        // register a couple of services from different bundles
        Bundle bundle1 = mockBundle("plugin-1");
        LifecycleAware lifecycleAware1 = mockLifecycleAwareAndRegisterService(bundle1);
        Bundle bundle2 = mockBundle("plugin-2");
        LifecycleAware lifecycleAware2 = mockLifecycleAwareAndRegisterService(bundle2);

        // both bundles should be marked as enabled, just like the plugin system does when a group of dependent plugins
        // is enabled at the same time.
        when(pluginAccessor.isPluginEnabled("plugin-1")).thenReturn(true);
        when(pluginAccessor.isPluginEnabled("plugin-2")).thenReturn(true);

        enablePlugin("plugin-1");

        // Should have been called now, in response to plugin1 being enabled
        verify(lifecycleAware1).onStart();
        // plugin2 LifecycleAware components should _not_ be notified until a PluginEnabledEvent is received for that
        // plugin
        verify(lifecycleAware2, never()).onStart();

        enablePlugin("plugin-2");

        verify(lifecycleAware2).onStart();
        verifyNoMoreInteractions(lifecycleAware1, lifecycleAware2);
    }


    @Test
    public void onStartCalledWhenLifecycleAwareRegisteredAfterPluginEnabled() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
        isApplicationSetup = true;
        defaultLifecycleManager.start();

        enablePlugin(pluginKey);

        final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
        // Should have been called now, since the system is started and the plugin is enabled
        verify(lifecycleAware).onStart();
    }

    @Test
    public void notifyOnStartCalledWhenPluginFrameworkStartedWhenSetup() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        isApplicationSetup = true;
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));

        assertThat(notifyOnStartCalled, is(1));
    }

    @Test
    public void notifyOnStartCalledWhenStartCalledIfApplicationIsSetupAfterPluginFrameworkStarted() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));

        // Not yet setup, so we should not have started yet
        assertThat(notifyOnStartCalled, is(0));

        isApplicationSetup = true;
        defaultLifecycleManager.start();

        assertThat(notifyOnStartCalled, is(1));
    }

    @Test
    public void notifyOnStartOnlyCalledOnceEvenWhenBothPluginFrameworkStartedAndStartCalledWhenSetup() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        isApplicationSetup = true;
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
        defaultLifecycleManager.start();

        assertThat(notifyOnStartCalled, is(1));
    }

    @Test
    public void onStartNotCalledWhenLifecycleAwareUnregisteredBeforeStart() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();
        // This is the shape you see if a plugin fails to enable but does get some services up before being disabled.
        final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
        final ServiceReference serviceReference = registerService(pluginBundle, lifecycleAware);
        unregisterService(serviceReference);

        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
        isApplicationSetup = true;
        defaultLifecycleManager.start();
    }

    @Test
    public void verifyListenerRegistrationManagement() throws Exception {
        defaultLifecycleManager.afterPropertiesSet();

        assertThat(serviceListener, notNullValue());

        verify(pluginEventManager).register(defaultLifecycleManager);
        // This is a little smoke and mirrors, in that we just captured it, but i want to keep this test orthogonal, and it serves
        // to document the listener registration management also.
        verify(bundleContext).addServiceListener(same(serviceListener), anyString());


        verify(pluginEventManager, never()).unregister(defaultLifecycleManager);
        verify(bundleContext, never()).removeServiceListener(serviceListener);

        defaultLifecycleManager.destroy();

        verify(pluginEventManager).unregister(defaultLifecycleManager);
        verify(bundleContext).removeServiceListener(serviceListener);
    }

    @Test
    public void serviceDeregisteredJustAfterPostConstructSnapshotIsNotLeaked() throws Exception {
        // This is a very greybox test, but i think there's logic worth exercising
        final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
        final ServiceReference<LifecycleAware> serviceReference = registerService(pluginBundle, lifecycleAware);
        // Arrange that just after DefaultLifecycleManager.postConstruct asks for the references, one of them goes away.
        when(bundleContext.getServiceReferences(LifecycleAware.class, null)).thenAnswer(
                new Answer<Collection<ServiceReference<LifecycleAware>>>() {
                    @Override
                    public Collection<ServiceReference<LifecycleAware>> answer(final InvocationOnMock invocation)
                            throws Throwable {
                        // The idea is that BundleContext.getServiceReferences return the one service, and then it unregistered
                        final ImmutableList<ServiceReference<LifecycleAware>> services = ImmutableList.of(serviceReference);
                        unregisterService(serviceReference);
                        return services;
                    }
                });

        defaultLifecycleManager.afterPropertiesSet();

        // This is kind of a bit kludgy in terms of the test scenario, but it's the only really reasonable way to arrange we'd
        // detect the failure we're looking for. The alternative is to add debug logging for this scenario and capture logging.
        enablePlugin(pluginKey);
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
        isApplicationSetup = true;
        defaultLifecycleManager.start();

        // We should not start the lifecyleAware, because it wasn't registered by the time we started.
        verify(lifecycleAware, never()).onStart();
    }

    private void onStartThrowableIsCaught(Throwable t) throws Exception {
        // It would be even better if we could test other instances of LifecycleAware are unaffected also, but without playing
        // crazy brittle games on the HashSet ordering, i don't see a way to make such a test reliable.
        defaultLifecycleManager.afterPropertiesSet();

        final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
        final ServiceReference<LifecycleAware> service = registerService(pluginBundle, lifecycleAware);
        doThrow(t).when(lifecycleAware).onStart();
        enablePlugin(pluginKey);

        // Should not be called yet, as things haven't started
        verify(lifecycleAware, never()).onStart();

        isApplicationSetup = true;
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));

        // And now it should have started
        verify(lifecycleAware).onStart();
        // Check resource management in the exception case also
        verify(bundleContext).ungetService(service);
    }

    @Test
    public void onStartRuntimeExceptionsAreCaught() throws Exception {
        onStartThrowableIsCaught(new RuntimeException("Test onStart failure"));
    }

    @Test
    public void onStartErrorsAreCaught() throws Exception {
        onStartThrowableIsCaught(new Error("Test onStart failure"));
    }

    @Test
    public void staleServiceReferencesReturningNullBundleAndLifecycleAreHandled() throws Exception {
        verifyStaleServiceReferencesAreHandled(true);
    }

    @Test
    public void staleServiceReferencesReturningNullLifecycleAreHandled() throws Exception {
        verifyStaleServiceReferencesAreHandled(false);
    }

    private void verifyStaleServiceReferencesAreHandled(final boolean makeBundleNull) throws Exception {
        // This tests a race between DefaultLifecycleManager.notifyLifecycleAwareIfStartedAndEnabled() and service deregistration.
        // It's not worth the effort of actually constructing the race, we can just rig return values ahead of time.
        defaultLifecycleManager.afterPropertiesSet();

        final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
        final ServiceReference<LifecycleAware> service = registerService(pluginBundle, lifecycleAware);
        enablePlugin(pluginKey);

        if (makeBundleNull) {
            when(service.getBundle()).thenReturn(null);
        }
        when(bundleContext.getService(service)).thenReturn(null);

        isApplicationSetup = true;
        defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));

        // In fact the majority of the verification here is that we don't throw, but it's worth checking a few details

        // We shouldn't have called onStart(). Hard to imagine how we could have, but i guess aggressive (broken) caching might.
        verify(lifecycleAware, never()).onStart();
        // We shouldn't unget in this case, because we didn't get successfully.
        verify(bundleContext, never()).ungetService(service);
    }

    private LifecycleAware mockLifecycleAwareAndRegisterService(final Bundle bundle) {
        return mockLifecycleAwareAndRegisterService(bundle, LifecycleAware.class).left();
    }
}
