/*
 * 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 android.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import junit.framework.TestCase;

public final class LruCacheTest extends TestCase {
    private int expectedCreateCount;
    private int expectedPutCount;
    private int expectedHitCount;
    private int expectedMissCount;
    private int expectedEvictionCount;

    public void testStatistics() {
        LruCache<String, String> cache = new LruCache<String, String>(3);
        assertStatistics(cache);

        assertEquals(null, cache.put("a", "A"));
        expectedPutCount++;
        assertStatistics(cache);
        assertHit(cache, "a", "A");
        assertSnapshot(cache, "a", "A");

        assertEquals(null, cache.put("b", "B"));
        expectedPutCount++;
        assertStatistics(cache);
        assertHit(cache, "a", "A");
        assertHit(cache, "b", "B");
        assertSnapshot(cache, "a", "A", "b", "B");

        assertEquals(null, cache.put("c", "C"));
        expectedPutCount++;
        assertStatistics(cache);
        assertHit(cache, "a", "A");
        assertHit(cache, "b", "B");
        assertHit(cache, "c", "C");
        assertSnapshot(cache, "a", "A", "b", "B", "c", "C");

        assertEquals(null, cache.put("d", "D"));
        expectedPutCount++;
        expectedEvictionCount++; // a should have been evicted
        assertStatistics(cache);
        assertMiss(cache, "a");
        assertHit(cache, "b", "B");
        assertHit(cache, "c", "C");
        assertHit(cache, "d", "D");
        assertHit(cache, "b", "B");
        assertHit(cache, "c", "C");
        assertSnapshot(cache, "d", "D", "b", "B", "c", "C");

        assertEquals(null, cache.put("e", "E"));
        expectedPutCount++;
        expectedEvictionCount++; // d should have been evicted
        assertStatistics(cache);
        assertMiss(cache, "d");
        assertMiss(cache, "a");
        assertHit(cache, "e", "E");
        assertHit(cache, "b", "B");
        assertHit(cache, "c", "C");
        assertSnapshot(cache, "e", "E", "b", "B", "c", "C");
    }

    public void testStatisticsWithCreate() {
        LruCache<String, String> cache = newCreatingCache();
        assertStatistics(cache);

        assertCreated(cache, "aa", "created-aa");
        assertHit(cache, "aa", "created-aa");
        assertSnapshot(cache, "aa", "created-aa");

        assertCreated(cache, "bb", "created-bb");
        assertMiss(cache, "c");
        assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb");

        assertCreated(cache, "cc", "created-cc");
        assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb", "cc", "created-cc");

        expectedEvictionCount++; // aa will be evicted
        assertCreated(cache, "dd", "created-dd");
        assertSnapshot(cache, "bb", "created-bb",  "cc", "created-cc", "dd", "created-dd");

        expectedEvictionCount++; // bb will be evicted
        assertCreated(cache, "aa", "created-aa");
        assertSnapshot(cache, "cc", "created-cc", "dd", "created-dd", "aa", "created-aa");
    }

    public void testCreateOnCacheMiss() {
        LruCache<String, String> cache = newCreatingCache();
        String created = cache.get("aa");
        assertEquals("created-aa", created);
    }

    public void testNoCreateOnCacheHit() {
        LruCache<String, String> cache = newCreatingCache();
        cache.put("aa", "put-aa");
        assertEquals("put-aa", cache.get("aa"));
    }

    public void testConstructorDoesNotAllowZeroCacheSize() {
        try {
            new LruCache<String, String>(0);
            fail();
        } catch (IllegalArgumentException expected) {
        }
    }

    public void testCannotPutNullKey() {
        LruCache<String, String> cache = new LruCache<String, String>(3);
        try {
            cache.put(null, "A");
            fail();
        } catch (NullPointerException expected) {
        }
    }

    public void testCannotPutNullValue() {
        LruCache<String, String> cache = new LruCache<String, String>(3);
        try {
            cache.put("a", null);
            fail();
        } catch (NullPointerException expected) {
        }
    }

    public void testToString() {
        LruCache<String, String> cache = new LruCache<String, String>(3);
        assertEquals("LruCache[maxSize=3,hits=0,misses=0,hitRate=0%]", cache.toString());

        cache.put("a", "A");
        cache.put("b", "B");
        cache.put("c", "C");
        cache.put("d", "D");

        cache.get("a"); // miss
        cache.get("b"); // hit
        cache.get("c"); // hit
        cache.get("d"); // hit
        cache.get("e"); // miss

        assertEquals("LruCache[maxSize=3,hits=3,misses=2,hitRate=60%]", cache.toString());
    }

    public void testEvictionWithSingletonCache() {
        LruCache<String, String> cache = new LruCache<String, String>(1);
        cache.put("a", "A");
        cache.put("b", "B");
        assertSnapshot(cache, "b", "B");
    }

    public void testEntryEvictedWhenFull() {
        List<String> log = new ArrayList<String>();
        LruCache<String, String> cache = newRemovalLogCache(log);

        cache.put("a", "A");
        cache.put("b", "B");
        cache.put("c", "C");
        assertEquals(Collections.<String>emptyList(), log);

        cache.put("d", "D");
        assertEquals(Arrays.asList("a=A"), log);
    }

    /**
     * Replacing the value for a key doesn't cause an eviction but it does bring
     * the replaced entry to the front of the queue.
     */
    public void testPutCauseEviction() {
        List<String> log = new ArrayList<String>();
        LruCache<String, String> cache = newRemovalLogCache(log);

        cache.put("a", "A");
        cache.put("b", "B");
        cache.put("c", "C");
        cache.put("b", "B2");
        assertEquals(Arrays.asList("b=B>B2"), log);
        assertSnapshot(cache, "a", "A", "c", "C", "b", "B2");
    }

    public void testCustomSizesImpactsSize() {
        LruCache<String, String> cache = new LruCache<String, String>(10) {
            @Override protected int sizeOf(String key, String value) {
                return key.length() + value.length();
            }
        };

        assertEquals(0, cache.size());
        cache.put("a", "AA");
        assertEquals(3, cache.size());
        cache.put("b", "BBBB");
        assertEquals(8, cache.size());
        cache.put("a", "");
        assertEquals(6, cache.size());
    }

    public void testEvictionWithCustomSizes() {
        LruCache<String, String> cache = new LruCache<String, String>(4) {
            @Override protected int sizeOf(String key, String value) {
                return value.length();
            }
        };

        cache.put("a", "AAAA");
        assertSnapshot(cache, "a", "AAAA");
        cache.put("b", "BBBB"); // should evict a
        assertSnapshot(cache, "b", "BBBB");
        cache.put("c", "CC"); // should evict b
        assertSnapshot(cache, "c", "CC");
        cache.put("d", "DD");
        assertSnapshot(cache, "c", "CC", "d", "DD");
        cache.put("e", "E"); // should evict c
        assertSnapshot(cache, "d", "DD", "e", "E");
        cache.put("f", "F");
        assertSnapshot(cache, "d", "DD", "e", "E", "f", "F");
        cache.put("g", "G"); // should evict d
        assertSnapshot(cache, "e", "E", "f", "F", "g", "G");
        cache.put("h", "H");
        assertSnapshot(cache, "e", "E", "f", "F", "g", "G", "h", "H");
        cache.put("i", "III"); // should evict e, f, and g
        assertSnapshot(cache, "h", "H", "i", "III");
        cache.put("j", "JJJ"); // should evict h and i
        assertSnapshot(cache, "j", "JJJ");
    }

    public void testEvictionThrowsWhenSizesAreInconsistent() {
        LruCache<String, int[]> cache = new LruCache<String, int[]>(4) {
            @Override protected int sizeOf(String key, int[] value) {
                return value[0];
            }
        };

        int[] a = { 4 };
        cache.put("a", a);

        // get the cache size out of sync
        a[0] = 1;
        assertEquals(4, cache.size());

        // evict something
        try {
            cache.put("b", new int[] { 2 });
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    public void testEvictionThrowsWhenSizesAreNegative() {
        LruCache<String, String> cache = new LruCache<String, String>(4) {
            @Override protected int sizeOf(String key, String value) {
                return -1;
            }
        };

        try {
            cache.put("a", "A");
            fail();
        } catch (IllegalStateException expected) {
        }
    }

    /**
     * Naive caches evict at most one element at a time. This is problematic
     * because evicting a small element may be insufficient to make room for a
     * large element.
     */
    public void testDifferentElementSizes() {
        LruCache<String, String> cache = new LruCache<String, String>(10) {
            @Override protected int sizeOf(String key, String value) {
                return value.length();
            }
        };

        cache.put("a", "1");
        cache.put("b", "12345678");
        cache.put("c", "1");
        assertSnapshot(cache, "a", "1", "b", "12345678", "c", "1");
        cache.put("d", "12345678"); // should evict a and b
        assertSnapshot(cache, "c", "1", "d", "12345678");
        cache.put("e", "12345678"); // should evict c and d
        assertSnapshot(cache, "e", "12345678");
    }

    public void testEvictAll() {
        List<String> log = new ArrayList<String>();
        LruCache<String, String> cache = newRemovalLogCache(log);
        cache.put("a", "A");
        cache.put("b", "B");
        cache.put("c", "C");
        cache.evictAll();
        assertEquals(0, cache.size());
        assertEquals(Arrays.asList("a=A", "b=B", "c=C"), log);
    }

    public void testEvictAllEvictsSizeZeroElements() {
        LruCache<String, String> cache = new LruCache<String, String>(10) {
            @Override protected int sizeOf(String key, String value) {
                return 0;
            }
        };

        cache.put("a", "A");
        cache.put("b", "B");
        cache.evictAll();
        assertSnapshot(cache);
    }

    public void testRemoveWithCustomSizes() {
        LruCache<String, String> cache = new LruCache<String, String>(10) {
            @Override protected int sizeOf(String key, String value) {
                return value.length();
            }
        };
        cache.put("a", "123456");
        cache.put("b", "1234");
        cache.remove("a");
        assertEquals(4, cache.size());
    }

    public void testRemoveAbsentElement() {
        LruCache<String, String> cache = new LruCache<String, String>(10);
        cache.put("a", "A");
        cache.put("b", "B");
        assertEquals(null, cache.remove("c"));
        assertEquals(2, cache.size());
    }

    public void testRemoveNullThrows() {
        LruCache<String, String> cache = new LruCache<String, String>(10);
        try {
            cache.remove(null);
            fail();
        } catch (NullPointerException expected) {
        }
    }

    public void testRemoveCallsEntryRemoved() {
        List<String> log = new ArrayList<String>();
        LruCache<String, String> cache = newRemovalLogCache(log);
        cache.put("a", "A");
        cache.remove("a");
        assertEquals(Arrays.asList("a=A>null"), log);
    }

    public void testPutCallsEntryRemoved() {
        List<String> log = new ArrayList<String>();
        LruCache<String, String> cache = newRemovalLogCache(log);
        cache.put("a", "A");
        cache.put("a", "A2");
        assertEquals(Arrays.asList("a=A>A2"), log);
    }

    public void testEntryRemovedIsCalledWithoutSynchronization() {
        LruCache<String, String> cache = new LruCache<String, String>(3) {
            @Override protected void entryRemoved(
                    boolean evicted, String key, String oldValue, String newValue) {
                assertFalse(Thread.holdsLock(this));
            }
        };

        cache.put("a", "A");
        cache.put("a", "A2"); // replaced
        cache.put("b", "B");
        cache.put("c", "C");
        cache.put("d", "D");  // single eviction
        cache.remove("a");    // removed
        cache.evictAll();     // multiple eviction
    }

    public void testCreateIsCalledWithoutSynchronization() {
        LruCache<String, String> cache = new LruCache<String, String>(3) {
            @Override protected String create(String key) {
                assertFalse(Thread.holdsLock(this));
                return null;
            }
        };

        cache.get("a");
    }

    /**
     * Test what happens when a value is added to the map while create is
     * working. The map value should be returned by get(), and the created value
     * should be released with entryRemoved().
     */
    public void testCreateWithConcurrentPut() {
        final List<String> log = new ArrayList<String>();
        LruCache<String, String> cache = new LruCache<String, String>(3) {
            @Override protected String create(String key) {
                put(key, "B");
                return "A";
            }
            @Override protected void entryRemoved(
                    boolean evicted, String key, String oldValue, String newValue) {
                log.add(key + "=" + oldValue + ">" + newValue);
            }
        };

        assertEquals("B", cache.get("a"));
        assertEquals(Arrays.asList("a=A>B"), log);
    }

    /**
     * Test what happens when two creates happen concurrently. The result from
     * the first create to return is returned by both gets. The other created
     * values should be released with entryRemove().
     */
    public void testCreateWithConcurrentCreate() {
        final List<String> log = new ArrayList<String>();
        LruCache<String, Integer> cache = new LruCache<String, Integer>(3) {
            int callCount = 0;
            @Override protected Integer create(String key) {
                if (callCount++ == 0) {
                    assertEquals(2, get(key).intValue());
                    return 1;
                } else {
                    return 2;
                }
            }
            @Override protected void entryRemoved(
                    boolean evicted, String key, Integer oldValue, Integer newValue) {
                log.add(key + "=" + oldValue + ">" + newValue);
            }
        };

        assertEquals(2, cache.get("a").intValue());
        assertEquals(Arrays.asList("a=1>2"), log);
    }

    private LruCache<String, String> newCreatingCache() {
        return new LruCache<String, String>(3) {
            @Override protected String create(String key) {
                return (key.length() > 1) ? ("created-" + key) : null;
            }
        };
    }

    private LruCache<String, String> newRemovalLogCache(final List<String> log) {
        return new LruCache<String, String>(3) {
            @Override protected void entryRemoved(
                    boolean evicted, String key, String oldValue, String newValue) {
                String message = evicted
                        ? (key + "=" + oldValue)
                        : (key + "=" + oldValue + ">" + newValue);
                log.add(message);
            }
        };
    }

    private void assertHit(LruCache<String, String> cache, String key, String value) {
        assertEquals(value, cache.get(key));
        expectedHitCount++;
        assertStatistics(cache);
    }

    private void assertMiss(LruCache<String, String> cache, String key) {
        assertEquals(null, cache.get(key));
        expectedMissCount++;
        assertStatistics(cache);
    }

    private void assertCreated(LruCache<String, String> cache, String key, String value) {
        assertEquals(value, cache.get(key));
        expectedMissCount++;
        expectedCreateCount++;
        assertStatistics(cache);
    }

    private void assertStatistics(LruCache<?, ?> cache) {
        assertEquals("create count", expectedCreateCount, cache.createCount());
        assertEquals("put count", expectedPutCount, cache.putCount());
        assertEquals("hit count", expectedHitCount, cache.hitCount());
        assertEquals("miss count", expectedMissCount, cache.missCount());
        assertEquals("eviction count", expectedEvictionCount, cache.evictionCount());
    }

    private <T> void assertSnapshot(LruCache<T, T> cache, T... keysAndValues) {
        List<T> actualKeysAndValues = new ArrayList<T>();
        for (Map.Entry<T, T> entry : cache.snapshot().entrySet()) {
            actualKeysAndValues.add(entry.getKey());
            actualKeysAndValues.add(entry.getValue());
        }

        // assert using lists because order is important for LRUs
        assertEquals(Arrays.asList(keysAndValues), actualKeysAndValues);
    }
}
