/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.suite.sets;

import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Resource;
import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.collection.sets.HashSet;
import net.ranides.assira.test.TCollection;
import net.ranides.assira.test.TCollection.TItem;
import net.ranides.assira.test.TCollection.TItems;
import static net.ranides.assira.junit.NewAssert.*;
import org.junit.Ignore;
import net.ranides.assira.junit.TestContract;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
@SuppressWarnings("PMD.MethodNameRules")
public class SetTester<T> {
    
    private static final int BASIC_PUT = 128;
    
    private static final int BASIC_PUT_REPLACE = 40;
    
    private static final int BASIC_PUT_RANDOM = 220;
    
    private static final int BASIC_REMOVE_ALL = 97;
    
    private static final int BASIC_REMOVE_RANDOM = 273;
    
    @Resource(name = "collection!")
    private TCollection<T> $var;
    
    @TestContract
    public void basicAdd(Set<T> target) {
        for(TItem<T> item : $var.range(BASIC_PUT)) {
            target.add(item.value());
        }
        assertEquals(BASIC_PUT, target.size());
    }
    
    @TestContract
    public void basicAdd_HC(Set<T> target) {
        basicAdd(target);
        
        int prev = target.size();
        TItem<T> item1 = $var.item(1001,0);
        TItem<T> item2 = $var.item(1001,1);
        assertTrue(target.add(item1.value()));
        assertEquals(prev + 1, target.size());
        
        assertFalse(target.add(item1.value()));
        assertEquals(prev + 1, target.size());
        
        assertTrue(target.add(item2.value())); // identical hash but not eq
        assertEquals(prev + 2, target.size());
    }
    
    @TestContract
    public void basicAddReplace_HC(Set<T> target) {
        int prev = target.size();
        for (TItem<T> item : $var.urange(0, BASIC_PUT_REPLACE)) {
            target.add(item.value());
        }
        assertEquals(prev + (2 * BASIC_PUT_REPLACE), target.size());
    }
    
    @TestContract
    public void basicAddReplace(Set<T> target) {
        int prev = target.size();
        for (TItem<T> item : $var.range(BASIC_PUT_REPLACE)) {
            target.add(item.value());
        }
        assertEquals(prev + BASIC_PUT_REPLACE, target.size());
    }
	
	@TestContract
    public void basicAddRandom_HC(Set<T> target) {
        int prev = target.size();
        for (TItem<T> item : $var.urange(0, BASIC_PUT_RANDOM)) {
            target.add(item.value());
        }
        assertEquals(prev + (2 * BASIC_PUT_RANDOM), target.size());
    }
    
    @TestContract
    public void basicPutRandom(Set<T> target) {
        int prev = target.size();
        for (TItem<T> item : $var.range(0, BASIC_PUT_RANDOM)) {
            target.add(item.value());
        }
        assertEquals(prev + BASIC_PUT_RANDOM, target.size());
    }
    
