/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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
 *
 *      http://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 com.android.volley;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

import com.android.volley.toolbox.StringRequest;
import com.android.volley.utils.CacheTestUtils;
import java.util.concurrent.BlockingQueue;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
@SuppressWarnings("rawtypes")
public class CacheDispatcherTest {
    private CacheDispatcher mDispatcher;
    private @Mock BlockingQueue<Request<?>> mCacheQueue;
    private @Mock BlockingQueue<Request<?>> mNetworkQueue;
    private @Mock Cache mCache;
    private @Mock ResponseDelivery mDelivery;
    private @Mock Network mNetwork;
    private StringRequest mRequest;

    @Before
    public void setUp() throws Exception {
        initMocks(this);

        mRequest = new StringRequest(Request.Method.GET, "http://foo", null, null);
        mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    }

    private static class WaitForever implements Answer {
        @Override
        public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
            Thread.sleep(Long.MAX_VALUE);
            return null;
        }
    }

    @Test
    public void runStopsOnQuit() throws Exception {
        when(mCacheQueue.take()).then(new WaitForever());
        mDispatcher.start();
        mDispatcher.quit();
        mDispatcher.join(1000);
    }

    private static void verifyNoResponse(ResponseDelivery delivery) {
        verify(delivery, never()).postResponse(any(Request.class), any(Response.class));
        verify(delivery, never())
                .postResponse(any(Request.class), any(Response.class), any(Runnable.class));
        verify(delivery, never()).postError(any(Request.class), any(VolleyError.class));
    }

    // A cancelled request should not be processed at all.
    @Test
    public void cancelledRequest() throws Exception {
        mRequest.cancel();
        mDispatcher.processRequest(mRequest);
        verify(mCache, never()).get(anyString());
        verifyNoResponse(mDelivery);
    }

    // A cache miss does not post a response and puts the request on the network queue.
    @Test
    public void cacheMiss() throws Exception {
        mDispatcher.processRequest(mRequest);
        verifyNoResponse(mDelivery);
        verify(mNetworkQueue).put(mRequest);
        assertNull(mRequest.getCacheEntry());
    }

    // A non-expired cache hit posts a response and does not queue to the network.
    @Test
    public void nonExpiredCacheHit() throws Exception {
        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
        when(mCache.get(anyString())).thenReturn(entry);
        mDispatcher.processRequest(mRequest);
        verify(mDelivery).postResponse(any(Request.class), any(Response.class));
        verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class));
    }

    // A soft-expired cache hit posts a response and queues to the network.
    @Test
    public void softExpiredCacheHit() throws Exception {
        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
        when(mCache.get(anyString())).thenReturn(entry);
        mDispatcher.processRequest(mRequest);

        // Soft expiration needs to use the deferred Runnable variant of postResponse,
        // so make sure it gets to run.
        ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
        verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
        runnable.getValue().run();
        // This way we can verify the behavior of the Runnable as well.
        verify(mNetworkQueue).put(mRequest);
        assertSame(entry, mRequest.getCacheEntry());

        verify(mDelivery, never()).postError(any(Request.class), any(VolleyError.class));
    }

    // An expired cache hit does not post a response and queues to the network.
    @Test
    public void expiredCacheHit() throws Exception {
        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true);
        when(mCache.get(anyString())).thenReturn(entry);
        mDispatcher.processRequest(mRequest);
        verifyNoResponse(mDelivery);
        verify(mNetworkQueue).put(mRequest);
        assertSame(entry, mRequest.getCacheEntry());
    }

    // An fresh cache hit with parse error, does not post a response and queues to the network.
    @Test
    public void freshCacheHit_parseError() throws Exception {
        Request request = mock(Request.class);
        when(request.parseNetworkResponse(any(NetworkResponse.class)))
                .thenReturn(Response.error(new ParseError()));
        when(request.getCacheKey()).thenReturn("cache/key");
        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
        when(mCache.get(anyString())).thenReturn(entry);

        mDispatcher.processRequest(request);

        verifyNoResponse(mDelivery);
        verify(mNetworkQueue).put(request);
        assertNull(request.getCacheEntry());
        verify(mCache).invalidate("cache/key", true);
        verify(request).addMarker("cache-parsing-failed");
    }

    @Test
    public void duplicateCacheMiss() throws Exception {
        StringRequest secondRequest =
                new StringRequest(Request.Method.GET, "http://foo", null, null);
        mRequest.setSequence(1);
        secondRequest.setSequence(2);
        mDispatcher.processRequest(mRequest);
        mDispatcher.processRequest(secondRequest);
        verify(mNetworkQueue).put(mRequest);
        verifyNoResponse(mDelivery);
    }

    @Test
    public void tripleCacheMiss_networkErrorOnFirst() throws Exception {
        StringRequest secondRequest =
                new StringRequest(Request.Method.GET, "http://foo", null, null);
        StringRequest thirdRequest =
                new StringRequest(Request.Method.GET, "http://foo", null, null);
        mRequest.setSequence(1);
        secondRequest.setSequence(2);
        thirdRequest.setSequence(3);
        mDispatcher.processRequest(mRequest);
        mDispatcher.processRequest(secondRequest);
        mDispatcher.processRequest(thirdRequest);

        verify(mNetworkQueue).put(mRequest);
        verifyNoResponse(mDelivery);

        ((Request<?>) mRequest).notifyListenerResponseNotUsable();
        // Second request should now be in network queue.
        verify(mNetworkQueue).put(secondRequest);
        // Another unusable response, third request should now be added.
        ((Request<?>) secondRequest).notifyListenerResponseNotUsable();
        verify(mNetworkQueue).put(thirdRequest);
    }

    @Test
    public void duplicateSoftExpiredCacheHit_failedRequest() throws Exception {
        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
        when(mCache.get(anyString())).thenReturn(entry);

        StringRequest secondRequest =
                new StringRequest(Request.Method.GET, "http://foo", null, null);
        mRequest.setSequence(1);
        secondRequest.setSequence(2);

        mDispatcher.processRequest(mRequest);
        mDispatcher.processRequest(secondRequest);

        // Soft expiration needs to use the deferred Runnable variant of postResponse,
        // so make sure it gets to run.
        ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
        verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
        runnable.getValue().run();
        // This way we can verify the behavior of the Runnable as well.

        verify(mNetworkQueue).put(mRequest);
        verify(mDelivery)
                .postResponse(any(Request.class), any(Response.class), any(Runnable.class));

        ((Request<?>) mRequest).notifyListenerResponseNotUsable();
        // Second request should now be in network queue.
        verify(mNetworkQueue).put(secondRequest);
    }

    @Test
    public void duplicateSoftExpiredCacheHit_successfulRequest() throws Exception {
        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true);
        when(mCache.get(anyString())).thenReturn(entry);

        StringRequest secondRequest =
                new StringRequest(Request.Method.GET, "http://foo", null, null);
        mRequest.setSequence(1);
        secondRequest.setSequence(2);

        mDispatcher.processRequest(mRequest);
        mDispatcher.processRequest(secondRequest);

        // Soft expiration needs to use the deferred Runnable variant of postResponse,
        // so make sure it gets to run.
        ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
        verify(mDelivery).postResponse(any(Request.class), any(Response.class), runnable.capture());
        runnable.getValue().run();
        // This way we can verify the behavior of the Runnable as well.

        verify(mNetworkQueue).put(mRequest);
        verify(mDelivery)
                .postResponse(any(Request.class), any(Response.class), any(Runnable.class));

        ((Request<?>) mRequest).notifyListenerResponseReceived(Response.success(null, entry));
        // Second request should have delivered response.
        verify(mNetworkQueue, never()).put(secondRequest);
        verify(mDelivery)
                .postResponse(any(Request.class), any(Response.class), any(Runnable.class));
    }

    @Test
    public void processRequestNotifiesListener() throws Exception {
        RequestQueue.RequestEventListener listener = mock(RequestQueue.RequestEventListener.class);
        RequestQueue queue = new RequestQueue(mCache, mNetwork, 0, mDelivery);
        queue.addRequestEventListener(listener);
        mRequest.setRequestQueue(queue);

        Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false);
        when(mCache.get(anyString())).thenReturn(entry);
        mDispatcher.processRequest(mRequest);

        InOrder inOrder = inOrder(listener);
        inOrder.verify(listener)
                .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_STARTED);
        inOrder.verify(listener)
                .onRequestEvent(mRequest, RequestQueue.RequestEvent.REQUEST_CACHE_LOOKUP_FINISHED);
        inOrder.verifyNoMoreInteractions();
    }
}