package com.danikula.videocache;

import com.danikula.android.garden.io.IoUtils;
import com.danikula.videocache.file.FileCache;
import com.danikula.videocache.sourcestorage.SourceInfoStorage;
import com.danikula.videocache.sourcestorage.SourceInfoStorageFactory;
import com.danikula.videocache.support.ProxyCacheTestUtils;
import com.danikula.videocache.support.Response;

import org.junit.Test;
import org.mockito.Mockito;
import org.robolectric.RuntimeEnvironment;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_BIG_NAME;
import static com.danikula.videocache.support.ProxyCacheTestUtils.ASSETS_DATA_NAME;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_BIG_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_SIZE;
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadAssetFile;
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadTestData;
import static com.danikula.videocache.support.ProxyCacheTestUtils.newCacheFile;
import static org.fest.assertions.api.Assertions.assertThat;
import static org.fest.assertions.api.Assertions.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * Test {@link HttpProxyCache}.
 *
 * @author Alexey Danilov ([email protected]).
 */
public class HttpProxyCacheTest extends BaseTest {

    @Test
    public void testProcessRequestNoCache() throws Exception {
        Response response = processRequest(HTTP_DATA_URL, "GET /" + HTTP_DATA_URL + " HTTP/1.1");

        assertThat(response.data).isEqualTo(loadTestData());
        assertThat(response.code).isEqualTo(200);
        assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
        assertThat(response.contentType).isEqualTo("image/jpeg");
    }

    @Test
    public void testProcessPartialRequestWithoutCache() throws Exception {
        FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
        FileCache spyFileCache = Mockito.spy(fileCache);
        doThrow(new RuntimeException()).when(spyFileCache).read(any(byte[].class), anyLong(), anyInt());

        String httpRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-";
        Response response = processRequest(HTTP_DATA_URL, httpRequest, spyFileCache);

        byte[] fullData = loadTestData();
        byte[] partialData = new byte[fullData.length - 2000];
        System.arraycopy(fullData, 2000, partialData, 0, partialData.length);
        assertThat(response.data).isEqualTo(partialData);
        assertThat(response.code).isEqualTo(206);
    }

    @Test   // https://github.com/danikula/AndroidVideoCache/issues/43
    public void testPreventClosingOriginalSourceForNewPartialRequestWithoutCache() throws Exception {
        HttpUrlSource source = new HttpUrlSource(HTTP_DATA_BIG_URL);
        FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
        HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
        ExecutorService executor = Executors.newFixedThreadPool(5);
        Future<Response> firstRequestFeature = processAsync(executor, proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
        Thread.sleep(100);  // wait for first request started to process

        int offset = 30000;
        String partialRequest = "GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=" + offset + "-";
        Future<Response> secondRequestFeature = processAsync(executor, proxyCache, partialRequest);

        Response secondResponse = secondRequestFeature.get();
        Response firstResponse = firstRequestFeature.get();

        byte[] responseData = loadAssetFile(ASSETS_DATA_BIG_NAME);
        assertThat(firstResponse.data).isEqualTo(responseData);

        byte[] partialData = new byte[responseData.length - offset];
        System.arraycopy(responseData, offset, partialData, 0, partialData.length);
        assertThat(secondResponse.data).isEqualTo(partialData);
    }

    @Test
    public void testProcessManyThreads() throws Exception {
        final String url = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/space.jpg";
        HttpUrlSource source = new HttpUrlSource(url);
        FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
        final HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
        final byte[] loadedData = loadAssetFile("space.jpg");
        final Random random = new Random(System.currentTimeMillis());
        int concurrentRequests = 10;
        ExecutorService executor = Executors.newFixedThreadPool(concurrentRequests);
        Future[] results = new Future[concurrentRequests];
        int[] offsets = new int[concurrentRequests];
        final CountDownLatch finishLatch = new CountDownLatch(concurrentRequests);
        final CountDownLatch startLatch = new CountDownLatch(1);
        for (int i = 0; i < concurrentRequests; i++) {
            final int offset = random.nextInt(loadedData.length);
            offsets[i] = offset;
            results[i] = executor.submit(new Callable<Response>() {

                @Override
                public Response call() throws Exception {
                    try {
                        startLatch.await();
                        String partialRequest = "GET /" + url + " HTTP/1.1\nRange: bytes=" + offset + "-";
                        return processRequest(proxyCache, partialRequest);
                    } finally {
                        finishLatch.countDown();
                    }
                }
            });
        }
        startLatch.countDown();
        finishLatch.await();

        for (int i = 0; i < results.length; i++) {
            Response response = (Response) results[i].get();
            int offset = offsets[i];
            byte[] partialData = new byte[loadedData.length - offset];
            System.arraycopy(loadedData, offset, partialData, 0, partialData.length);
            assertThat(response.data).isEqualTo(partialData);
        }
    }

    @Test
    public void testLoadEmptyFile() throws Exception {
        String zeroSizeUrl = "https://raw.githubusercontent.com/danikula/AndroidVideoCache/master/files/empty.txt";
        HttpUrlSource source = new HttpUrlSource(zeroSizeUrl);
        HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(ProxyCacheTestUtils.newCacheFile()));
        GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1");
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Socket socket = mock(Socket.class);
        when(socket.getOutputStream()).thenReturn(out);

        CacheListener listener = Mockito.mock(CacheListener.class);
        proxyCache.registerCacheListener(listener);
        proxyCache.processRequest(request, socket);
        proxyCache.registerCacheListener(null);
        Response response = new Response(out.toByteArray());

        Mockito.verify(listener).onCacheAvailable(Mockito.<File>any(), eq(zeroSizeUrl), eq(100));
        assertThat(response.data).isEmpty();
    }

