package com.roskart.dropwizard.jaxws;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.annotation.ExceptionMetered;
import com.codahale.metrics.annotation.Metered;
import com.codahale.metrics.annotation.Timed;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Timer;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.service.invoker.Invoker;
import org.apache.cxf.service.model.BindingOperationInfo;
import org.apache.cxf.service.model.OperationInfo;
import org.junit.Before;
import org.junit.Test;

import java.lang.reflect.Method;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.hamcrest.CoreMatchers.*;

public class InstrumentedInvokerFactoryTest {

    // Test service implementation
    class InstrumentedService {

        public String foo() {
            return "fooReturn";
        }

        @Metered
        public String metered() {
            return "meteredReturn";
        }

        @Timed
        public String timed() {
            return "timedReturn";
        }

        @ExceptionMetered
        public String exceptionMetered(boolean doThrow) {
            if (doThrow) {
                throw new RuntimeException("Runtime exception occured");
            }
            else {
                return "exceptionMeteredReturn";
            }
        }
    }

    MetricRegistry testMetricRegistry;
    MetricRegistry mockMetricRegistry;
    InstrumentedInvokerFactory invokerBuilder;
    InstrumentedService instrumentedService;
    // CXF Exchange contains message exchange and is used by Invoker to obtain invoked method name
    Exchange exchange;

    /* Invokers that invoke test service implementation */

    class FooInvoker implements Invoker {
        @Override
        public Object invoke(Exchange exchange, Object o) {
            return instrumentedService.foo();
        }
    }

    class MeteredInvoker implements Invoker {
        @Override
        public Object invoke(Exchange exchange, Object o) {
            return instrumentedService.metered();
        }
    }

    class TimedInvoker implements Invoker {
        @Override
        public Object invoke(Exchange exchange, Object o) {
            return instrumentedService.timed();
        }
    }

    public class ExceptionMeteredInvoker implements Invoker {
        private boolean doThrow;
        public ExceptionMeteredInvoker(boolean doThrow) {
            this.doThrow = doThrow;
        }
        @Override
        public Object invoke(Exchange exchange, Object o) {
            return instrumentedService.exceptionMetered(doThrow);
        }
    }

    /**
     * Utility method that mimics runtime CXF behaviour. Enables AbstractInvoker.getTargetMethod to work properly
     * during the test.
     */
    private void setTargetMethod(Exchange exchange, String methodName, Class<?>... parameterTypes) {

        try {
            OperationInfo oi = exchange.getBindingOperationInfo().getOperationInfo();
            when(oi.getProperty(Method.class.getName()))
                    .thenReturn(InstrumentedService.class.getMethod(methodName, parameterTypes));
        }
        catch (Exception e) {
            fail("setTargetMethod failed: " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    @Before
    public void setUp() {
        exchange = mock(Exchange.class);

        BindingOperationInfo boi = mock(BindingOperationInfo.class);
        when(exchange.getBindingOperationInfo()).thenReturn(boi);

        OperationInfo oi = mock(OperationInfo.class);
        when(boi.getOperationInfo()).thenReturn(oi);

        testMetricRegistry = new MetricRegistry();
        mockMetricRegistry = mock(MetricRegistry.class);

        invokerBuilder = new InstrumentedInvokerFactory(mockMetricRegistry);
        instrumentedService = new InstrumentedService();
    }

    @Test
    public void noAnnotation() {

        Timer timer = testMetricRegistry.timer("timed");
        Meter meter = testMetricRegistry.meter("metered");
        when(mockMetricRegistry.timer(anyString())).thenReturn(timer);
        when(mockMetricRegistry.meter(anyString())).thenReturn(meter);

        long oldtimervalue = timer.getCount();
        long oldmetervalue = meter.getCount();

        Invoker invoker = invokerBuilder.create(instrumentedService, new FooInvoker());
        this.setTargetMethod(exchange, "foo"); // simulate CXF behavior

        Object result = invoker.invoke(exchange, null);
        assertEquals("fooReturn", result);

        assertThat(timer.getCount(), is(oldtimervalue));
        assertThat(meter.getCount(), is(oldmetervalue));
    }

    @Test
    public void meteredAnnotation() {

        Timer timer = testMetricRegistry.timer("timed");
        Meter meter = testMetricRegistry.meter("metered");
        when(mockMetricRegistry.timer(anyString())).thenReturn(timer);
        when(mockMetricRegistry.meter(anyString())).thenReturn(meter);

        long oldtimervalue = timer.getCount();
        long oldmetervalue = meter.getCount();

        Invoker invoker = invokerBuilder.create(instrumentedService, new MeteredInvoker());
        this.setTargetMethod(exchange, "metered"); // simulate CXF behavior

        Object result = invoker.invoke(exchange, null);
        assertEquals("meteredReturn", result);

        assertThat(timer.getCount(), is(oldtimervalue));
        assertThat(meter.getCount(), is(1 + oldmetervalue));
    }

    @Test
    public void timedAnnotation() {

        Timer timer = testMetricRegistry.timer("timed");
        Meter meter = testMetricRegistry.meter("metered");
        when(mockMetricRegistry.timer(anyString())).thenReturn(timer);
        when(mockMetricRegistry.meter(anyString())).thenReturn(meter);

        long oldtimervalue = timer.getCount();
        long oldmetervalue = meter.getCount();

        Invoker invoker = invokerBuilder.create(instrumentedService, new TimedInvoker());
        this.setTargetMethod(exchange, "timed"); // simulate CXF behavior

        Object result = invoker.invoke(exchange, null);
        assertEquals("timedReturn", result);

        assertThat(timer.getCount(), is(1 + oldtimervalue));
        assertThat(meter.getCount(), is(oldmetervalue));
    }

    @Test
    public void exceptionMeteredAnnotation() {

        Timer timer = testMetricRegistry.timer("timed");
        Meter meter = testMetricRegistry.meter("metered");
        Meter exceptionmeter = testMetricRegistry.meter("exceptionMeteredExceptions");
        when(mockMetricRegistry.timer(anyString())).thenReturn(timer);
        when(mockMetricRegistry.meter(contains("metered"))).thenReturn(meter);
        when(mockMetricRegistry.meter(contains("exceptionMetered"))).thenReturn(exceptionmeter);

        long oldtimervalue = timer.getCount();
        long oldmetervalue = meter.getCount();
        long oldexceptionmetervalue = exceptionmeter.getCount();

        // Invoke InstrumentedResource.exceptionMetered without exception beeing thrown

        Invoker invoker = invokerBuilder.create(instrumentedService, new ExceptionMeteredInvoker(false));
        this.setTargetMethod(exchange, "exceptionMetered", boolean.class); // simulate CXF behavior

        Object result = invoker.invoke(exchange, null);
        assertEquals("exceptionMeteredReturn", result);

        assertThat(timer.getCount(), is(oldtimervalue));
        assertThat(meter.getCount(), is(oldmetervalue));
        assertThat(exceptionmeter.getCount(), is(oldexceptionmetervalue));

        // Invoke InstrumentedResource.exceptionMetered with exception beeing thrown

        invoker = invokerBuilder.create(instrumentedService, new ExceptionMeteredInvoker(true));

        try {
            invoker.invoke(exchange, null);
            fail("Exception shall be thrown here");
        }
        catch (Exception e) {
            assertThat(e, is(instanceOf(RuntimeException.class)));
        }

        assertThat(timer.getCount(), is(oldtimervalue));
        assertThat(meter.getCount(), is(oldmetervalue));
        assertThat(exceptionmeter.getCount(), is(1 + oldexceptionmetervalue));

    }

}