/*
 * RHQ Management Platform
 * Copyright (C) 2013 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package org.rhq.enterprise.server.util;

import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.util.Collections;
import java.util.List;

import javax.persistence.Query;

import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;

/**
 * @author Lukas Krejci
 * @since 4.9
 */
@Test
public class QueryUtilityTest {

    private int[] attemptCounters;
    private Query dataQuery;
    private Query countQuery;
    private int numberOfInconsistentResults;

    @BeforeClass
    public void setupMocks() {
        attemptCounters = new int[2];
        dataQuery = Mockito.mock(Query.class);
        when(dataQuery.getResultList()).then(new Answer<List<?>>() {
            @Override
            public List<?> answer(InvocationOnMock invocation) throws Throwable {
                int attempt = attemptCounters[0]++;

                if (attempt < numberOfInconsistentResults) {
                    return Collections.emptyList();
                } else if (attempt < QueryUtility.PHANTOM_READ_MAX_ATTEMPTS) {
                    return Collections.singletonList(this);
                } else {
                    throw new AssertionError(
                        "Shouldn't have been called more than " + QueryUtility.PHANTOM_READ_MAX_ATTEMPTS + " times");
                }
            }
        });

        countQuery = Mockito.mock(Query.class);
        when(countQuery.getSingleResult()).then(new Answer<Long>() {
            @Override
            public Long answer(InvocationOnMock invocation) throws Throwable {
                if (attemptCounters[1]++ < QueryUtility.PHANTOM_READ_MAX_ATTEMPTS) {
                    return 1l;
                } else {
                    throw new AssertionError(
                        "Shouldn't have been called more than " + QueryUtility.PHANTOM_READ_MAX_ATTEMPTS + " times");
                }
            }
        });
    }

    @BeforeMethod
    public void resetAttemptCounters() {
        for(int i = 0; i < attemptCounters.length; ++i) {
            attemptCounters[i] = 0;
        }
    }
    public void defaultPagedDataFetchSettings() {
        QueryUtility.PagedDataFetchSettings settings = new QueryUtility.PagedDataFetchSettings();

        assertEquals(settings.getMaxAttempts(), QueryUtility.PHANTOM_READ_MAX_ATTEMPTS, "Wrong default max attempts");
        assertEquals(settings.getMinWaitTime(), QueryUtility.PHANTOM_READ_MIN_WAIT_TIME, "Wrong default min wait time");
        assertEquals(settings.getMaxWaitTime(), QueryUtility.PHANTOM_READ_MAX_WAIT_TIME, "Wrong default max wait time");
        assertFalse(settings.isThrowOnMaxAttempts(), "Wrong default throw on max attempts");
    }

    public void lagCoefficientForPagedDataFetch() {
        QueryUtility.PagedDataFetchSettings settings = new QueryUtility.PagedDataFetchSettings();
        settings.setMaxAttempts(10);
        settings.setMinWaitTime(1);
        settings.setMaxWaitTime(10);

        float lag = settings.getLagIncreaseCoefficient();
        assertEquals(lag, (float) Math.pow(10, 1d / 8), "Unexpected lag computed");
    }

    public void dataFetchPerformsMaxAttemptsOnInconsistentResults() {
        numberOfInconsistentResults = QueryUtility.DEFAULT_PHANTOM_READ_MAX_ATTEMPTS;

        PageControl pc = PageControl.getUnlimitedInstance();

        PageList<Object> result = QueryUtility.fetchPagedDataAndCount(dataQuery, countQuery, pc, null);

        assertEquals(result, Collections.emptyList(), "The result should be empty");
        assertEquals(result.getTotalSize(), 1, "Unexpected total size");
        assertFalse(result.isConsistent(), "The result should be inconsistent");

        assertEquals(attemptCounters[0], QueryUtility.PHANTOM_READ_MAX_ATTEMPTS);
        assertEquals(attemptCounters[1], QueryUtility.PHANTOM_READ_MAX_ATTEMPTS);
    }

    public void dataFetchReturnsConsistentResultsWhenDetected() {
        numberOfInconsistentResults = 2;

        PageControl pc = PageControl.getUnlimitedInstance();

        PageList<Object> result = QueryUtility.fetchPagedDataAndCount(dataQuery, countQuery, pc, null);

        assertEquals(result.size(), 1, "The result should have 1 element");
        assertEquals(result.getTotalSize(), 1, "Unexpected total size");
        assertTrue(result.isConsistent(), "The result should be consistent");

        assertEquals(attemptCounters[0], numberOfInconsistentResults + 1);
        assertEquals(attemptCounters[1], numberOfInconsistentResults + 1);
    }

    public void dataFetchThrowsAfterMaxAttemptsWhenSetUpSo() {
        numberOfInconsistentResults = QueryUtility.DEFAULT_PHANTOM_READ_MAX_ATTEMPTS;

        PageControl pc = PageControl.getUnlimitedInstance();
        QueryUtility.PagedDataFetchSettings settings = new QueryUtility.PagedDataFetchSettings();
        settings.setThrowOnMaxAttempts(true);

        try {
            QueryUtility.fetchPagedDataAndCount(dataQuery, countQuery, pc, settings);
            fail("Fetch should have thrown an exception after max attempts");
        } catch (PhantomReadMaxAttemptsExceededException e) {
            //expected
            PageList<?> result = e.getList();
            assertEquals(result, Collections.emptyList(), "The result should be empty");
            assertEquals(result.getTotalSize(), 1, "Unexpected total size");
            assertFalse(result.isConsistent(), "The result should be inconsistent");
        }

        assertEquals(attemptCounters[0], QueryUtility.PHANTOM_READ_MAX_ATTEMPTS);
        assertEquals(attemptCounters[1], QueryUtility.PHANTOM_READ_MAX_ATTEMPTS);
    }

    public void repeatedFetchesWaitLongEnough() {
        numberOfInconsistentResults = QueryUtility.DEFAULT_PHANTOM_READ_MAX_ATTEMPTS;

        PageControl pc = PageControl.getUnlimitedInstance();

        long time = System.currentTimeMillis();
        PageList<?> result = QueryUtility.fetchPagedDataAndCount(dataQuery, countQuery, pc, null);
        time = System.currentTimeMillis() - time;

        QueryUtility.PagedDataFetchSettings unusedSettings = new QueryUtility.PagedDataFetchSettings();

        assertTrue(time >= unusedSettings.getMinimumTotalWaitTime(),
            "The fetch should have spent more time trying. Was " + time + "ms, but should have been at least " +
                unusedSettings.getMinimumTotalWaitTime() + "ms");

        assertEquals(result, Collections.emptyList(), "The result should be empty");
        assertEquals(result.getTotalSize(), 1, "Unexpected total size");
        assertFalse(result.isConsistent(), "The result should be inconsistent");

        assertEquals(attemptCounters[0], QueryUtility.PHANTOM_READ_MAX_ATTEMPTS);
        assertEquals(attemptCounters[1], QueryUtility.PHANTOM_READ_MAX_ATTEMPTS);
    }
}
