package edu.caltech.cs2.interfaces;

import edu.caltech.cs2.interfaces.ICollection;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

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

import static edu.caltech.cs2.project03.Project03TestOrdering.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public interface ICollectionTests {
    ICollection<Object> newCollection();

    @Order(collectionTestLevel)
    @DisplayName("Simple tests of various ICollection functions")
    @ParameterizedTest(name = "Test add(), size(), isEmpty(), contains(), and clear() on [{arguments}]")
    @ValueSource(strings = {
            "",
            "1",
            "0, 1, 2, 3",
            "5, 4, 3, 2, 1",
            "8, 3, 5, 7, 4, 3, 12, 12, 1"
    })
    default void testCollectionFunctions(String inputs) {
        ICollection<Object> impl = newCollection();
        List<Object> reference = new java.util.ArrayList<>();
        // Check that collection is empty
        assertTrue(impl.isEmpty(), "collection should be empty");

        // Check that values are not in collection
        for (Object value : inputs.trim().split(", ")) {
            assertFalse(impl.contains(value), "value should not be contained");
        }

        // Add all values to collection
        for (Object value : inputs.trim().split(", ")) {
            impl.add(value);
            reference.add(value);
        }

        // Check that size() and isEmpty() is correct
        assertEquals(reference.size(), impl.size(), "sizes should be equal");
        assertFalse(impl.isEmpty(), "collection should not be empty");

        // Check that values are in collection
        for (Object value : inputs.trim().split(", ")) {
            assertTrue(impl.contains(value), "value should be contained");
        }

        // Clear and make sure size() and isEmpty() match
        impl.clear();
        assertEquals(0, impl.size(), "size should be 0");
        assertTrue(impl.isEmpty(), "collection should be empty");

        // Check that values are not in collection
        for (Object value : inputs.trim().split(", ")) {
            assertFalse(impl.contains(value), "value should not be contained");
        }
    }

    @Order(collectionTestLevel)
    @Test
    @DisplayName("Test repeated emptying and filling of ICollection with single element")
    default void testFillEmptyCollection() {
        ICollection<Object> impl = newCollection();
        for (int i = 0; i < 10; i ++) {
            impl.add("a");
            assertEquals(1, impl.size(), "collection should have 1 element");
            impl.clear();
            assertTrue(impl.isEmpty());
        }
    }

    @Order(collectionTestLevel)
    @DisplayName("Stress test for add(...)")
    @ParameterizedTest(name = "Test add()ing {1} random numbers with seed = {0}")
    @CsvSource({
            "100, 3000", "42, 1000"
    })
    default void stressTestAdd(int seed, int size) {
        Random r = new Random(seed);
        List<Integer> reference = new java.util.ArrayList<>();
        ICollection<Object> impl = newCollection();
        // Test adding values updates size and displays contained correctly
        for (int i = 0; i < size; i++) {
            int num = r.nextInt();
            reference.add(num);
            impl.add(num);
            assertEquals(reference.size(), impl.size(), "size()s are not equal");
            assertEquals(reference.contains(num), impl.contains(num), "value should be contained");
        }
        // Test that values not in collection are not contained
        for (int i = 0; i < size; i++) {
            int num = r.nextInt();
            assertEquals(reference.contains(num), impl.contains(num), "contained values do not match");
        }
    }

    @Order(collectionTestLevel)
    @DisplayName("Stress test for contains(...)")
    @ParameterizedTest(name = "Test contains() with {1} random numbers and seed = {0}")
    @CsvSource({
            "100, 3000", "42, 1000"
    })
    default void stressTestContains(int seed, int size) {
        Random r = new Random(seed);
        List<Integer> nums = new java.util.ArrayList<>();
        ICollection<Object> impl = newCollection();
        // Add values to both the list of nums and test collection
        for (int i = 0; i < size; i++) {
            int num = r.nextInt();
            nums.add(num);
            impl.add(num);
        }
        // Shuffle order of nums and check that all are contained in the collection
        Collections.shuffle(nums);
        for (int num : nums) {
            assertTrue(impl.contains(num), "value should be contained");
        }

        // Test that values not in collection are not contained
        for (int i = 0; i < size; i++) {
            int num = r.nextInt();
            assertEquals(nums.contains(num), impl.contains(num), "contained values do not match");
        }
    }
}