package com.atlassian.stash.internal.request;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.cluster.ClusterInformation;
import com.atlassian.bitbucket.cluster.ClusterNode;
import com.atlassian.bitbucket.cluster.ClusterService;
import com.atlassian.bitbucket.event.cluster.ClusterNodeAddedEvent;
import com.atlassian.bitbucket.event.cluster.ClusterNodeRemovedEvent;
import com.atlassian.bitbucket.event.request.RequestEndedEvent;
import com.atlassian.bitbucket.event.request.RequestStartedEvent;
import com.atlassian.bitbucket.request.RequestCallback;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestInfoProvider;
import com.atlassian.event.api.EventPublisher;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.Set;
import java.util.regex.Pattern;

import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.*;

/**
 * unit test for {@link DefaultRequestManager}
 */
@RunWith(MockitoJUnitRunner.class)
public class DefaultRequestManagerTest {

    public static final String PROTOCOL = "protocol";
    public static final String DETAILS = "details";
    public static final String REQUEST = "request";
    public static final String ADDRESS = "address";
    public static final String SESSION_ID = "sessionId";
    public static final String LOCAL_NODE_ID = "ID";

    @Mock
    private AuthenticationContext authContext;
    @Mock
    ClusterInformation clusterInformation;
    @Mock
    ClusterNode clusterNode;
    @Mock
    ClusterService clusterService;
    @Mock
    private EventPublisher eventPublisher;
    @Mock
    private RequestInfoProvider requestInfoProvider;
    private DefaultRequestManager requestManager;

    @Before
    public void setupRequestInfoProvider() {
        when(clusterNode.getId()).thenReturn(LOCAL_NODE_ID);
        when(clusterInformation.getLocalNode()).thenReturn(clusterNode);
        when(clusterService.getInformation()).thenReturn(clusterInformation);
        when(clusterService.isAvailable()).thenReturn(false);

        when(requestInfoProvider.getProtocol()).thenReturn(PROTOCOL);
        when(requestInfoProvider.getDetails()).thenReturn(DETAILS);
        when(requestInfoProvider.getRawRequest()).thenReturn(REQUEST);
        when(requestInfoProvider.getRemoteAddress()).thenReturn(ADDRESS);
        when(requestInfoProvider.getSessionId()).thenReturn(SESSION_ID);

        createRequestManager();
    }

    @Test
    public void testReturnsCallbackReturnValue() {
        final String returnValue = "return";

        assertEquals(returnValue, requestManager.doAsRequest(new RequestCallback<Object, RuntimeException>() {
            @Override
            public Object withRequest(@Nonnull RequestContext requestContext) throws RuntimeException {
                return returnValue;
            }
        }, requestInfoProvider));
    }

