/*
 * Copyright 2014-2020 Real Logic Limited.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.agrona.concurrent;

import org.agrona.ErrorHandler;
import org.agrona.LangUtil;
import org.agrona.concurrent.status.AtomicCounter;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.nio.channels.ClosedByInterruptException;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

public class AgentInvokerTest
{
    private final ErrorHandler mockErrorHandler = mock(ErrorHandler.class);
    private final AtomicCounter mockAtomicCounter = mock(AtomicCounter.class);
    private final Agent mockAgent = mock(Agent.class);
    private final AgentInvoker invoker = new AgentInvoker(mockErrorHandler, mockAtomicCounter, mockAgent);

    @Test
    public void shouldFollowLifecycle() throws Exception
    {
        invoker.start();
        invoker.start();
        verify(mockAgent, times(1)).onStart();
        verifyNoMoreInteractions(mockAgent);

        invoker.invoke();
        invoker.invoke();
        verify(mockAgent, times(2)).doWork();
        verifyNoMoreInteractions(mockAgent);

        invoker.close();
        invoker.close();
        verify(mockAgent, times(1)).onClose();
        verifyNoMoreInteractions(mockAgent);
    }

    @Test
    public void shouldReturnAgent()
    {
        assertThat(invoker.agent(), is(mockAgent));
    }

    @Test
    public void shouldNotDoWorkOnClosedRunnerButCallOnClose() throws Exception
    {
        invoker.close();
        invoker.invoke();

        verify(mockAgent, never()).onStart();
        verify(mockAgent, never()).doWork();
        verify(mockErrorHandler, never()).onError(any());
        verify(mockAtomicCounter, never()).increment();
        verify(mockAgent).onClose();
    }

    @Test
    public void shouldReportExceptionThrownByAgent() throws Exception
    {
        final RuntimeException expectedException = new RuntimeException();
        when(mockAgent.doWork()).thenThrow(expectedException);

        invoker.start();
        invoker.invoke();

        verify(mockAgent).doWork();
        verify(mockErrorHandler).onError(expectedException);
        verify(mockAtomicCounter).increment();
        verify(mockAgent, never()).onClose();
        reset(mockAgent);

        invoker.invoke();

        verify(mockAgent).doWork();
        reset(mockAgent);

        invoker.close();

        verify(mockAgent, never()).doWork();
        verify(mockAgent).onClose();
    }

    @Test
    public void shouldReportExceptionThrownOnStart() throws Exception
    {
        final RuntimeException expectedException = new RuntimeException();
        Mockito.doThrow(expectedException).when(mockAgent).onStart();

        invoker.start();
        invoker.invoke();

        verify(mockAgent, never()).doWork();
        verify(mockErrorHandler).onError(expectedException);
        verify(mockAgent).onClose();

        assertTrue(invoker.isStarted());
        assertFalse(invoker.isRunning());
        assertTrue(invoker.isClosed());
    }

    @Test
    public void shouldHandleAgentTerminationExceptionThrownByAgent() throws Exception
    {
        final RuntimeException expectedException = new AgentTerminationException();
        when(mockAgent.doWork()).thenThrow(expectedException);

        invoker.start();
        invoker.invoke();

        verify(mockAgent).doWork();
        verify(mockErrorHandler).onError(expectedException);
        verify(mockAtomicCounter, never()).increment();
        verify(mockAgent).onClose();
        assertTrue(invoker.isClosed());

        reset(mockAgent);
        invoker.invoke();

        verify(mockAgent, never()).doWork();
        assertTrue(invoker.isClosed());
    }

    @Test
    public void shouldNotReportClosedByInterruptException() throws Exception
    {
        when(mockAgent.doWork()).thenThrow(new ClosedByInterruptException());

        assertExceptionNotReported();
        assertTrue(Thread.interrupted()); // by throwing ClosedByInterruptException
    }

    @Test
    public void shouldNotReportRethrownClosedByInterruptException() throws Exception
    {
        when(mockAgent.doWork()).thenAnswer(
            (inv) ->
            {
                try
                {
                    throw new ClosedByInterruptException();
                }
                catch (final ClosedByInterruptException ex)
                {
                    LangUtil.rethrowUnchecked(ex);
                }

                return null;
            });

        assertExceptionNotReported();
        assertTrue(Thread.interrupted()); // by throwing ClosedByInterruptException
    }

    private void assertExceptionNotReported()
    {
        invoker.start();
        invoker.invoke();
        invoker.close();

        verify(mockErrorHandler, never()).onError(any());
        verify(mockAtomicCounter, never()).increment();
    }
}