    @Test
    public void testCacheListenerCalledAtTheEnd() throws Exception {
        File file = ProxyCacheTestUtils.newCacheFile();
        File tempFile = ProxyCacheTestUtils.getTempFile(file);
        HttpProxyCache proxyCache = new HttpProxyCache(new HttpUrlSource(HTTP_DATA_URL), new FileCache(file));
        CacheListener listener = Mockito.mock(CacheListener.class);
        proxyCache.registerCacheListener(listener);
        processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");

        Mockito.verify(listener).onCacheAvailable(tempFile, HTTP_DATA_URL, 100);    // must be called for temp file ...
        Mockito.verify(listener).onCacheAvailable(file, HTTP_DATA_URL, 100);        // .. and for original file too
    }

    @Test(expected = ProxyCacheException.class)
    public void testTouchSourceForAbsentSourceInfoAndCache() throws Exception {
        SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newEmptySourceInfoStorage();
        HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
        HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile()));
        processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
        proxyCache.shutdown();
        fail("Angry source should throw error! There is no file and caches source info");
    }

    @Test(expected = ProxyCacheException.class)
    public void testTouchSourceForExistedSourceInfoAndAbsentCache() throws Exception {
        SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
        sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "image/jpg"));
        HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
        HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(newCacheFile()));
        processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
        proxyCache.shutdown();
        fail("Angry source should throw error! There is no cache file");
    }

    @Test
    public void testTouchSourceForExistedSourceInfoAndCache() throws Exception {
        SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
        sourceInfoStorage.put(HTTP_DATA_URL, new SourceInfo(HTTP_DATA_URL, HTTP_DATA_SIZE, "cached/mime"));
        HttpUrlSource source = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
        File file = newCacheFile();
        IoUtils.saveToFile(loadAssetFile(ASSETS_DATA_NAME), file);
        HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(file));
        Response response = processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
        proxyCache.shutdown();
        assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
        assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
        assertThat(response.contentType).isEqualTo("cached/mime");
    }

    @Test
    public void testReuseSourceInfo() throws Exception {
        SourceInfoStorage sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(RuntimeEnvironment.application);
        HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
        File cacheFile = newCacheFile();
        HttpProxyCache proxyCache = new HttpProxyCache(source, new FileCache(cacheFile));
        processRequest(proxyCache, "GET /" + HTTP_DATA_URL + " HTTP/1.1");

        HttpUrlSource notOpenableSource = ProxyCacheTestUtils.newNotOpenableHttpUrlSource(HTTP_DATA_URL, sourceInfoStorage);
        HttpProxyCache proxyCache2 = new HttpProxyCache(notOpenableSource, new FileCache(cacheFile));
        Response response = processRequest(proxyCache2, "GET /" + HTTP_DATA_URL + " HTTP/1.1");
        proxyCache.shutdown();

        assertThat(response.data).isEqualTo(loadAssetFile(ASSETS_DATA_NAME));
        assertThat(response.contentLength).isEqualTo(HTTP_DATA_SIZE);
        assertThat(response.contentType).isEqualTo("image/jpeg");
    }

    private Response processRequest(String sourceUrl, String httpRequest) throws ProxyCacheException, IOException {
        FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
        return processRequest(sourceUrl, httpRequest, fileCache);
    }

    private Response processRequest(String sourceUrl, String httpRequest, FileCache fileCache) throws ProxyCacheException, IOException {
        HttpUrlSource source = new HttpUrlSource(sourceUrl);
        HttpProxyCache proxyCache = new HttpProxyCache(source, fileCache);
        return processRequest(proxyCache, httpRequest);
    }

    private Response processRequest(HttpProxyCache proxyCache, String httpRequest) throws ProxyCacheException, IOException {
        GetRequest request = new GetRequest(httpRequest);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Socket socket = mock(Socket.class);
        when(socket.getOutputStream()).thenReturn(out);
        proxyCache.processRequest(request, socket);
        return new Response(out.toByteArray());
    }

    private Future<Response> processAsync(ExecutorService executor, final HttpProxyCache proxyCache, final String httpRequest) {
        return executor.submit(new Callable<Response>() {

            @Override
            public Response call() throws Exception {
                return processRequest(proxyCache, httpRequest);
            }
        });
    }
}