package com.atlassian.stash.internal.server;

import com.atlassian.johnson.Johnson;
import com.atlassian.johnson.JohnsonEventContainer;
import com.atlassian.johnson.event.Event;
import com.atlassian.johnson.event.EventLevel;
import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
import com.atlassian.bitbucket.AbstractServiceTest;
import com.atlassian.stash.internal.annotation.Throttled;
import com.atlassian.stash.internal.throttle.InternalThrottleService;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.server.ApplicationState;
import com.atlassian.bitbucket.server.ApplicationStatusService;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.servlet.ServletContext;
import java.util.concurrent.TimeUnit;

import static com.atlassian.stash.internal.annotation.Throttled.SCM_HOSTING;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class DefaultApplicationStatusServiceTest extends AbstractServiceTest {

    @Mock
    private ApplicationPropertiesService applicationPropertiesService;
    @Mock
    private JohnsonEventContainer eventContainer;
    @Mock
    private InternalThrottleService throttleService;
    @Mock
    private ServletContext servletContext;
    private DefaultApplicationStatusService service;

    public DefaultApplicationStatusServiceTest() {
        super(DefaultApplicationStatusService.class, ApplicationStatusService.class);
    }

    @Before
    public void setup() {
        when(servletContext.getAttribute(Johnson.ATTR_EVENT_CONTAINER)).thenReturn(eventContainer);
        service = new DefaultApplicationStatusService(applicationPropertiesService, throttleService, servletContext);
    }

    @After
    public void teardown() {
        // leave Johnson in a cleans state
        Johnson.terminate();
    }

    @Test
    public void testQueueing() {
        service.setServerBusyQueueTime(1, TimeUnit.MILLISECONDS);
        when(throttleService.getLongestQueueingTimeForCurrentTicketRequests(SCM_HOSTING)).thenReturn(10L);
        assertTrue(service.isQueueingRequests());
    }

    @Test
    public void testQueuingIgnoresScmCommand() {
        service.setServerBusyQueueTime(1, TimeUnit.MILLISECONDS);

        when(throttleService.getLongestQueueingTimeForCurrentTicketRequests(eq(Throttled.SCM_COMMAND)))
                .thenReturn(100L);

        assertFalse(service.isQueueingRequests());
        verify(throttleService, never()).getLongestQueueingTimeForCurrentTicketRequests(eq(Throttled.SCM_COMMAND));
    }

    @Test
    public void testNotQueueing() {
        service.setServerBusyQueueTime(1, TimeUnit.MINUTES);
        when(throttleService.getLongestQueueingTimeForCurrentTicketRequests(SCM_HOSTING)).thenReturn(1L);
        assertFalse(service.isQueueingRequests());
    }

    @Test
    public void testRejecting() {
        service.setServerBusyMessageTimeout(10); //10 mins
        when(throttleService.getTimeSinceLastRejectedTicketRequest(SCM_HOSTING)).thenReturn(10L);
        assertTrue(service.hasRecentlyRejectedRequests());
    }

    @Test
    public void testRejectingIncludesScmCommand() {
        service.setServerBusyMessageTimeout(10); //10 mins

        when(throttleService.getTimeSinceLastRejectedTicketRequest(eq(Throttled.SCM_COMMAND))).thenReturn(10L);

        assertTrue(service.hasRecentlyRejectedRequests());
        verify(throttleService).getTimeSinceLastRejectedTicketRequest(eq(SCM_HOSTING));
        verify(throttleService).getTimeSinceLastRejectedTicketRequest(eq(Throttled.SCM_COMMAND));
    }

    @Test
    public void testNotRejecting() {
        service.setServerBusyMessageTimeout(1); //1 mins
        when(throttleService.getTimeSinceLastRejectedTicketRequest(SCM_HOSTING)).thenReturn(120L * 1000);
        assertFalse(service.hasRecentlyRejectedRequests());
    }

    @Test
    public void testGetStateStartupAfterSetup() {
        // while it's starting up, the status should be STARTING regardless of whether the application has been set up
        when(applicationPropertiesService.isSetup()).thenReturn(true);
        assertEquals(ApplicationState.STARTING, service.getState());

        // when startup completes, the state should be RUNNING
        service.onStart(mock(PluginFrameworkStartedEvent.class));
        assertEquals(ApplicationState.RUNNING, service.getState());

        service.shutdown();
        assertEquals(ApplicationState.STOPPING, service.getState());
    }

    @Test
    public void testGetStateStartUpFirstRun() {
        // while it's starting up, the status should be STARTING regardless of whether the application has been set up
        when(applicationPropertiesService.isSetup()).thenReturn(false);
        assertEquals(ApplicationState.STARTING, service.getState());

        // when startup completes, the state should be FIRST_RUN
        service.onStart(mock(PluginFrameworkStartedEvent.class));
        assertEquals(ApplicationState.FIRST_RUN, service.getState());

        service.shutdown();
        assertEquals(ApplicationState.STOPPING, service.getState());
    }

    @Test
    public void testGetStateFatalJohnson() {
        // a Johnson event during startup should trigger an error immediately
        mockEvents("fatal");
        assertEquals(ApplicationState.ERROR, service.getState());

        service.onStart(mock(PluginFrameworkStartedEvent.class));
        assertEquals(ApplicationState.ERROR, service.getState());

        service.shutdown();
        assertEquals(ApplicationState.STOPPING, service.getState());
    }

    @Test
    public void testGetStateErrorJohnson() {
        // a Johnson event during startup should trigger an error immediately
        mockEvents("error");
        assertEquals(ApplicationState.ERROR, service.getState());

        service.onStart(mock(PluginFrameworkStartedEvent.class));
        assertEquals(ApplicationState.ERROR, service.getState());

        service.shutdown();
        assertEquals(ApplicationState.STOPPING, service.getState());
    }

    @Test
    public void testGetStateWarningJohnson() {
        // a "warning" Johnson event should be ignored
        mockEvents("warning");
        when(applicationPropertiesService.isSetup()).thenReturn(true);
        assertEquals(ApplicationState.STARTING, service.getState());

        service.onStart(mock(PluginFrameworkStartedEvent.class));
        assertEquals(ApplicationState.RUNNING, service.getState());

        service.shutdown();
        assertEquals(ApplicationState.STOPPING, service.getState());
    }

    @Test
    public void testGetStateMaintenanceJohnson() {
        mockEvents("maintenance");
        assertEquals(ApplicationState.MAINTENANCE, service.getState());

        service.onStart(mock(PluginFrameworkStartedEvent.class));
        assertEquals(ApplicationState.MAINTENANCE, service.getState());

        service.shutdown();
        assertEquals(ApplicationState.STOPPING, service.getState());
    }

    @Test
    public void testGetStateSystemMaintenanceJohnson() {
        mockEvents("system-maintenance");
        assertEquals(ApplicationState.MAINTENANCE, service.getState());

        service.onStart(mock(PluginFrameworkStartedEvent.class));
        assertEquals(ApplicationState.MAINTENANCE, service.getState());

        service.shutdown();
        assertEquals(ApplicationState.STOPPING, service.getState());
    }

    private void mockEvents(String... levels) {
        ImmutableList.Builder<Event> builder = ImmutableList.builder();

        for (String level : levels) {
            Event event = mock(Event.class);
            EventLevel eventLevel = mock(EventLevel.class);
            when(event.getLevel()).thenReturn(eventLevel);
            when(eventLevel.getLevel()).thenReturn(level);

            builder.add(event);
        }

        when(eventContainer.hasEvents()).thenReturn(levels.length > 0);
        when(eventContainer.getEvents()).thenReturn(builder.build());
    }
}