    @TestContract
    public void basicRemove_HC(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3});
        
        assertNotRemove(target, $var.item(1,1));
        
        assertRemove(target, $var.item(1));
        assertRemove(target, $var.item(2));
        assertRemove(target, $var.item(3));
        
        assertEquals(0, target.size());
        
        assertNotRemove(target, $var.item(3));
    }
    
    @TestContract
    public void basicRemove(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3});
        
        assertNotRemove(target, $var.item(5));
        
        assertRemove(target, $var.item(1));
        assertRemove(target, $var.item(2));
        assertRemove(target, $var.item(3));
        
        assertEquals(0, target.size());
        
        assertNotRemove(target, $var.item(3));
    }
   
    @TestContract
    public void basicRemoveAll(Function<int[], Set<T>> f) {
        TItems<T> items = $var.range(0, BASIC_REMOVE_ALL);
        Set<T> target = f.apply(items.indexes());

        assertEquals(BASIC_REMOVE_ALL, target.size());
        
        for(int i=0; i<BASIC_REMOVE_ALL; i++) {
            assertRemove(target, items.at(i));
            assertEquals(BASIC_REMOVE_ALL-i-1, target.size());
        }
        assertEquals(0, target.size());
    }
    
    @TestContract
    public void basicRemoveRandom(Function<int[], Set<T>> f) {
        TItems<T> items = $var.range(0, BASIC_REMOVE_RANDOM);
        Set<T> target = f.apply(items.indexes());

        assertEquals(BASIC_REMOVE_RANDOM, target.size());

        items.shuffle(777);
        for(int i=0; i<BASIC_REMOVE_RANDOM; i++) {
            assertRemove(target, items.at(i));
            assertEquals(BASIC_REMOVE_RANDOM-i-1, target.size());
        }
        assertEquals(0, target.size());
    }
    
    @TestContract
    public void logicRemove_HC(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3});
        
        assertFalse(target.remove($var.item(1,1).value()));
        assertEquals(3, target.size());
		
        assertContains(target, $var.item(1));
        assertRemove(target, $var.item(1));
        assertNotContains(target, $var.item(1));
        
        assertContains(target, $var.item(2));
        assertRemove(target, $var.item(2));
        assertNotContains(target, $var.item(2));
        
        assertContains(target, $var.item(3));
        assertRemove(target, $var.item(3));
        assertNotContains(target, $var.item(3));
    }
    
    @TestContract
    public void logicRemove(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3});
        
        assertFalse(target.remove($var.item(5).value()));
        assertEquals(3, target.size());
		
        assertContains(target, $var.item(1));
        assertRemove(target, $var.item(1));
        assertNotContains(target, $var.item(1));
        
        assertContains(target, $var.item(2));
        assertRemove(target, $var.item(2));
        assertNotContains(target, $var.item(2));
        
        assertContains(target, $var.item(3));
        assertRemove(target, $var.item(3));
        assertNotContains(target, $var.item(3));
    }
    
    @TestContract
    public void basicContains_HC(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2});
        
        assertEquals(true, target.contains($var.item(1).value()));
        assertEquals(true, target.contains($var.item(2).value()));
        
        assertEquals(false, target.contains($var.item(1,1).value()));
        assertEquals(false, target.contains($var.item(3).value()));
    }
    
    @TestContract
    public void basicContains(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2});
        
        assertEquals(true, target.contains($var.item(1).value()));
        assertEquals(true, target.contains($var.item(2).value()));
        assertEquals(false, target.contains($var.item(3).value()));
    }
    
    @TestContract
    public void basicClear(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3});

		assertEquals(3, target.size());
		assertEquals(false, target.isEmpty());
		
		target.clear();
        assertEquals(0, target.size());
		assertEquals(true, target.isEmpty());
		
		target.clear();
		assertEquals(0, target.size());
		assertEquals(true, target.isEmpty());
	}
	
	@TestContract
    public void basicClear_HC(Set<T> target) {
		assertEquals(0, target.size());

        $var.list().item(1).item(1,1).item(2).into(target);
		assertEquals(3, target.size());
		assertEquals(false, target.isEmpty());
		
		target.clear();
		assertEquals(0, target.size());
		assertEquals(true, target.isEmpty());
		
		target.clear();
		assertEquals(0, target.size());
		assertEquals(true, target.isEmpty());
	}
    
    @TestContract
    public void basicEquals_HC(Supplier<Set<T>> generator) {
        Set<T> map1 = generator.get();
        Set<T> map2 = generator.get();
        Set<T> map3 = generator.get();
        Set<T> map4 = generator.get();
        Set<T> map5 = generator.get();
        
        $var.list().item(5,0).item(2).item(3).into(map1);
        $var.list().item(5,0).item(2).item(3).into(map2);
        $var.list().item(5,1).item(2).item(3).into(map3);
        $var.list()          .item(2).item(3).into(map4);
        $var.list().item(5,1)        .item(3).item(4).into(map5);

        assertEquality(map1, map2, map3, map4);
        assertEquality(map1, map2, map3, map5);
    }

    @TestContract
    public void basicEquals(Function<int[], Set<T>> f) {
        Set<T> map1 = f.apply(new int[]{5,2,3});
        Set<T> map2 = f.apply(new int[]{5,2,3});
        Set<T> map4 = f.apply(new int[]{2,3});
        Set<T> map5 = f.apply(new int[]{7,3,4});
        
        assertEquality(map1, map2, map4);
        assertEquality(map1, map2, map5);
    }
    
    @TestContract
    public void basicSerializable_HC(Set<T> target) {
        $var.list().item(1).item(1,1).item(2).item(3).into(target);
        assertSerializable(target);
    }
    
    @TestContract
    public void basicSerializable(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3,4});
        assertSerializable(target);
    }
    
    @TestContract
    public void basicIterator_HC(Set<T> target) {
        assertEquals(0, target.size());
		$var.list().item(1).item(1,1).item(2).item(3).into(target);
		assertEquals(4, target.size());

        Iterator<T> iterator = target.iterator();
        while(iterator.hasNext()) {
            T key = iterator.next();
            assertEquals(true, target.contains(key));
        }
        
        assertThrows(NoSuchElementException.class, ()-> iterator.next());
	}
    
    @TestContract
    public void basicIterator(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3,5});
		assertEquals(4, target.size());

        Iterator<T> iterator = target.iterator();
        while(iterator.hasNext()) {
            T key = iterator.next();
            assertEquals(true, target.contains(key));
        }
        
        assertThrows(NoSuchElementException.class, ()-> iterator.next());
	}
    
    @TestContract
    public void basicIterator_Remove_HC(Set<T> target) {
		assertEquals(0, target.size());
		$var.list().item(1).item(1,1).item(2).item(3).into(target);
		T k1 = $var.item(1).value();
		T k2 = $var.item(2).value();
        
		Iterator<T> iterator = target.iterator();
		while(iterator.hasNext()) {
			T key = iterator.next();
			if(k1.equals(key)) {
                iterator.remove();
            }
            if(k2.equals(key)) {
                iterator.remove();
                assertThrows(IllegalStateException.class, ()-> iterator.remove());
            }
		}
        
        assertEquals(2, target.size());
        assertNotContains(target, $var.item(1));
        assertNotContains(target, $var.item(2));
		assertContains(target, $var.item(1,1));
		assertContains(target, $var.item(3));
        
        target.clear();
        assertEquals(0, target.size());
	}
    
    @TestContract
    public void basicIterator_Remove(Function<int[], Set<T>> f) {
        Set<T> target = f.apply(new int[]{1,2,3,5});

		T k1 = $var.item(1).value();
		T k2 = $var.item(2).value();
        
		Iterator<T> iterator = target.iterator();
		while(iterator.hasNext()) {
			T key = iterator.next();
			if(k1.equals(key)) {
                iterator.remove();
            }
            if(k2.equals(key)) {
                iterator.remove();
                assertThrows(IllegalStateException.class, ()-> iterator.remove());
            }
		}
        
        assertEquals(2, target.size());
        assertNotContains(target, $var.item(1));
        assertNotContains(target, $var.item(2));
		assertContains(target, $var.item(5));
		assertContains(target, $var.item(3));
        
        target.clear();
        assertEquals(0, target.size());
	}
    
    @TestContract
    public void randomIterator_Remove(Function<int[], Set<T>> f) {
        Random rand = new Random(777);
        for(int i=0; i<100; i++) {
            randomIterator_Remove(f,rand.nextInt(1000));
        }
        randomIterator_Remove(f,32);
        randomIterator_Remove(f,128);
        randomIterator_Remove(f,273);
        randomIterator_Remove(f,1027);
        randomIterator_Remove(f,10000);
	}
    
    private void randomIterator_Remove(Function<int[], Set<T>> f, int size) {
        // HashSets mają przekombinowaną procedurę usuwania pozycji
        // nie możemy napisać jakiegoś testu, który sprawdzałby jej poprawność
        // bez totalnie sztucznego podstawiania kluczy i hashcode'ów
        //
        // taka metoda byłaby bezużyteczna z praktycznego punktu widzenia
        // bo testowałaby tylko jedną konkretną implementację dla jednego konkretnego rozmiaru
        // usuwamy więc losowo z dużej mapy, licząc, że większość kodu ruszy do akcji
        Set<T> target = f.apply($var.range(size).indexes());
        Set<T> keys = new HashSet<>(target);
        Random rand = new Random(777);
        
        for(int i=0; i<8; i++) {
            Iterator<T> iterator = target.iterator();
            while(iterator.hasNext()) {
                T key = iterator.next();
                if( rand.nextBoolean() ) {
                    iterator.remove();
                    keys.remove(key);
                }
            }
            assertEquals(keys.size(), target.size());
            assertContentEquals(keys, target);
        }

	}
        
    public static <V> void assertHead(TCollection<V> generator, SortedSet<V> target, V[] content) {
        assertHead(true, generator, target, content);
    }
    
    private static <V> void assertHead(boolean nested, TCollection<V>  generator, SortedSet<V> target, V[] content) {
        int n = content.length;
        assertEquals(0, target.headSet(generator.item(0).value()).size());
        assertEquals(n, target.headSet(generator.item(1000).value()).size());
        assertRange(target, content);
        
        assertEquals(0, target.headSet(generator.item(0).value()).headSet(generator.item(1000).value()).size());

        for(int i=0; i<n; i++) {
            V point = content[i];
            V[] exp = ArrayUtils.head(content, point, generator.comparator());
            SortedSet<V> map = target.headSet(point);
            assertEquals(exp.length, map.size());
            if(nested) {
                assertHead(false, generator, map, exp);
                assertTail(false, generator, map, exp);
                checkSub(false, generator, map, exp);
            }
        }
    }
    
    @Ignore
    public static <V> void assertTail(TCollection<V> generator, SortedSet<V> target, V[] content) {
        assertTail(true, generator, target, content);
    }
    
    private static <V> void assertTail(boolean nested, TCollection<V> generator, SortedSet<V> target, V[] content) {
        int n = content.length;
        assertEquals(n, target.tailSet(generator.item(0).value()).size());
        assertEquals(0, target.tailSet(generator.item(1000).value()).size());
        assertRange(target, content);
        
        for(int i=0; i<n; i++) {
            V entry = content[i];
            V[] exp = ArrayUtils.tail(content, entry, generator.comparator());
            SortedSet<V> map = target.tailSet(entry);
            assertEquals(exp.length, map.size());
            if(nested) {
                assertHead(false, generator, map, exp);
                assertTail(false, generator, map, exp);
                checkSub(false, generator, map, exp);
            }
        }
    }
    
    @Ignore
    public static <V> void assertSub(TCollection<V> generator, SortedSet<V> target, V[] content) {
        checkSub(true, generator, target, content);
    }
    
    private static <V> void checkSub(boolean nested, TCollection<V> generator, SortedSet<V> target, V[] content) {
        assertSub(nested, generator, target, 3, 1000, content);
        assertSub(nested, generator, target, 5, 1000, content);
            
        assertSub(nested, generator, target, 3, 7, content);
        assertSub(nested, generator, target, 3, 6, content);
        assertSub(nested, generator, target, 4, 6, content);
        assertSub(nested, generator, target, 5, 6, content);

        assertSub(nested, generator, target, 6, 7, content);
        assertSub(nested, generator, target, 6, 6, content);
        
        assertSub(nested, generator, target, 0, 3, content);
        assertSub(nested, generator, target, 0, 5, content);
        assertSub(nested, generator, target, 0, 1000, content);
    }
    
    private static <V> void assertSub(boolean nested, TCollection<V> generator, SortedSet<V> target, int ibegin, int iend, V[] content) {
        V begin = generator.item(ibegin).value();
        V end = generator.item(iend).value();
        V[] exp = ArrayUtils.slice(content, begin, end, generator.comparator());
        SortedSet<V> map = target.subSet(begin, end);
        assertEquals(exp.length, map.size());
        assertRange(map, exp);
       
        if(nested) {
            assertHead(false, generator, map, exp);
            assertTail(false, generator, map, exp);
            checkSub(false, generator, map, exp);
        }
    }
    
    @Ignore
    public static <V> void assertRange(SortedSet<V> target, V[] content) {
        if(content.length > 0) {
            assertEquals(content[0], target.first());
            assertEquals(content[content.length-1], target.last());
        } else {
            assertThrows(NoSuchElementException.class, ()->target.first());
            assertThrows(NoSuchElementException.class, ()->target.last());
        }
    }
    
    private void assertRemove(Set<T> target, TItem<T> item) {
        int size = target.size();
        assertTrue(target.remove(item.value()));
        assertEquals(size-1, target.size());
    }
    
    private void assertNotRemove(Set<T> target, TItem<T> item) {
        int size = target.size();
        assertFalse(target.remove(item.value()));
        assertEquals(size, target.size());
    }
    
    private void assertContains(Set<T> target, TItem<T> item) {
		assertTrue(target.contains(item.value()));
    }
    
    private void assertNotContains(Set<T> target, TItem<T> item) {
        assertFalse(target.contains(item.value()));
    }
    
    private static <Q> void assertContentEquals(Collection<Q> a, Collection<Q> b) {
        assertTrue("assertContentEquals", a.containsAll(b) && b.containsAll(a));
    }
    
}
