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

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiPredicate;
import java.util.function.Predicate;


/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class NewAssert extends org.junit.Assert {
    
    private NewAssert() { }
    
    public static void assertThrows(Class<? extends Throwable> error, Action action) {
        try {
            action.run();
            fail(error + " expected");
        } catch(Throwable cause) { // NOPMD
            if( !error.isInstance(cause)) {
                throw TestUtils.rethrow(cause);
            }
        }
    }
    
    public static void assertThrows(Class<? extends Throwable> error, String message, Action action) {
        try {
            action.run();
            fail(error + " expected");
        } catch(Throwable cause) { // NOPMD
            if( !error.isInstance(cause)) {
                throw TestUtils.rethrow(cause);
            }
            if( !message.equals(cause.getMessage())) {
                throw TestUtils.rethrow(cause);
            }
        }
    }
    
    public static void assertThrows(Predicate<Throwable> handler, Action action) {
        try {
            action.run();
            fail("Exception expected");
        } catch(Throwable cause) { // NOPMD
            if( !handler.test(cause)) {
                throw TestUtils.rethrow(cause);
            }
        }
    }
    
    public static void assertSerializable(Object value) {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        try (ObjectOutputStream ostream = new ObjectOutputStream(buffer)) {
            ostream.writeObject(value);
            
            InputStream idata = new ByteArrayInputStream(buffer.toByteArray());
            try (ObjectInputStream istream = new ObjectInputStream(idata)) {
                assertEquals(value, istream.readObject());
            }
        } catch(IOException | ClassNotFoundException cause) {
            throw TestUtils.rethrow(cause);
        }
    }
    
    /**
     * Requirements (object : hash / pseudo-value)
     * <pre>
     *  A : 1 / 1
     *  B : 1 / 1
     *  C : 1 / 2
     *  D : 2 / 3
     * </pre>
     * @param a
     * @param b
     * @param c
     * @param d
     */
    @SuppressFBWarnings("SA_LOCAL_SELF_COMPARISON")
    public static void assertEquality(Object a, Object b, Object c, Object d) {
        int ha = a.hashCode();
        int hb = b.hashCode();
        int hc = c.hashCode();
        int hd = d.hashCode();
        
        assertTrue("hashcode: a==b", ha == hb);
        assertTrue("hashcode: a==c", ha == hc);
        assertTrue("hashcode: a!=d", ha != hd);
        
        assertTrue("a==a", a.equals(a));
        assertTrue("b==b", b.equals(b));
        assertTrue("c==c", c.equals(c));
        assertTrue("d==d", d.equals(d));
        
        assertSymEquals("a == b", a,b);
        
        assertSymNotEquals("a != c", a,c);
        assertSymNotEquals("a != d", a,d);
        assertSymNotEquals("b != c", b,c);
        assertSymNotEquals("b != d", b,d);
        assertSymNotEquals("c != d", c,d);
        
        assertFalse("a != object", a.equals(new Object()));
        assertFalse("b != object", b.equals(new Object()));
        assertFalse("c != object", c.equals(new Object()));
        assertFalse("d != object", d.equals(new Object()));
    }
    
    /**
     * Requirements (object : hash / pseudo-value)
     * <pre>
     *  A : 1 / 1
     *  B : 1 / 1
     *  D : 2 / 2
     * </pre>
     * @param a
     * @param b
     * @param d
     */
    @SuppressFBWarnings("SA_LOCAL_SELF_COMPARISON")
    public static void assertEquality(Object a, Object b, Object d) {
        int ha = a.hashCode();
        int hb = b.hashCode();
        int hd = d.hashCode();
        
        assertTrue("hashcode: a==b", ha == hb);
        assertTrue("hashcode: a!=d", ha != hd);
        
        assertTrue("a==a", a.equals(a));
        assertTrue("b==b", b.equals(b));
        assertTrue("d==d", d.equals(d));
        
        assertSymEquals("a == b", a,b);
        
        assertSymNotEquals("a != d", a,d);
        assertSymNotEquals("b != d", b,d);
        
        assertFalse("a != object", a.equals(new Object()));
        assertFalse("b != object", b.equals(new Object()));
        assertFalse("d != object", d.equals(new Object()));
    }
	
	public static void assertSymEquals(Object value1, Object value2) {
        assertEquals(Objects.hashCode(value1), Objects.hashCode(value2));
		assertEquals(value1, value2);
        assertEquals(value2, value1);
	}
    
    public static void assertSymEquals(String message, Object value1, Object value2) {
        assertEquals(message, Objects.hashCode(value1), Objects.hashCode(value2));
        assertEquals(message, value1, value2);
        assertEquals(message, value2, value1);
    }
    
    public static void assertSymNotEquals(Object value1, Object value2) {
        assertNotEquals(value1, value2);
        assertNotEquals(value2, value1);
    }
    
    public static void assertSymNotEquals(String message, Object value1, Object value2) {
        assertNotEquals(message, value1, value2);
        assertNotEquals(message, value2, value1);
    }
    
    public static <A,B> void assertMatch(Collection<A> expected, Collection<B> values, BiPredicate<A,B> predicate) {
        assertMatch("", expected, values, predicate);
    }
    
    public static <A,B> void assertMatch(String message, Collection<A> expected, Collection<B> values, BiPredicate<A,B> predicate) {
        assertEquals(message + ": different size", expected.size(), values.size());
        Iterator<A> ia = expected.iterator();
        Iterator<B> ib = values.iterator();
        for(int i=0, n=expected.size(); i<n; i++) {
            A a = ia.next();
            B b = ib.next();
            if(!predicate.test(a,b)) {
                fail("line[" + i+"] expected '" + a +"' but was '" + b + "'");
            }
		}
    }
    
    @SuppressWarnings("unchecked")    
    public static <E extends Enum<E>> void assertEnumInvariants(Class<E> enumtype) throws Exception {
        Method mValues = enumtype.getMethod("values");
        Method mValueOf = enumtype.getMethod("valueOf", String.class);
        
        E[] values = (E[])mValues.invoke(null);

        assertTrue( values.getClass().getComponentType().equals(enumtype) );
        assertTrue( values.length > 0 );
        assertTrue( enumtype.isInstance(values[0]) );
        
        assertEquals(values[0], mValueOf.invoke(null, values[0].name()));
        
        assertThrows(IllegalArgumentException.class, ()-> {
            try {
                mValueOf.invoke(null, "__NONAME_IN_ENUM__");
            } catch(InvocationTargetException cause) {
                throw TestUtils.rethrow(cause.getCause());
            }
        });
        
        assertNotSame(mValues.invoke(null), mValues.invoke(null));
        assertArrayEquals((E[])mValues.invoke(null), (E[])mValues.invoke(null));
    }
    
    public static <K,V> void assertEquivalent(Map<K,V> a, Map<K,V> b) {
        assertEquivalent(a.entrySet(), b.entrySet());
    }
    
    public static <T> void assertEquivalent(Collection<T> a, Collection<T> b) {
        String message = a + " not equivalent to " + b;
        assertTrue(message, a.size()==b.size());
        assertTrue(message, a.containsAll(b));
        assertTrue(message, b.containsAll(a));
	}
    
    public static void assertEquals(Instant a, Instant b, Duration dt) {
        assertTrue(a + " and " + b + " are not equal", Duration.between(a, b).abs().compareTo(dt.abs()) <= 0);
    }
    
    public static void assertEquals(Date a, Date b, Duration dt) {
        assertTrue(a + " and " + b + " are not equal", Math.abs(a.getTime() - b.getTime()) <= dt.toMillis());
    }
    
    public static void assertEquals(Date a, Date b, long dt) {
        assertTrue(a + " and " + b + " are not equal", Math.abs(a.getTime() - b.getTime()) <= dt);
    }
    
    public static void assertEmpty(Optional<?> value) {
        assertFalse("Optional should be empty", value.isPresent());
    }
    
    public static void assertEmpty(Collection<?> value) {
        assertTrue("Collection should be empty", value.isEmpty());
    }
    
    public static void assertEmpty(Map<?,?> value) {
        assertTrue("Map should be empty", value.isEmpty());
    }

    public interface Action {
        
        void run() throws Throwable;
        
    }

}
