/*
* Copyright (c) 2010 Red Hat, Inc.
*
* 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 org.ovirt.engine.api.common.util;

import static org.easymock.EasyMock.expect;

import java.lang.ref.ReferenceQueue;

import org.easymock.IExpectationSetters;
import org.easymock.classextension.EasyMock;
import org.easymock.classextension.IMocksControl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;


public class ReapableMapTest extends Assert {

    private static final String[] NUMBERS = { "one", "two", "three", "four", "five" };

    private static final NumberMapper MAPPER = new NumberMapper();

    private IMocksControl control;
    private ReapedMap<String, Integer> map;

    @Before
    public void setUp() {
        control = EasyMock.createNiceControl();
    }

    @Test
    public void testReapingWithoutGC() throws Exception {
        map = new ReapedMap<String, Integer>(1000);
        populate(1, 2, 3);
        assertSizes(3, 0);
        assertExpected(1, 2, 3);
        map.reapable("one");
        map.reapable("three");
        assertSizes(1, 2);
        map.reapable("one");
        assertSizes(1, 2);
        assertExpected(1, 2, 3);
        assertEquals(new Integer(3), map.remove("three"));
        assertSizes(1, 1);
        assertExpected(1, 2);
        assertNull(map.get("three"));
        Thread.sleep(1100);
        assertExpected(1);
        assertNull(map.get("one"));
        assertExpected(2);
        assertSizes(1, 0);
        populate(4, 5);
        map.clear();
    }

    @Test
    public void testReapingOnGetWithGC() throws Exception {
        setUpGCExpectations(5);

        populate(1, 2, 3);
        map.reapable("one");
        map.reapable("three");
        assertSizes(1, 2);
        assertExpected(3);
        assertSizes(1, 1);
        assertNull(map.get("three"));

        control.verify();
    }

    @Test
    public void testReapingOnGetWithAccessBasedAging() throws Exception {
        setUpAccessBaseAgingExpectations();

        populate(1, 2, 3);
        assertSizes(3, 0);
        map.reapable("one");
        map.reapable("three");
        assertSizes(1, 2);
        for (int i = 0 ; i < 6 ; i++) {
            Thread.sleep(250);
            assertExpected(i == 0 ? 1 : 3);
        }
        assertSizes(1, 1);
        assertNull(map.get("one"));
        assertExpected(3);
        Thread.sleep(500);
        assertExpected(3);
        assertSizes(1, 1);

        Thread.sleep(1200);
        assertExpected(2);
        assertSizes(1, 0);
        assertNull(map.get("three"));

        control.verify();
    }

    @Test
    public void testReapingOnPutWithGC() throws Exception {
        setUpGCExpectations(5);

        populate(1, 2, 3);
        map.reapable("one");
        map.reapable("three");
        assertSizes(1, 2);
        populate(4);
        assertSizes(2, 1);
        assertNull(map.get("three"));

        control.verify();
    }

    @Test
    public void testReapingOnRemoveWithGC() throws Exception {
        setUpGCExpectations(5);

        populate(1, 2, 3);
        map.reapable("one");
        map.reapable("three");
        assertSizes(1, 2);
        map.remove("two");
        assertSizes(0, 1);
        assertNull(map.get("three"));

        control.verify();
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void setUpGCExpectations(int gcAfter) {
        ReferenceQueue<Integer> queue = (ReferenceQueue<Integer>)control.createMock(ReferenceQueue.class);
        map = new ReapedMap<String, Integer>(10000, false, queue);
        for (int i = 0 ; i < gcAfter ; i++) {
            expect(queue.poll()).andReturn(null);
        }
        // the gcAfter^th queue poll simulates a GC event and triggers deletion
        // on the reapable map
        ReapedMap.IdAwareReference<String, Integer> ref = control.createMock(ReapedMap.IdAwareReference.class);
        // awkward syntax required to work around compilation error
        // on Reference<capture#nnn ? extends Integer> mismatch
        ((IExpectationSetters) expect(queue.poll())).andReturn(ref);
        IExpectationSetters<? extends String> refSetter = expect(ref.getKey());
        ((IExpectationSetters<String>)refSetter).andReturn("three");

        control.replay();
    }

    @SuppressWarnings("unchecked")
    private void setUpAccessBaseAgingExpectations() {
        ReferenceQueue<Integer> queue = (ReferenceQueue<Integer>)control.createMock(ReferenceQueue.class);
        map = new ReapedMap<String, Integer>(1000, true, queue);
        expect(queue.poll()).andReturn(null).anyTimes();

        control.replay();
    }

    private void populate(Integer ... values) {
        for (Integer v: values) {
            map.put(MAPPER.getKey(v), v);
        }
    }

    private void assertSizes(int i, int j) {
        assertEquals("unexpected primary map size", i, map.size());
        assertEquals("unexpected secondary map size", j, map.reapableSize());
    }

    private void assertExpected(Integer ... values) {
        for (Integer v: values) {
            assertEquals(v, map.get(MAPPER.getKey(v)));
        }
    }

    private static class NumberMapper {
        public String getKey(Integer i) {
            return i <= NUMBERS.length ? NUMBERS[i-1] : null;
        }
    };
}
