// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.image_fetcher;

import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;

import android.graphics.Bitmap;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.robolectric.annotation.Config;

import org.chromium.base.Callback;
import org.chromium.base.DiscardableReferencePool;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.components.browser_ui.util.BitmapCache;

import jp.tomorrowkey.android.gifplayer.BaseGifImage;

/**
 * Unit tests for InMemoryCachedImageFetcher.
 */
@SuppressWarnings("unchecked")
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class InMemoryCachedImageFetcherTest {
    private static final String UMA_CLIENT_NAME = "TestUmaClient";
    private static final String URL = "http://foo.bar";
    private static final int WIDTH_PX = 100;
    private static final int HEIGHT_PX = 200;
    private static final int DEFAULT_CACHE_SIZE = 100;
    private static final int UNKNOWN_IMAGE_FETCHER_CONFIG = -1;

    @Rule
    public ExpectedException mExpectedException = ExpectedException.none();

    private final Bitmap mBitmap =
            Bitmap.createBitmap(WIDTH_PX, HEIGHT_PX, Bitmap.Config.ARGB_8888);

    // The image fetcher under test.
    private InMemoryCachedImageFetcher mInMemoryCachedImageFetcher;
    private BitmapCache mBitmapCache;
    private DiscardableReferencePool mReferencePool;

    @Mock
    private ImageFetcherBridge mBridge;
    @Mock
    private CachedImageFetcher mMockImageFetcher;
    @Mock
    private Callback<Bitmap> mCallback;
    @Mock
    private Runtime mRuntime;
    @Captor
    private ArgumentCaptor<Integer> mWidthCaptor;
    @Captor
    private ArgumentCaptor<Integer> mHeightCaptor;
    @Captor
    private ArgumentCaptor<Callback<Bitmap>> mCallbackCaptor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        doReturn(mBridge).when(mMockImageFetcher).getImageFetcherBridge();

        mReferencePool = new DiscardableReferencePool();
        mBitmapCache = Mockito.spy(new BitmapCache(mReferencePool, DEFAULT_CACHE_SIZE));
        mInMemoryCachedImageFetcher =
                new InMemoryCachedImageFetcher(mMockImageFetcher, mBitmapCache);
    }

    @After
    public void tearDown() {
        mInMemoryCachedImageFetcher.destroy();
    }

    private void answerFetch(Bitmap bitmap, boolean deleteBitmapCacheOnFetch) {
        mInMemoryCachedImageFetcher =
                new InMemoryCachedImageFetcher(mMockImageFetcher, mBitmapCache);
        // clang-format off
        doAnswer((InvocationOnMock invocation) -> {
            if (deleteBitmapCacheOnFetch) {
                mInMemoryCachedImageFetcher.destroy();
                mReferencePool.drain();
            }

            mCallbackCaptor.getValue().onResult(bitmap);
            return null;
        }).when(mMockImageFetcher)
                .fetchImage(any(),
                        mCallbackCaptor.capture());
        // clang-format on
    }

    // Use with junit.Assume to turn assertions on/off for specific test.
    private boolean assertionsEnabled() {
        return InMemoryCachedImageFetcherTest.class.desiredAssertionStatus();
    }

    @Test
    public void testConstructor_unknownConfig() {
        assumeFalse(assertionsEnabled());
        doReturn(UNKNOWN_IMAGE_FETCHER_CONFIG).when(mMockImageFetcher).getConfig();
        mInMemoryCachedImageFetcher =
                new InMemoryCachedImageFetcher(mMockImageFetcher, mBitmapCache);
        Assert.assertEquals(
                ImageFetcherConfig.IN_MEMORY_ONLY, mInMemoryCachedImageFetcher.getConfig());
    }

    @Test
    public void testFetchImageCachesFirstCall() {
        answerFetch(mBitmap, false);
        ImageFetcher.Params params =
                ImageFetcher.Params.create(URL, UMA_CLIENT_NAME, WIDTH_PX, HEIGHT_PX);
        mInMemoryCachedImageFetcher.fetchImage(params, mCallback);
        verify(mCallback).onResult(mBitmap);

        reset(mCallback);
        mInMemoryCachedImageFetcher.fetchImage(params, mCallback);
        verify(mCallback).onResult(mBitmap);

        verify(mMockImageFetcher).fetchImage(eq(params), any());

        // Verify metrics are reported.
        verify(mBridge).reportEvent(UMA_CLIENT_NAME, ImageFetcherEvent.JAVA_IN_MEMORY_CACHE_HIT);
    }

    @Test
    public void testFetchImageDoesNotCacheAfterDestroy() {
        try {
            answerFetch(mBitmap, true);

            // No exception should be thrown here when bitmap cache is null.
            ImageFetcher.Params params =
                    ImageFetcher.Params.create(URL, UMA_CLIENT_NAME, WIDTH_PX, HEIGHT_PX);
            mInMemoryCachedImageFetcher.fetchImage(params, (Bitmap bitmap) -> {});
        } catch (Exception e) {
            Assert.fail("Destroy called in the middle of execution shouldn't throw");
        }
    }

    @Test
    public void testFetchGif() {
        ImageFetcher.Params params = ImageFetcher.Params.create(URL, UMA_CLIENT_NAME);
        mInMemoryCachedImageFetcher.fetchGif(params, (BaseGifImage gif) -> {});
        verify(mMockImageFetcher).fetchGif(eq(params), any());
    }

    @Test
    public void testClear() {
        mInMemoryCachedImageFetcher =
                new InMemoryCachedImageFetcher(mMockImageFetcher, mBitmapCache);
        mInMemoryCachedImageFetcher.clear();

        verify(mBitmapCache).clear();
    }

    @Test
    public void testDestroy() {
        mInMemoryCachedImageFetcher.destroy();

        verify(mMockImageFetcher).destroy();
        verify(mBitmapCache).destroy();

        // Check that calling methods after destroy throw AssertionErrors.
        mExpectedException.expect(AssertionError.class);
        mExpectedException.expectMessage("fetchGif called after destroy");
        mInMemoryCachedImageFetcher.fetchGif(ImageFetcher.Params.create("", ""), null);
        mExpectedException.expectMessage("fetchImage called after destroy");
        mInMemoryCachedImageFetcher.fetchImage(ImageFetcher.Params.create("", "", 100, 100), null);
        mExpectedException.expectMessage("clear called after destroy");
        mInMemoryCachedImageFetcher.clear();
    }

    @Test
    public void testEncodeCacheKey() {
        Assert.assertEquals("url/1/100/200",
                mInMemoryCachedImageFetcher.encodeCacheKey("url", /*shouldResize=*/true, 100, 200));
    }

    @Test
    public void testDetermineCacheSize_clientRequestedSmallerThanAvailable() {
        long totalMemory = 200L;
        long allocatedMemory = 100L;
        int clientRequest = 10;
        doReturn(totalMemory).when(mRuntime).maxMemory();
        doReturn(allocatedMemory).when(mRuntime).totalMemory();
        doReturn(0L).when(mRuntime).freeMemory();

        // We calculate the in-memory cache size as a percentage of available memory.
        Assert.assertEquals("Cache size should be bounded by the space requested by the client.",
                clientRequest,
                InMemoryCachedImageFetcher.determineCacheSize(mRuntime, clientRequest));
    }

    @Test
    public void testDetermineCacheSize_clientRequestedLargerThanAvailable() {
        long totalMemory = 200L;
        long allocatedMemory = 120L;
        int clientRequest = 100;
        doReturn(totalMemory).when(mRuntime).maxMemory();
        doReturn(allocatedMemory).when(mRuntime).totalMemory();
        doReturn(0L).when(mRuntime).freeMemory();

        // We calculate the in-memory cache size as a percentage of available memory.
        Assert.assertEquals("Client requests should be bounded by 1/8th of the available memory.",
                10, InMemoryCachedImageFetcher.determineCacheSize(mRuntime, clientRequest));
    }

    @Test
    public void testDetermineCacheSize_freeMemoryLowerBound() {
        long totalMemory = 200L;
        long allocatedMemory = 199L;
        int clientRequest = 10;
        doReturn(totalMemory).when(mRuntime).maxMemory();
        doReturn(allocatedMemory).when(mRuntime).totalMemory();
        doReturn(0L).when(mRuntime).freeMemory();

        // We calculate the in-memory cache size as a percentage of available memory.
        Assert.assertEquals("The minimum cache size is 1.", 1,
                InMemoryCachedImageFetcher.determineCacheSize(mRuntime, clientRequest));
    }
}
