/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */

package org.alfresco.repo.virtual.page;

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

import junit.framework.TestCase;

import org.alfresco.query.ListBackedPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.virtual.page.PageCollator.PagingResultsSource;
import org.alfresco.util.Pair;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;
import org.springframework.util.comparator.ComparableComparator;

public class PageCollatorTest extends TestCase
{
    private static Log logger = LogFactory.getLog(PageCollatorTest.class);

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
    }

    private int[] createMergedPage(int skip, int count, List<Integer> s1, Integer[] s2)
    {
        return createMergedPage(skip,
                                count,
                                s1,
                                ArrayUtils.toPrimitive(s2));
    }

    private int[] createMergedPage(int skip, int count, List<Integer> s1, int[] s2)
    {
        int[] s1Primitive = (s1 == null || s1.isEmpty()) ? new int[] {} : ArrayUtils.toPrimitive((Integer[]) s1
                    .toArray(new Integer[s1.size()]));

        return createMergedPage(skip,
                                count,
                                s1Primitive,
                                s2);
    }

    /**
     * Creates an page-array by concatenating the given arrays than skipping and
     * trimming the resulted array.
     * 
     * @param skip
     * @param count
     * @param s1
     * @param s2
     * @return a page-array obtained by merging <code>s1</code> and
     *         <code>s2</code>, sorting the result than copying
     *         <code>count</code> elements from the concatenation starting with
     *         the element at index <code>skip</code> into the returned array of
     *         size <code>count</code>. If <code>skip</code> value is out of
     *         bounds an empty array is returned.
     */
    private int[] createMergedPage(int skip, int count, int[] s1, int[] s2)
    {
        final int[] m = Arrays.copyOf(s1,
                                      s1.length + s2.length);
        System.arraycopy(s2,
                         0,
                         m,
                         s1.length,
                         s2.length);
        Arrays.sort(m);

        final int zeroCount = (count == 0 ? m.length : count);

        final int len = Math.min(m.length - skip,
                                 zeroCount);
        if (len > 0)
        {
            int[] p = new int[len];

            System.arraycopy(m,
                             skip,
                             p,
                             0,
                             len);

            return p;
        }
        else
        {
            return new int[] {};
        }
    }

    private void assertEqualPages(int[] expected, int[] actual)
    {
        assertEqualPages(Arrays.toString(expected) + " vs " + Arrays.toString(actual),
                         expected,
                         actual);
    }

    private void assertEqualPages(String message, int[] expected, int[] actual)
    {
        assertTrue(message,
                   Arrays.equals(expected,
                                 actual));
    }

    private void assertEqualPages(String message, int[] expected, List<Integer> page)
    {
        assertEqualPages(message,
                         expected,
                         ArrayUtils.toPrimitive((Integer[]) page.toArray(new Integer[] {})));
    }

    @Test
    public void testCreateMergedPage() throws Exception
    {
        // { 0, 1, 2, 3, 4, 7, 8, 9, 10 }
        final int[] s1 = new int[] { 0, 1, 3, 7, 8 };
        final int[] s2 = new int[] { 2, 4, 9, 10 };

        assertEqualPages(new int[] { 2, 3, 4 },
                         createMergedPage(2,
                                          3,
                                          s1,
                                          s2));

        assertEqualPages(new int[] { 9, 10 },
                         createMergedPage(7,
                                          2,
                                          s1,
                                          s2));

        assertEqualPages(new int[] { 0, 1, 2, 3, 4 },
                         createMergedPage(0,
                                          5,
                                          s1,
                                          s2));

        assertEqualPages(new int[] { 0, 1, 2, 3, 4 },
                         createMergedPage(0,
                                          5,
                                          s1,
                                          s2));

        assertEqualPages(new int[] { 4, 7, 8, 9 },
                         createMergedPage(4,
                                          4,
                                          s1,
                                          s2));

        assertEqualPages(new int[] {},
                         createMergedPage(10,
                                          4,
                                          s1,
                                          s2));

        assertEqualPages(new int[] {},
                         createMergedPage(9,
                                          1,
                                          s1,
                                          s2));

        assertEqualPages(new int[] {},
                         createMergedPage(9,
                                          1,
                                          s1,
                                          s2));

        assertEqualPages(new int[] {},
                         createMergedPage(11,
                                          3,
                                          s1,
                                          s2));

        final int[] s3 = new int[] { 2, 3 };
        final int[] s4 = new int[] { 7, 10, 13, 11 };

        assertEqualPages(new int[] { 13 },
                         createMergedPage(5,
                                          2,
                                          s3,
                                          s4));

        final int[] s5 = new int[] { 1, 2, 3 };
        final int[] s6 = new int[] { 5 };

        assertEqualPages(new int[] { 5 },
                         createMergedPage(3,
                                          1,
                                          s5,
                                          s6));

        assertEqualPages(new int[] { 1, 2, 3, 5 },
                         createMergedPage(0,
                                          0,
                                          s5,
                                          s6));
    }

    /**
     * A page source that uses a generic array for the entire possible page
     * range.
     * 
     * @author Bogdan Horje
     * @param <R>
     */
    class ArrayPageSource<R> implements PagingResultsSource<R>
    {
        private R[] array;

        private boolean boundsError;

        public ArrayPageSource(R[] array)
        {
            this(array,
                 true);
        }

        public ArrayPageSource(R[] array, boolean boundsError)
        {
            super();
            this.array = array;
            this.boundsError = boundsError;
        }

        @Override
        public PagingResults<R> retrieve(PagingRequest pr) throws PageCollationException
        {
            final int skip = pr.getSkipCount();
            final int pageSize = pr.getMaxItems();
            if (skip < 0 || pageSize < 0)
            {
                throw new PageCollationException("Invalid page!");
            }

            if (boundsError && (skip >= array.length))
            {
                throw new InvalidPageBounds("Out of bounds " + skip + ">=" + array.length);
            }

            return new ListBackedPagingResults<R>(Arrays.asList(array),
                                                  pr);
        }

    }

    public void assertCollate(List<Integer> results, Integer[] source, int skip, int pageSize) throws Exception
    {
        assertCollate(results,
                      source,
                      skip,
                      pageSize,
                      false);
    }

    public void assertCollate(List<Integer> results, Integer[] source, int skip, int pageSize, boolean boundsError)
                throws Exception
    {
        Arrays.sort(source);
        Collections.sort(results);
        int[] expected = createMergedPage(skip,
                                          pageSize,
                                          results,
                                          source);
        PagingResults<Integer> actualResults = new PageCollator<Integer>().collate(results,
                                                                                   new ArrayPageSource(source),
                                                                                   new PagingRequest(skip,
                                                                                                     pageSize),
                                                                                   new ComparableComparator<Integer>());
        List<Integer> actualPage = actualResults.getPage();

        final String message = "[" + results + " + " + Arrays.toString(source) + " ] -> " + Arrays.toString(expected)
                    + " != " + actualPage;
        assertEqualPages(message,
                         expected,
                         actualPage);
        assertEquals("Invalid moreItems info!",
                     (pageSize != 0) && (skip + pageSize < results.size() + source.length),
                     actualResults.hasMoreItems());
        assertTrue(message,
                   (pageSize == 0) || actualPage.size() <= pageSize);
        final int expectedTotal = source.length + results.size();
        if (boundsError && !new Pair<Integer, Integer>(null,
                                                       null).equals(actualResults.getTotalResultCount()))
        {
            assertEquals("Invalid total info",
                         new Pair<Integer, Integer>(expectedTotal,
                                                    expectedTotal),
                         actualResults.getTotalResultCount());
        }
        logger.info(actualPage);
    }

    @Test
    public void testCollate() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(2,
                                               4,
                                               9,
                                               10);
        final Integer[] s2 = new Integer[] { 0, 1, 3, 5, 7, 8 };

        assertCollate(s1,
                      s2,
                      0,
                      3);
        assertCollate(s1,
                      s2,
                      2,
                      3);

        final List<Integer> s3 = Arrays.asList(2,
                                               4,
                                               9,
                                               10,
                                               12,
                                               15,
                                               17,
                                               18);
        final Integer[] s4 = new Integer[] { 0, 1, 3, 8, 16, 19 };

        assertCollate(s3,
                      s4,
                      0,
                      3);
        assertCollate(s3,
                      s4,
                      2,
                      3);
        assertCollate(s3,
                      s4,
                      6,
                      3);
        assertCollate(s3,
                      s4,
                      7,
                      3);

        final List<Integer> s5 = Arrays.asList(2,
                                               3,
                                               6,
                                               8);
        final Integer[] s6 = new Integer[] { 7, 10, 13, 11, 17, 19 };

        assertCollate(s5,
                      s6,
                      0,
                      1);
        assertCollate(s5,
                      s6,
                      1,
                      2);

    }

    public void testCollateBoundary1() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(2,
                                               3);
        final Integer[] s2 = new Integer[] { 7, 10, 13, 11 };

        assertCollate(s1,
                      s2,
                      6,
                      1,
                      true);
    }

    public void testCollateBoundary2() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(2,
                                               3);
        final Integer[] s2 = new Integer[] { 7, 10, 13, 11 };

        assertCollate(s1,
                      s2,
                      5,
                      2,
                      true);
    }

    public void testCollateBoundary3() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(1,
                                               2,
                                               3);
        final Integer[] s2 = new Integer[] { 5, 6, 7, 8 };

        assertCollate(s1,
                      s2,
                      6,
                      1,
                      true);
    }

    public void testCollateBoundary4() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(1,
                                               2,
                                               3);
        final Integer[] s2 = new Integer[] { 5 };

        assertCollate(s1,
                      s2,
                      3,
                      1,
                      false);
    }

    public void testCollateBoundary5() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(2,
                                               3);
        final Integer[] s2 = new Integer[] { 7, 10, 13 };

        assertCollate(s1,
                      s2,
                      10,
                      2,
                      true);
    }

    public void testCollateBoundary6() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(2,
                                               3);
        final Integer[] s2 = new Integer[] { 7, 10, 13 };

        assertCollate(s1,
                      s2,
                      0,
                      0,
                      true);
    }

    public void testCollateBoundary7() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(10);
        final Integer[] s2 = new Integer[] { 1, 2, 3 };

        assertCollate(s1,
                      s2,
                      6,
                      1,
                      true);
    }

    public void testCollateBoundary8() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(1);
        final Integer[] s2 = new Integer[] { 5, 6, 7 };

        assertCollate(s1,
                      s2,
                      5,
                      1,
                      true);
    }

    public void testCollateBoundary9() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(1);
        final Integer[] s2 = new Integer[] { 5 };

        assertCollate(s1,
                      s2,
                      0,
                      2,
                      true);
    }

    public void testCollateBoundary10() throws Exception
    {
        final List<Integer> s1 = Arrays.asList();
        final Integer[] s2 = new Integer[] { 5 };

        assertCollate(s1,
                      s2,
                      0,
                      2,
                      true);
    }

    public void testCollateBoundary11() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(1);
        final Integer[] s2 = new Integer[] {};

        assertCollate(s1,
                      s2,
                      0,
                      1,
                      false);
    }

    public void testCollateBoundary12() throws Exception
    {
        final List<Integer> s1 = Arrays.asList();
        final Integer[] s2 = new Integer[] {};

        assertCollate(s1,
                      s2,
                      0,
                      1,
                      false);
    }

    public void testCollateBoundary13() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(6,
                                               7,
                                               8,
                                               9,
                                               10,
                                               11,
                                               12,
                                               13,
                                               14);
        final Integer[] s2 = new Integer[] { 1 };

        assertCollate(s1,
                      s2,
                      3,
                      5,
                      false);
    }

    public void testCollateFalsePositive1() throws Exception
    {
        final List<Integer> s1 = Arrays.asList(2,
                                               3);
        final Integer[] s2 = new Integer[] { 7, 10, 13 };

        try
        {
            new PageCollator<Integer>().collate(s1,
                                                new ArrayPageSource(s2),
                                                new PagingRequest(-1,
                                                                  1),
                                                new ComparableComparator<Integer>());
            fail("Invalid page data.");
        }
        catch (PageCollationException e)
        {
            logger.info(e.getMessage());
        }

        try
        {
            new PageCollator<Integer>().collate(s1,
                                                new ArrayPageSource(s2),
                                                new PagingRequest(1,
                                                                  -1),
                                                new ComparableComparator<Integer>());
            fail("Invalid page data.");
        }
        catch (PageCollationException e)
        {
            logger.info(e.getMessage());
        }
    }
}
