package com.atlassian.stash.internal.home;

import com.atlassian.event.api.EventPublisher;
import com.atlassian.johnson.Johnson;
import com.atlassian.johnson.event.AddEvent;
import com.atlassian.johnson.event.EventLevel;
import com.atlassian.johnson.event.RemoveEvent;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
import com.atlassian.plugin.module.ModuleFactory;
import com.atlassian.bitbucket.concurrent.LockService;
import com.atlassian.bitbucket.IntegrityException;
import com.atlassian.bitbucket.help.HelpPathService;
import com.atlassian.bitbucket.home.HomeUpdate;
import com.atlassian.bitbucket.home.HomeUpdateHandler;
import com.atlassian.bitbucket.home.HomeUpdateHandlerModuleDescriptor;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.stash.internal.server.InternalApplicationPropertiesService;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.*;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.io.ClassPathResource;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.EventObject;
import java.util.List;
import java.util.concurrent.locks.Lock;

import static org.junit.Assert.*;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class HomeTrackerTest {

    @Mock
    private PluginFrameworkStartedEvent event;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private HelpPathService helpPathService;
    @Spy
    @SuppressWarnings("unused")
    private I18nService i18nService = new SimpleI18nService();
    @Mock
    private PluginAccessor pluginAccessor;
    @Mock
    private InternalApplicationPropertiesService propertiesService;
    @Mock
    private LockService lockService;
    @InjectMocks
    private HomeTracker tracker;

    @BeforeClass
    public static void setupJohnson() throws IOException {
        Johnson.initialize(new ClassPathResource("minimal-johnson-config.xml").getURL().getPath());
    }

    @AfterClass
    public static void tearDownJohnson() {
        Johnson.terminate();
    }

    @Before
    public void setup() {
        when(event.getPluginAccessor()).thenReturn(pluginAccessor);

        when(helpPathService.getPageTitle("bitbucket.help.home.directory")).thenReturn("helpTitle");
        when(helpPathService.getPageUrl("bitbucket.help.home.directory")).thenReturn("helpUrl");

        when(lockService.getLock(anyString())).thenReturn(mock(Lock.class));
    }

    @Test
    public void testOnPluginFrameworkStarted() {
        prepareHome("/var/data/stash", "/users/jdoe/stash-home");

        HomeUpdateHandler handler = mock(HomeUpdateHandler.class);
        ModuleFactory moduleFactory = mock(ModuleFactory.class);

        HomeUpdateHandlerModuleDescriptor descriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory);
        when(moduleFactory.createModule(isNull(String.class), same(descriptor))).thenReturn(handler);

        when(pluginAccessor.getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class)))
                .thenReturn(Arrays.asList(descriptor));

        tracker.onPluginFrameworkStarted(event);

        ArgumentCaptor<EventObject> eventCaptor = ArgumentCaptor.forClass(EventObject.class);
        ArgumentCaptor<HomeUpdate> updateCaptor = ArgumentCaptor.forClass(HomeUpdate.class);

        verify(eventPublisher, times(2)).publish(eventCaptor.capture());

        List<EventObject> events = eventCaptor.getAllValues();
        assertEquals(2, events.size());

        AddEvent add = (AddEvent) events.get(0); //May ClassCastException if the wrong event is being raised
        assertNotNull(add.getEvent());
        assertSame(tracker, add.getSource());
        assertTrue(add.getEvent().getDesc().equals("bitbucket.shared.home.updated"));
        assertEquals(EventLevel.WARNING, add.getEvent().getLevel().getLevel()); //Should have remained a WARNING

        RemoveEvent remove = (RemoveEvent) events.get(1); //May ClassCastException if the wrong event is being raised
        assertSame(add.getEvent(), remove.getEvent());
        assertSame(tracker, remove.getSource());

        verify(handler).apply(updateCaptor.capture());
        verify(handler, never()).rollback(any(HomeUpdate.class));

        HomeUpdate update = updateCaptor.getValue();
        assertNotNull(update);
        assertEquals("/var/data/stash", update.getNewDir());
        assertEquals("/users/jdoe/stash-home", update.getOldDir());

        verify(pluginAccessor).getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class));
        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService).setLastSharedHomeDir(eq("/var/data/stash"));
        verifyNoMoreInteractions(propertiesService);
    }

    @Test
    public void testOnPluginFrameworkStartedForBaseline() {
        prepareCurrentHome("/var/data/stash");

        tracker.onPluginFrameworkStarted(event);

        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService).setLastSharedHomeDir(eq("/var/data/stash"));
        verifyNoMoreInteractions(propertiesService);
        verifyZeroInteractions(eventPublisher); //Johnson should not be used when capturing the baseline
        verifyZeroInteractions(pluginAccessor); //HomeUpdateHandlers should not be invoked when capturing the baseline
    }

    @Test
    public void testOnPluginFrameworkStartedIgnoresExceptionsOnRollback() {
        prepareHome("/var/data/stash", "/users/jdoe/stash-home");

        ModuleFactory moduleFactory = mock(ModuleFactory.class);
        HomeUpdateHandlerModuleDescriptor firstDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 10);
        HomeUpdateHandlerModuleDescriptor secondDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 20);

        HomeUpdateHandler first = mock(HomeUpdateHandler.class);
        doThrow(UnsupportedOperationException.class).when(first).rollback(any(HomeUpdate.class));

        HomeUpdateHandler second = mock(HomeUpdateHandler.class);
        doThrow(new IntegrityException(new KeyedMessage("key", "localised", "root")))
                .when(second).apply(any(HomeUpdate.class));

        when(moduleFactory.createModule(isNull(String.class), same(firstDescriptor))).thenReturn(first);
        when(moduleFactory.createModule(isNull(String.class), same(secondDescriptor))).thenReturn(second);
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class)))
                .thenReturn(Arrays.asList(firstDescriptor, secondDescriptor));

        tracker.onPluginFrameworkStarted(event);

        ArgumentCaptor<AddEvent> addCaptor = ArgumentCaptor.forClass(AddEvent.class);
        ArgumentCaptor<HomeUpdate> updateCaptor = ArgumentCaptor.forClass(HomeUpdate.class);

        verify(eventPublisher, times(1)).publish(addCaptor.capture());
        verify(eventPublisher, never()).publish(isA(RemoveEvent.class)); //Should never remove the lock

        AddEvent add = addCaptor.getValue();
        assertNotNull(add.getEvent());
        assertSame(tracker, add.getSource());
        assertTrue(add.getEvent().getDesc().equals("bitbucket.shared.home.updatefailed"));
        assertEquals(EventLevel.FATAL, add.getEvent().getLevel().getLevel()); //Should have been promoted to FATAL

        InOrder ordered = inOrder(first, second);
        ordered.verify(first).apply(updateCaptor.capture());
        ordered.verify(second).apply(updateCaptor.capture());
        ordered.verify(first).rollback(updateCaptor.capture());
        verify(second, never()).rollback(any(HomeUpdate.class)); //The handler that throws isn't rolled back

        HomeUpdate update = updateCaptor.getValue();
        assertNotNull(update);
        assertEquals("/var/data/stash", update.getNewDir());
        assertEquals("/users/jdoe/stash-home", update.getOldDir());
        for (HomeUpdate value : updateCaptor.getAllValues()) {
            assertSame(update, value);
        }

        verify(pluginAccessor).getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class));
        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService, never()).setLastSharedHomeDir(anyString()); //Should not capture the new home directory
        verifyNoMoreInteractions(propertiesService);
    }

    @Test
    public void testOnPluginFrameworkStartedRollsBackInReverseOrder() {
        prepareHome("/var/data/stash", "/users/jdoe/stash-home");

        HomeUpdateHandler first = mock(HomeUpdateHandler.class);
        HomeUpdateHandler fourth = mock(HomeUpdateHandler.class);
        ModuleFactory moduleFactory = mock(ModuleFactory.class);
        HomeUpdateHandlerModuleDescriptor firstDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 10);
        HomeUpdateHandlerModuleDescriptor secondDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 20);
        HomeUpdateHandlerModuleDescriptor thirdDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 30);
        HomeUpdateHandlerModuleDescriptor fourthDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 40);

        HomeUpdateHandler second = mock(HomeUpdateHandler.class);
        doThrow(UnsupportedOperationException.class).when(second).rollback(any(HomeUpdate.class));

        HomeUpdateHandler third = mock(HomeUpdateHandler.class);
        doThrow(new IntegrityException(new KeyedMessage("key", "localised", "root")))
                .when(third).apply(any(HomeUpdate.class));

        when(moduleFactory.createModule(isNull(String.class), same(firstDescriptor))).thenReturn(first);
        when(moduleFactory.createModule(isNull(String.class), same(secondDescriptor))).thenReturn(second);
        when(moduleFactory.createModule(isNull(String.class), same(thirdDescriptor))).thenReturn(third);
        when(moduleFactory.createModule(isNull(String.class), same(fourthDescriptor))).thenReturn(fourth);
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class)))
                //The descriptor order here intentionally doesn't match the weights, to verify sorting is applied
                .thenReturn(Arrays.asList(fourthDescriptor, secondDescriptor, firstDescriptor, thirdDescriptor));

        tracker.onPluginFrameworkStarted(event);

        ArgumentCaptor<AddEvent> addCaptor = ArgumentCaptor.forClass(AddEvent.class);
        ArgumentCaptor<HomeUpdate> updateCaptor = ArgumentCaptor.forClass(HomeUpdate.class);

        verify(eventPublisher, times(1)).publish(addCaptor.capture());
        verify(eventPublisher, never()).publish(isA(RemoveEvent.class)); //Should never remove the lock

        AddEvent add = addCaptor.getValue();
        assertNotNull(add.getEvent());
        assertSame(tracker, add.getSource());
        assertTrue(add.getEvent().getDesc().equals("bitbucket.shared.home.updatefailed"));
        assertEquals(EventLevel.FATAL, add.getEvent().getLevel().getLevel()); //Should have been promoted to FATAL

        InOrder ordered = inOrder(first, second, third);
        ordered.verify(first).apply(updateCaptor.capture()); //Applies successfully
        ordered.verify(second).apply(updateCaptor.capture()); //Applies successfully
        ordered.verify(third).apply(updateCaptor.capture()); //Throws and breaks out of the apply processing
        ordered.verify(second).rollback(updateCaptor.capture()); //Throws and the exception is ignored
        ordered.verify(first).rollback(updateCaptor.capture()); //Exception from second shouldn't prevent rollback here
        verify(third, never()).rollback(any(HomeUpdate.class)); //The handler that throws isn't rolled back
        verifyZeroInteractions(fourth); //Should never be applied, because third failed, and never be rolled back

        HomeUpdate update = updateCaptor.getValue();
        assertNotNull(update);
        assertEquals("/var/data/stash", update.getNewDir());
        assertEquals("/users/jdoe/stash-home", update.getOldDir());
        for (HomeUpdate value : updateCaptor.getAllValues()) { //Should be the same update on all handlers
            assertSame(update, value);
        }

        verify(pluginAccessor).getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class));
        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService, never()).setLastSharedHomeDir(anyString()); //Should not capture the new home directory
        verifyNoMoreInteractions(propertiesService);
    }

    @Test
    public void testOnPluginFrameworkStartedRollsBackOnException() {
        prepareHome("/var/data/stash", "/users/jdoe/stash-home");

        HomeUpdateHandler first = mock(HomeUpdateHandler.class);
        ModuleFactory moduleFactory = mock(ModuleFactory.class);
        HomeUpdateHandlerModuleDescriptor firstDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 10);
        HomeUpdateHandlerModuleDescriptor secondDescriptor = new HomeUpdateHandlerModuleDescriptor(moduleFactory, 20);

        HomeUpdateHandler second = mock(HomeUpdateHandler.class);
        doThrow(new IntegrityException(new KeyedMessage("key", "localised", "root")))
                .when(second).apply(any(HomeUpdate.class));

        when(moduleFactory.createModule(isNull(String.class), same(firstDescriptor))).thenReturn(first);
        when(moduleFactory.createModule(isNull(String.class), same(secondDescriptor))).thenReturn(second);
        when(pluginAccessor.getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class)))
                .thenReturn(Arrays.asList(firstDescriptor, secondDescriptor));

        tracker.onPluginFrameworkStarted(event);

        ArgumentCaptor<AddEvent> addCaptor = ArgumentCaptor.forClass(AddEvent.class);
        ArgumentCaptor<HomeUpdate> updateCaptor = ArgumentCaptor.forClass(HomeUpdate.class);

        verify(eventPublisher, times(1)).publish(addCaptor.capture());
        verify(eventPublisher, never()).publish(isA(RemoveEvent.class)); //Should never remove the lock

        AddEvent add = addCaptor.getValue();
        assertNotNull(add.getEvent());
        assertSame(tracker, add.getSource());
        assertTrue(add.getEvent().getDesc().equals("bitbucket.shared.home.updatefailed"));
        assertEquals(EventLevel.FATAL, add.getEvent().getLevel().getLevel()); //Should have been promoted to FATAL

        InOrder ordered = inOrder(first, second);
        ordered.verify(first).apply(updateCaptor.capture());
        ordered.verify(second).apply(updateCaptor.capture());
        ordered.verify(first).rollback(updateCaptor.capture());
        verify(second, never()).rollback(any(HomeUpdate.class)); //The handler that throws isn't rolled back

        HomeUpdate update = updateCaptor.getValue();
        assertNotNull(update);
        assertEquals("/var/data/stash", update.getNewDir());
        assertEquals("/users/jdoe/stash-home", update.getOldDir());

        verify(pluginAccessor).getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class));
        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService, never()).setLastSharedHomeDir(anyString()); //Should not capture the new home directory
        verifyNoMoreInteractions(propertiesService);
    }

    @Test
    public void testOnPluginFrameworkStartedWithNoHandlers() {
        prepareHome("/var/data/stash", "/users/jdoe/stash-home");

        tracker.onPluginFrameworkStarted(event);

        verify(pluginAccessor).getEnabledModuleDescriptorsByClass(same(HomeUpdateHandlerModuleDescriptor.class));
        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService).setLastSharedHomeDir(eq("/var/data/stash"));
        verifyNoMoreInteractions(propertiesService);
        verifyZeroInteractions(eventPublisher);
    }

    @Test
    public void testOnPluginFrameworkStartedWithSameLocationForUnix() {
        prepareHome("/var/data/stash");

        tracker.onPluginFrameworkStarted(event);

        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService, never()).setLastSharedHomeDir(anyString());
        verifyZeroInteractions(eventPublisher);
        verifyZeroInteractions(pluginAccessor);
    }

    @Test
    public void testOnPluginFrameworkStartedWithSameLocationForWindows() {
        prepareHome("C:\\Stash\\Home");

        tracker.onPluginFrameworkStarted(event);

        verify(propertiesService).getSharedHomeDir();
        verify(propertiesService).getLastSharedHomeDir();
        verify(propertiesService, never()).setLastSharedHomeDir(anyString());
        verifyZeroInteractions(eventPublisher);
        verifyZeroInteractions(pluginAccessor);
    }

    private void prepareCurrentHome(String currentPath) {
        //Using a mock File allows testing Unix and Windows paths regardless of the OS for the JVM running the tests
        File file = mock(File.class);
        when(file.getAbsolutePath()).thenReturn(currentPath);

        when(propertiesService.getSharedHomeDir()).thenReturn(file);
    }

    private void prepareHome(String path) {
        prepareHome(path, path);
    }

    private void prepareHome(String currentPath, String lastPath) {
        prepareCurrentHome(currentPath);

        when(propertiesService.getLastSharedHomeDir()).thenReturn(lastPath);
    }
}
