/*
 * Copyright (C) 2015 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 com.android.volley.Request.Priority;
import com.android.volley.RequestQueue.RequestFinishedListener;
import com.android.volley.mock.MockRequest;
import com.android.volley.mock.ShadowSystemClock;
import com.android.volley.toolbox.NoCache;
import com.android.volley.utils.ImmediateResponseDelivery;
import com.crossbow.BuildConfig;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;


/**
 * Integration tests for {@link RequestQueue}, that verify its behavior in conjunction with real dispatcher, queues and
 * Requests. Network is mocked out
 */
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, shadows = {ShadowSystemClock.class})
public class RequestQueueIntegrationTest {

    private ResponseDelivery mDelivery;
    @Mock private Network mMockNetwork;

    @Before public void setUp() throws Exception {
        mDelivery = new ImmediateResponseDelivery();
        initMocks(this);
    }

    @Test public void add_requestProcessedInCorrectOrder() throws Exception {
        // Enqueue 2 requests with different cache keys, and different priorities. The second, higher priority request
        // takes 20ms.
        // Assert that first request is only handled after the first one has been parsed and delivered.
        MockRequest lowerPriorityReq = new MockRequest();
        MockRequest higherPriorityReq = new MockRequest();
        lowerPriorityReq.setCacheKey("1");
        higherPriorityReq.setCacheKey("2");
        lowerPriorityReq.setPriority(Priority.LOW);
        higherPriorityReq.setPriority(Priority.HIGH);

        RequestFinishedListener listener = mock(RequestFinishedListener.class);
        Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
            @Override
            public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
                Thread.sleep(20);
                return mock(NetworkResponse.class);
            }
        };
        //delay only for higher request
        when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer);
        when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class));

        RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);
        queue.addRequestFinishedListener(listener);
        queue.add(lowerPriorityReq);
        queue.add(higherPriorityReq);
        queue.start();

        // you cannot do strict order verification in combination with timeouts with mockito 1.9.5 :(
        // as an alternative, first verify no requests have finished, while higherPriorityReq should be processing
        verifyNoMoreInteractions(listener);
        // verify higherPriorityReq goes through first
        verify(listener, timeout(100)).onRequestFinished(higherPriorityReq);
        // verify lowerPriorityReq goes last
        verify(listener, timeout(10)).onRequestFinished(lowerPriorityReq);
        queue.stop();
    }

    /**
     * Asserts that requests with same cache key are processed in order.
     *
     * Needs to be an integration test because relies on complex interations between various queues
     */
    @Test public void add_dedupeByCacheKey() throws Exception {
        // Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the
        // second request is only handled after the first one has been parsed and delivered.
        Request req1 = new MockRequest();
        Request req2 = new MockRequest();
        RequestFinishedListener listener = mock(RequestFinishedListener.class);
        Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
            @Override
            public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
                Thread.sleep(20);
                return mock(NetworkResponse.class);
            }
        };
        //delay only for first
        when(mMockNetwork.performRequest(req1)).thenAnswer(delayAnswer);
        when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class));

        RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery);
        queue.addRequestFinishedListener(listener);
        queue.add(req1);
        queue.add(req2);
        queue.start();

        // you cannot do strict order verification with mockito 1.9.5 :(
        // as an alternative, first verify no requests have finished, then verify req1 goes through
        verifyNoMoreInteractions(listener);
        verify(listener, timeout(100)).onRequestFinished(req1);
        verify(listener, timeout(10)).onRequestFinished(req2);
        queue.stop();
    }

    /**
     * Verify RequestFinishedListeners are informed when requests are canceled
     *
     * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
     */
    @Test public void add_requestFinishedListenerCanceled() throws Exception {
        RequestFinishedListener listener = mock(RequestFinishedListener.class);
        Request request = new MockRequest();
        Answer<NetworkResponse> delayAnswer = new Answer<NetworkResponse>() {
            @Override
            public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
                Thread.sleep(200);
                return mock(NetworkResponse.class);
            }
        };
        RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);

        when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer);

        queue.addRequestFinishedListener(listener);
        queue.start();
        queue.add(request);

        request.cancel();
        verify(listener, timeout(100)).onRequestFinished(request);
        queue.stop();
    }

    /**
     * Verify RequestFinishedListeners are informed when requests are successfully delivered
     *
     * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
     */
    @Test public void add_requestFinishedListenerSuccess() throws Exception {
        NetworkResponse response = mock(NetworkResponse.class);
        Request request = new MockRequest();
        RequestFinishedListener listener = mock(RequestFinishedListener.class);
        RequestFinishedListener listener2 = mock(RequestFinishedListener.class);
        RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);

        queue.addRequestFinishedListener(listener);
        queue.addRequestFinishedListener(listener2);
        queue.start();
        queue.add(request);

        verify(listener, timeout(100)).onRequestFinished(request);
        verify(listener2, timeout(100)).onRequestFinished(request);

        queue.stop();
    }

    /**
     * Verify RequestFinishedListeners are informed when request errors
     *
     * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction
     */
    @Test public void add_requestFinishedListenerError() throws Exception {
        RequestFinishedListener listener = mock(RequestFinishedListener.class);
        Request request = new MockRequest();
        RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery);

        when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError());

        queue.addRequestFinishedListener(listener);
        queue.start();
        queue.add(request);

        verify(listener, timeout(100)).onRequestFinished(request);
        queue.stop();
    }

}
