package de.accso.flexinale.common.shared_kernel;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

class SelectionTest {

    private record OPTION(Integer i) {}

    @Test
    void testRandomSelectorOneOption() {
        // arrange
        Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);
    }

    @Test
    void testRandomSelectorAddOption() {
        // arrange
        Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.add(new OPTION(2));

        // assert
        selected = sut.next();
        assertThat(selected.size()).isEqualTo(1);
    }

    @Test
    void testRandomSelectorRemoveOnlyOption() {
        // arrange
        Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.remove(new OPTION(1));

        Set<OPTION> selectedAgain = sut.next();
        assertThat(selectedAgain.isEmpty()).isTrue();
    }

    @Test
    void testRandomSelectorRemoveOption() {
        // arrange
        Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
        sut.addAll(options);

        // act
        sut.remove(new OPTION(1));

        // assert
        Set<OPTION> selected = sut.next();

        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected.contains(new OPTION(2))).isTrue();
    }

    @Test
    void testRandomSelectorMultipleOptions() {
        // arrange
        Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
        int numOptions = 100;
        HashSet<OPTION> options = new HashSet<>();
        for (int counter=0; counter<numOptions; counter++) {
            options.add(new OPTION(counter));
        }
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options.containsAll(selected)).isTrue();
    }


    @Test
    void testConstantFirstSelectorOneOption() {
        // arrange
        Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
        HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
        sut.addAll(options);

        // act, first choice
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected).isEqualTo(options);

        // act, second choice does not change
        selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(options);
    }

    @Test
    void testConstantFirstSelectorAddOption() {
        // arrange
        Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.add(new OPTION(2));

        // assert
        selected = sut.next();
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);
    }

    @Test
    void testConstantFirstSelectorRemoveOnlyOption() {
        // arrange
        Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.remove(new OPTION(1));

        Set<OPTION> selectedAgain = sut.next();
        assertThat(selectedAgain.isEmpty()).isTrue();
    }

    @Test
    void testConstantFirstSelectorRemoveOption() {
        // arrange
        Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
        sut.addAll(options);

        // act
        sut.remove(new OPTION(1));

        // assert
        Set<OPTION> selected = sut.next();

        assertThat(selected.size()).isEqualTo(1);
        assertThat(options.contains(new OPTION(2))).isTrue();
    }

    @Test
    void testConstantFirstFirstSelectorMultipleOptions() {
        // arrange
        Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
        int numOptions = 100;
        HashSet<OPTION> options = new HashSet<>();
        for (int counter=0; counter<numOptions; counter++) {
            options.add(new OPTION(counter));
        }
        sut.addAll(options);

        // act, first choice
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));

        // act, second choice does not change
        selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
    }

    @Test
    void testConstantLastSelectorOneOption() {
        // arrange
        Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
        HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
        sut.addAll(options);

        // act, first choice
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));

        // act, second choice does not change
        selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
    }

    @Test
    void testConstantLastSelectorAddOption() {
        // arrange
        Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.add(new OPTION(2));

        // assert
        selected = sut.next();
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(2)));
    }

    @Test
    void testConstantLastSelectorRemoveOnlyOption() {
        // arrange
        Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.remove(new OPTION(1));

        Set<OPTION> selectedAgain = sut.next();
        assertThat(selectedAgain.isEmpty()).isTrue();
    }

    @Test
    void testConstantLastSelectorRemoveOption() {
        // arrange
        Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
        sut.addAll(options);

        // act
        sut.remove(new OPTION(1));

        // assert
        Set<OPTION> selected = sut.next();

        assertThat(selected.size()).isEqualTo(1);
        assertThat(options.contains(new OPTION(2))).isTrue();
    }

    @Test
    void testConstantLastSelectorMultipleOptions() {
        // arrange
        Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
        int numOptions = 100;
        HashSet<OPTION> options = new HashSet<>();
        for (int counter=0; counter<numOptions; counter++) {
            options.add(new OPTION(counter));
        }
        sut.addAll(options);

        // act, first choice
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(numOptions - 1)));

        // act, second choice does not change
        selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(numOptions - 1)));
    }

    @Test
    void testRoundRobinSelectorOneOption() {
        // arrange
        Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
        HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
        sut.addAll(options);

        // act, first choice
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));

        // act, second choice does not change
        selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
    }

    @Test
    void testRoundRobinSelectorAddOption() {
        // arrange
        Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.add(new OPTION(2));

        // assert
        selected = sut.next();
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(options); // start from scratch
        selected = sut.next();
        assertThat(selected).isEqualTo(Set.of(new OPTION(2)));
    }

    @Test
    void testRoundRobinSelectorRemoveOnlyOption() {
        // arrange
        Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.remove(new OPTION(1));

        Set<OPTION> selectedAgain = sut.next();
        assertThat(selectedAgain.isEmpty()).isTrue();
    }

    @Test
    void testRoundRobinSelectorRemoveOption() {
        // arrange
        Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
        sut.addAll(options);

        // act
        sut.remove(new OPTION(1));

        // assert
        Set<OPTION> selected = sut.next();

        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected.contains(new OPTION(2))).isTrue();
        selected = sut.next();
        assertThat(selected.contains(new OPTION(2))).isTrue();
    }

    @Test
    void testRoundRobinSelectorMultipleOptions() {
        // arrange
        Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
        int numOptions = 100;
        HashSet<OPTION> options = new HashSet<>();
        for (int counter=0; counter<numOptions; counter++) {
            options.add(new OPTION(counter));
        }
        sut.addAll(options);

        // act
        for (int counter=0; counter<numOptions; counter++) {
            Set<OPTION> selected = sut.next();

            // assert
            assertThat(selected.size()).isEqualTo(1);
            assertThat(selected).isEqualTo(Set.of(new OPTION(counter)));
        }
    }

    @Test
    void testRoundRobinSelectorMultipleOptionsSelectingMoreThanOptions() {
        // arrange
        Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
        int numOptions = 100;
        HashSet<OPTION> options = new HashSet<>();
        for (int counter=0; counter<numOptions; counter++) {
            options.add(new OPTION(counter));
        }
        sut.addAll(options);

        // act once - selection goes through all <numOptions>
        List<OPTION> selectedFirstTime = new ArrayList<>();
        for (int counter=0; counter<numOptions; counter++) {
            selectedFirstTime.addAll(sut.next());
        }

        // act again - selection starts from scratch, goes again through all <numOptions>
        List<OPTION> selectedSecondTime = new ArrayList<>();
        for (int counter=0; counter<numOptions; counter++) {
            selectedSecondTime.addAll(sut.next());
        }

        // assert
        assertThat(selectedFirstTime.size()).isEqualTo(numOptions);
        assertThat(selectedFirstTime.size()).isEqualTo(selectedSecondTime.size());
        assertThat(selectedFirstTime).isEqualTo(selectedSecondTime);
    }

    @Test
    void testAllSelectorOneOption() {
        // arrange
        Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
        HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
        sut.addAll(options);

        // act, first choice
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));

        // act, second choice does not change
        selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
    }


    @Test
    void testAllSelectorAddOption() {
        // arrange
        Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.add(new OPTION(2));

        // assert
        selected = sut.next();
        assertThat(selected.size()).isEqualTo(2);
    }

    @Test
    void testAllSelectorRemoveOnlyOption() {
        // arrange
        Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1));
        sut.addAll(options);

        // act
        Set<OPTION> selected = sut.next();

        // assert
        assertThat(selected.size()).isEqualTo(1);
        assertThat(options).isEqualTo(selected);

        // act
        sut.remove(new OPTION(1));

        Set<OPTION> selectedAgain = sut.next();
        assertThat(selectedAgain.isEmpty()).isTrue();
    }

    @Test
    void testAllSelectorRemoveOption() {
        // arrange
        Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
        Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
        sut.addAll(options);

        // act
        sut.remove(new OPTION(1));

        // assert
        Set<OPTION> selected = sut.next();

        assertThat(selected.size()).isEqualTo(1);
        assertThat(selected.contains(new OPTION(2))).isTrue();
    }

    @Test
    void testAllSelectorMultipleOptions() {
        // arrange
        Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
        int numOptions = 100;
        HashSet<OPTION> options = new HashSet<>();
        for (int counter=0; counter<numOptions; counter++) {
            options.add(new OPTION(counter));
        }
        sut.addAll(options);

        // act
        for (int counter=0; counter<numOptions; counter++) {
            Set<OPTION> selected = sut.next();

            // assert
            assertThat(selected.size()).isEqualTo(numOptions);
            assertThat(selected).isEqualTo(options);
        }
    }
}