    @Test
    public void testSendsRequestEvents() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull RequestContext requestContext) throws RuntimeException {
                verifyRequestContext(requestContext);

                // verify that the RequestStartedEvent has been sent and that it is backed by the data provided by the
                // requestInfoProvider
                ArgumentCaptor<RequestStartedEvent> startedEvent = ArgumentCaptor.forClass(RequestStartedEvent.class);
                verify(eventPublisher).publish(startedEvent.capture());
                assertSame(requestContext, startedEvent.getValue().getRequestContext());

                verify(eventPublisher, never()).publish(isA(RequestEndedEvent.class));
                reset(eventPublisher);
                return null;
            }
        }, requestInfoProvider);

        ArgumentCaptor<RequestEndedEvent> endedEvent = ArgumentCaptor.forClass(RequestEndedEvent.class);
        verify(eventPublisher).publish(endedEvent.capture());
        verifyRequestContext(endedEvent.getValue().getRequestContext());

    }

    @Test
    public void testNoNestedRequests() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull final RequestContext outerContext) {

                // nest a call to doAsRequest with a different RequestInfoProvider. This should be ignored; if
                return requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
                    @Override
                    public Void withRequest(@Nonnull RequestContext innerContext) {
                        assertSame(outerContext, innerContext);
                        return null;
                    }
                }, mock(RequestInfoProvider.class));
            }
        }, requestInfoProvider);
    }

    @Test
    public void testRecordsDuration() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull final RequestContext context) {
                context.addCleanupCallback(() ->
                        assertThat(context.getDuration().get().toMillis(), is(greaterThan(0L))));
                return null;
            }
        }, requestInfoProvider);

        requestManager.getRequestContext();
    }

    @Test
    public void testRequestMetaDataInRequest() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull final RequestContext outerContext) {
                assertNotNull(requestManager.getRequestMetadata());
                assertEquals(requestManager.getRequestMetadata(), requestManager.getRequestContext());
                return null;
            }
        }, requestInfoProvider);
    }

    @Test
    public void testRequestMetaDataAfterStatefulApply() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull final RequestContext outerContext) {
                requestManager.getState().apply();
                //should now have our own copy of the state
                assertNotNull(requestManager.getRequestMetadata());
                assertNotEquals(requestManager.getRequestMetadata(), requestManager.getRequestContext());
                return null;
            }
        }, requestInfoProvider);
    }

    @Test
    public void testRequestMetaDataAfterStatefulRemove() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull final RequestContext outerContext) {
                requestManager.getState().remove();
                //should now have reverted to picking up the original provider
                assertNotNull(requestManager.getRequestMetadata());
                assertEquals(requestManager.getRequestMetadata(), requestManager.getRequestContext());

                return null;
            }
        }, requestInfoProvider);
    }


    @Test
    public void testRequestIdWhenClustered() {
        when(clusterService.isClustered()).thenReturn(true);

        createRequestManager();

        assertClusteredRequestId();
    }

    @Test
    public void testRequestIdWhenNotClustered() {
        when(clusterService.isClustered()).thenReturn(false);

        createRequestManager();

        assertNonClusteredRequestId();
    }

    @Test
    public void testRequestIdWhenNotClusteredThenClusteredThenNonClustered() {
        Set<ClusterNode> nodes = ImmutableSet.<ClusterNode>builder().add(mock(ClusterNode.class)).add(mock(ClusterNode.class)).build();

        createRequestManager();

        assertNonClusteredRequestId();

        requestManager.onClusterMembershipChanged(
                new ClusterNodeAddedEvent(this, mock(ClusterNode.class), nodes));

        assertClusteredRequestId();

        requestManager.onClusterMembershipChanged(
                new ClusterNodeRemovedEvent(this, mock(ClusterNode.class), Collections.<ClusterNode>emptySet()));

        assertNonClusteredRequestId();
    }

    private void assertClusteredRequestId() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull final RequestContext outerContext) {
                assertTrue(outerContext.getId() + " doesn't match expected clustered request pattern",
                        Pattern.compile("\\*[A-Z2-9]+x\\d+x\\d+x\\d+").matcher(outerContext.getId()).matches());
                return null;
            }
        }, requestInfoProvider);
    }

    private void assertNonClusteredRequestId() {
        requestManager.doAsRequest(new RequestCallback<Void, RuntimeException>() {
            @Override
            public Void withRequest(@Nonnull final RequestContext outerContext) {
                assertTrue(outerContext.getId() + " doesn't match expected non-clustered request pattern",
                        Pattern.compile("@[A-Z2-9]+x\\d+x\\d+x\\d+").matcher(outerContext.getId()).matches());
                return null;
            }
        }, requestInfoProvider);
    }

    private void verifyRequestContext(RequestContext context) {
        assertNotNull(context);
        assertNotNull(context.getId());
        assertEquals(PROTOCOL, context.getProtocol());
        assertEquals(DETAILS, context.getDetails());
        assertEquals(REQUEST, context.getRawRequest());
        assertEquals(ADDRESS, context.getRemoteAddress());
        assertEquals(SESSION_ID, context.getSessionId());
    }

    private void createRequestManager() {
        requestManager = new DefaultRequestManager(authContext, eventPublisher);
        requestManager.setClusterService(clusterService);
    }
}
