package de.idealo.logback.appender.jediswriter;

import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.anyVararg;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import de.idealo.logback.appender.jedisclient.JedisClient;
import de.idealo.logback.appender.jediswriter.AbstractBufferedJedisWriter;
import de.idealo.logback.appender.jediswriter.BufferedJedisRPusher;

import ch.qos.logback.core.spi.DeferredProcessingAware;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.exceptions.JedisConnectionException;

public class BufferedJedisRPusherTest {
    private static final String KEY = "TEST_KEY";
    private static final int DEFAULT_QUEUE_ITEMS = 3;
    private static final int DEFAULT_BATCH_WAIT_MILLIS = 100;

    @Mock
    private JedisClient client;
    @Mock
    private Function<DeferredProcessingAware, String> messageCreator;
    @Mock
    private Pipeline pipeline;

    private AbstractBufferedJedisWriter writer;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
        when(messageCreator.apply(Matchers.any())).thenAnswer(invocation -> String.valueOf(invocation.getArgumentAt(0, DeferredProcessingAware.class)));
        final Optional<Pipeline> defaultPipeline = Optional.of(pipeline);
        when(client.getPipeline()).thenReturn(defaultPipeline);

        writer = new BufferedJedisRPusher(client, messageCreator, KEY, DEFAULT_QUEUE_ITEMS, DEFAULT_BATCH_WAIT_MILLIS);
    }

    @After
    public void shutdown() {
        writer.close();
    }

    @Test
    public void flusher_thread_is_running() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(Math.round(DEFAULT_BATCH_WAIT_MILLIS * 2.5));
        Assert.assertEquals(2, writer.getFlusherThreadActions());
    }

    @Test
    public void ignore_null_events() throws InterruptedException {
        for (int i = 0; i < DEFAULT_QUEUE_ITEMS; i++) {
            writer.append(null);
        }
        verify(pipeline, times(0)).rpush(anyString(), anyVararg());
        verify(pipeline, times(0)).sync();
    }

    @Test
    public void send_on_full_queue() throws InterruptedException {
        int batchFullEvents = 5;
        for (int i = 0; i < DEFAULT_QUEUE_ITEMS * batchFullEvents; i++) {
            writer.append(mock(DeferredProcessingAware.class));
        }
        verify(pipeline, times(batchFullEvents)).rpush(anyString(), anyVararg());
        verify(pipeline, times(batchFullEvents)).sync();
    }

    @Test
    public void send_on_second_try_due_to_no_pipline_on_first_try() throws InterruptedException {
        when(client.getPipeline()).thenReturn(Optional.empty()).thenReturn(Optional.of(pipeline));
        for (int i = 0; i < DEFAULT_QUEUE_ITEMS; i++) {
            writer.append(mock(DeferredProcessingAware.class));
        }
        verify(client, times(2)).getPipeline();
        verify(client).reconnect();
        verify(pipeline, times(1)).rpush(anyString(), anyVararg());
        verify(pipeline, times(1)).sync();
    }

    @Test
    public void send_on_second_try_due_to_exception_on_first_rpush() throws InterruptedException {
        when(pipeline.rpush(anyString(), anyVararg())).thenThrow(new JedisConnectionException("")).thenReturn(null);
        for (int i = 0; i < DEFAULT_QUEUE_ITEMS; i++) {
            writer.append(mock(DeferredProcessingAware.class));
        }
        verify(client, times(2)).getPipeline();
        verify(client).reconnect();
        verify(pipeline, times(2)).rpush(anyString(), anyVararg());
        verify(pipeline, times(1)).sync();
    }

    @Test
    public void dont_send_on_too_many_failures() throws InterruptedException {
        when(pipeline.rpush(anyString(), anyVararg())).thenThrow(new JedisConnectionException(""));
        for (int i = 0; i < DEFAULT_QUEUE_ITEMS; i++) {
            writer.append(mock(DeferredProcessingAware.class));
        }
        verify(client, times(2)).getPipeline();
        verify(client, times(2)).reconnect();
        verify(pipeline, times(2)).rpush(anyString(), anyVararg());
        verify(pipeline, times(0)).sync();
    }
}