package aud.exam.prep;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

public abstract class SequenceProcessorTest<S> {

    protected final Comparator<Integer> cmp = Integer::compareTo;

    protected final SequenceProcessor<Integer, S> processor;

    protected SequenceProcessorTest(SequenceProcessor<Integer, S> processor) {
        this.processor = processor;
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_createAndIterateWork(List<Integer> list) {
        S s = processor.create(list);

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_findOfNonExistsIsFalse(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.find(s, -1));
        assertFalse(processor.find(s, 999));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_findOfExistsIsTrue(List<Integer> list) {
        S s = processor.create(list);
        for (var n : list) {
            assertTrue(processor.find(s, n));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_findBinaryOfNonExistsIsFalse(List<Integer> list) {
        list.sort(cmp);
        S s = processor.create(list);
        assertFalse(processor.findBinary(s, -1, cmp));
        assertFalse(processor.findBinary(s, 999, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_findBinaryOfExistsIsTrue(List<Integer> list) {
        list.sort(Comparable::compareTo);
        S s = processor.create(list);
        for (var n : list) {
            assertTrue(processor.findBinary(s, n, cmp));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_overrideOfNotExistsIsFalseAndDoesNotModify(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.override(s, -1, -5));
        assertFalse(processor.override(s, 999, -5));

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_overrideOfExistsIsTrueAndDoesModify(List<Integer> list) {
        S s = processor.create(list);
        for (int i = 0; i < list.size(); i++) {
            var to = -(i*i) - 5;

            assertTrue(processor.override(s, list.get(i), to));
            list.set(i, to);

            assertTrue(processor.check(s));
            assertIterableEquals(list, processor.iterate(s));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_overrideAllOfNotExistsIsFalseAndDoesNotModify(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.overrideAll(s, -1, -5));
        assertFalse(processor.overrideAll(s, 999, -5));

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_overrideAllOfExistsIsTrueAndDoesModify(List<Integer> list) {
        S s = processor.create(list);
        for (int i = 0; i < list.size(); i++) {
            var from = list.get(i);
            var to = -(i*i) - 5;

            assertTrue(processor.overrideAll(s, from, to));
            list.replaceAll(k ->
                Objects.equals(k, from) ? to : k);

            assertTrue(processor.check(s));
            assertIterableEquals(list, processor.iterate(s));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_overrideAtOfBadIndexIsFalseAndDoesNotModify(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.overrideAt(s, -5, -1));
        assertFalse(processor.overrideAt(s, -5, 999));

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_overrideAtOfExistsIsTrueAndDoesModify(List<Integer> list) {
        S s = processor.create(list);
        for (int i = 0; i < list.size(); i++) {
            var to = -(i*i) - 5;

            assertTrue(processor.overrideAt(s, to, i));
            list.set(i, to);

            assertTrue(processor.check(s));
            assertIterableEquals(list, processor.iterate(s));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_insertInOrderWorks(List<Integer> list) {
        list.sort(cmp);
        S s = processor.create(list);

        for (int i = -1; i < 110; i++) {
            processor.insertInOrder(s, i, cmp);

            list.add(i);
            list.sort(cmp);

            assertTrue(processor.check(s));
            assertIterableEquals(list, processor.iterate(s));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_removeOfNonExistsIsFalseAndDoesNotModify(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.remove(s, -1));
        assertFalse(processor.remove(s, 999));

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_removeOfExistsIsTrueAndDoesModify(List<Integer> list) {
        S s = processor.create(list);

        while (!list.isEmpty()) {
            var toRemove = list.get(list.size()/2);

            list.remove(toRemove);
            assertTrue(processor.remove(s, toRemove));

            assertTrue(processor.check(s));
            assertIterableEquals(list, processor.iterate(s));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_removeAllOfNonExistsIsFalseAndDoesNotModify(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.removeAll(s, -1));
        assertFalse(processor.removeAll(s, 999));

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_removeAllOfExistsIsTrueAndDoesModify(List<Integer> list) {
        S s = processor.create(list);

        while (!list.isEmpty()) {
            var toRemove = list.get(list.size()/2);

            list.removeAll(Collections.singleton(toRemove));
            assertTrue(processor.removeAll(s, toRemove));

            assertTrue(processor.check(s));
            assertIterableEquals(list, processor.iterate(s));
        }
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_ifAscendingOfUnsortedIsFalse(List<Integer> list) {
        assumeTrue(list.size() > 2, "The list is to small");

        list.add(list.size()/2, -1);
        S s = processor.create(list);
        assertFalse(processor.isAscending(s, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_ifAscendingOfSortedIsTrue(List<Integer> list) {
        list.sort(cmp);
        S s = processor.create(list);
        assertTrue(processor.isAscending(s, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_maxWorks(List<Integer> list) {
        list.add(-1);
        S s = processor.create(list);
        var max = list
            .stream()
            .max(cmp)
            .orElseThrow();
        assertEquals(max, processor.max(s, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_secondMaxWorks(List<Integer> list) {
        list.add(-1);
        list.add(-2);
        S s = processor.create(list);
        var max = list
            .stream()
            .sorted(cmp.reversed())
            .skip(1)
            .findFirst()
            .orElseThrow();
        assertEquals(max, processor.secondMax(s, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_isItemWiseLessOrEqualOfSameIsTrue(List<Integer> list) {
        S s = processor.create(list);
        assertTrue(processor.isItemWiseLessOrEqual(s, s, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_isItemWiseLessOrEqualWithPlus5Works(List<Integer> list) {
        S a = processor.create(list);

        for (int i = 0; i < list.size(); i++) {
            if (i % 2 == 0) {
                list.set(i, list.get(i)+5);
            }
        }
        S b = processor.create(list);

        assertTrue(processor.isItemWiseLessOrEqual(a, b, cmp));

        assumeTrue(!list.isEmpty(), "List must not be empty to be less then");
        assertFalse(processor.isItemWiseLessOrEqual(b, a, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_isItemWiseLessOrEqualWithMoreElementsWorks(List<Integer> list) {
        S a = processor.create(list);

        for (int i = 1; i < 4; i++) {
            list.add(-i);
        }
        S b = processor.create(list);

        assertTrue(processor.isItemWiseLessOrEqual(a, b, cmp));
        assertFalse(processor.isItemWiseLessOrEqual(b, a, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_isItemWiseLessOfSameIsFalse(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.isItemWiseLess(s, s, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_isItemWiseLessWithAlmostSameIsTrue(List<Integer> list) {
        S a = processor.create(list);

        var index = list.size()/2;
        if (list.isEmpty()) {
            list.add(1);
        } else {
            list.set(index, list.get(index)+1);
        }
        S b = processor.create(list);

        assertTrue(processor.isItemWiseLess(a, b, cmp));
        assertFalse(processor.isItemWiseLess(b, a, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_isItemWiseLessWithPlus5Works(List<Integer> list) {
        S a = processor.create(list);

        for (int i = 0; i < list.size(); i++) {
            if (i % 2 == 0) {
                list.set(i, list.get(i)+5);
            }
        }
        if (list.isEmpty()) {
            list.add(1);
        }
        S b = processor.create(list);

        assertTrue(processor.isItemWiseLess(a, b, cmp));
        assertFalse(processor.isItemWiseLess(b, a, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_isLexSmallerOfSameIfFalse(List<Integer> list) {
        S s = processor.create(list);
        assertFalse(processor.isLexSmaller(s, s, cmp));
    }

    @ParameterizedTest
    @ArgumentsSource(DoubleLatinProvider.class)
    void testThat_isLexSmallerWithWorksLikeStrings(String stringA, String stringB) {
        S a = toSequenceByAscii(stringA);
        S b = toSequenceByAscii(stringB);

        assertEquals(
            stringA.compareTo(stringB) < 0,
            processor.isLexSmaller(a, b, cmp));

        assertEquals(
            stringB.compareTo(stringA) < 0,
            processor.isLexSmaller(b, a, cmp));
    }

    protected S toSequenceByAscii(String string) {
        var list = string
            .chars()
            .boxed()
            .collect(Collectors.toList());
        return processor.create(list);
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_doubleAllKeysWorks(List<Integer> list) {
        S s = processor.create(list);

        list = list
            .stream()
            .flatMap(n ->
                Stream.of(n, n))
            .collect(Collectors.toList());

        processor.doubleAllKeys(s);
        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_rotateRightWorks(List<Integer> list) {
        S s = processor.create(list);

        if (list.size() > 1) {
            var right = list.remove(list.size()-1);
            list.add(0, right);
        }

        s = processor.rotateRight(s);

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_rotateLeftWorks(List<Integer> list) {
        S s = processor.create(list);

        if (list.size() > 1) {
            var left = list.remove(0);
            list.add(left);
        }

        s = processor.rotateLeft(s);

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(DuplicateListProvider.class)
    void testThat_removeDuplicatesWorks(List<Integer> list) {
        S s = processor.create(list);

        var noDuplicatesList = new ArrayList<Integer>();
        for (var n : list) {
            if (noDuplicatesList.isEmpty() || !Objects.equals(n, noDuplicatesList.get(noDuplicatesList.size()-1))) {
                noDuplicatesList.add(n);
            }
        }

        processor.removeDuplicates(s);

        assertTrue(processor.check(s));
        assertIterableEquals(noDuplicatesList, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_invertWorks(List<Integer> list) {
        S s = processor.create(list);

        Collections.reverse(list);
        s = processor.invert(s);

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(ListProvider.class)
    void testThat_cloneWorks(List<Integer> list) {
        S a = processor.create(list);
        S b = processor.clone(a);

        assertNotSame(a, b);
        assertTrue(processor.check(b));
        assertIterableEquals(list, processor.iterate(b));
    }

    @ParameterizedTest
    @ArgumentsSource(DoubleListProvider.class)
    void testThat_cloneWorks(List<Integer> listA, List<Integer> listB) {
        S a = processor.create(listA);
        S b = processor.create(listB);

        var list = new ArrayList<Integer>();
        while(!listA.isEmpty() || !listB.isEmpty()) {
            if (!listA.isEmpty()) {
                list.add(listA.remove(0));
            }
            if (!listB.isEmpty()) {
                list.add(listB.remove(0));
            }
        }

        S s = processor.alternate(a, b);

        assertTrue(processor.check(s));
        assertIterableEquals(list, processor.iterate(s));
    }

    @ParameterizedTest
    @ArgumentsSource(DoubleListProvider.class)
    void testThat_mergeWorks(List<Integer> listA, List<Integer> listB) {
        listA.sort(cmp);
        S a = processor.create(listA);

        listB.sort(cmp);
        S b = processor.create(listB);

        listA.addAll(listB);
        listA.sort(cmp);

        S s = processor.merge(a, b, cmp);

        assertTrue(processor.check(s));
        assertIterableEquals(listA, processor.iterate(s));
    }
}