package com.atlassian.stash.internal.aop;

import com.atlassian.bitbucket.throttle.ResourceBusyException;
import com.atlassian.bitbucket.i18n.KeyedMessage;
import com.atlassian.stash.internal.annotation.Throttled;
import com.atlassian.bitbucket.throttle.ThrottleService;
import com.atlassian.bitbucket.throttle.Ticket;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;

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

@RunWith(MockitoJUnitRunner.class)
public class ThrottledAspectTest {

    private static final String RESOURCE_NAME = "some-resource";

    private Throttled annotation;
    @InjectMocks
    private ThrottledAspect aspect;
    @Mock
    private ProceedingJoinPoint joinPoint;
    @Mock
    private ThrottleService throttleService;
    @Mock
    private Ticket ticket;

    @Before
    public void setUp() throws Exception {
        annotation = AnnotationUtils.findAnnotation(ThrottledClass.class, Throttled.class);
        
        when(throttleService.acquireTicket(eq(RESOURCE_NAME))).thenReturn(ticket);
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testReleaseOnThrow() throws Throwable {
        doThrow(UnsupportedOperationException.class).when(joinPoint).proceed();

        try {
            aspect.throttle(joinPoint, annotation);
        } finally {
            verifyThrottling();
        }
    }

    @Test(expected = ResourceBusyException.class)
    public void testResourceBusyExceptionPropagates() throws Throwable {
        doThrow(new ResourceBusyException(mock(KeyedMessage.class), RESOURCE_NAME))
                .when(throttleService).acquireTicket(eq(RESOURCE_NAME));

        aspect.throttle(joinPoint, annotation);
    }

    @Test
    public void testThrottleWithAnnotatedClass() throws Throwable {
        //When the annotation is present on the class, Spring will provide it directly to the throttle(...) invocation
        aspect.throttle(joinPoint, annotation);

        verifyThrottling();
    }

    @Test
    public void testThrottleWithAnnotatedMethod() throws Throwable {
        trainForThrottledMethod();

        //When the annotation is present on the method, Spring (or maybe it's AspectJ) will not provide it to the
        //throttle(...) invocation, so the internals look for it on the method.
        aspect.throttle(joinPoint, null);

        verifyThrottling();
    }
    
    private void trainForThrottledMethod() {
        ThrottledMethod target = new ThrottledMethod();

        MethodSignature signature = mock(MethodSignature.class);
        when(signature.getMethod()).thenReturn(ReflectionUtils.findMethod(ThrottledEntity.class, "throttled"));

        when(joinPoint.getSignature()).thenReturn(signature);
        when(joinPoint.getTarget()).thenReturn(target);
    }

    private void verifyThrottling() {
        verify(throttleService).acquireTicket(eq(RESOURCE_NAME)); //Verify a ticket was acquired
        verify(ticket).close(); //Verify the acquired ticket was closed
    }

    @Throttled(RESOURCE_NAME)
    public static class ThrottledClass implements ThrottledEntity {

        public void throttled() {

        }
    }

    public static interface ThrottledEntity {

        @SuppressWarnings("unused") //Used via Reflection in trainForThrottledMethod
        void throttled();
    }

    public static class ThrottledMethod implements ThrottledEntity {

        @Throttled(RESOURCE_NAME)
        public void throttled() {

        }
    }
}
