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

import net.ranides.assira.junit.NewAssert;
import net.ranides.assira.trace.ExceptionUtils;
import org.junit.Test;
import static org.junit.Assert.*;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public class EventLockTest {

    private static void sleep(long milis) {
        try {
            Thread.sleep(milis);
        } catch(InterruptedException ex) {
            throw ExceptionUtils.rethrow(ex);
        }
    }
    
    /**
     * MUSIMY tu mieć duże opóźnienia, jeśli jednocześnie chcemy sprawdzić
     * naprawdę EventLock oraz uniknąć race-condition. Moglibyśmy wprowadzić
     * jakąś synchronizację... ale wtedy z definicji byśmy testowali tę 
     * smart-synchronizację, a nie nasz EventLock.
     */
    @Test
    public void testSingleLock() throws InterruptedException {
        final EventReactor main = EventReactor.newInstance("name", 32, 1000);

        new Thread(() -> {
            // sleep unikają race condition (event wysłany przed rozpoczęciem waitForEvent)
            sleep(100);
            main.signalEvent(new WriteEvent(11));
            sleep(100);
            main.signalEvent(new ReadEvent(12));
            sleep(100);
            main.signalEvent(new ReadEvent(13));
            sleep(100);
            main.signalEvent(new ReadEvent(14));
            sleep(100);
            main.signalEvent(new Event(){});
            sleep(100);
            main.signalEvent(new ReadEvent(16));
            sleep(100);
            main.signalEvent(new WriteEvent(17));
            sleep(100);
            
            main.signalEvent(new WriteEvent(19));
            sleep(400);
            main.signalEvent(new WriteEvent(20));
            sleep(100);
            
            main.signalEvent(new ReadEvent(21));
            sleep(400);
            main.signalEvent(new ReadEvent(22));
            sleep(100);
            
            main.stop();
        }).start();
        
        
        IOEvent event;
        
        // taka konstrukcja normalnie jest podatna na race condition
        event = EventLock.singleLock(WriteEvent.class, main).waitForEvent();
        assertEquals(11, event.handle());
        
        event = EventLock.singleLock(ReadEvent.class, main).waitForEvent(ReadEvent.class);
        assertEquals(12, event.handle());
        
        event = EventLock.singleLock(WriteEvent.class, main).waitForEvent(WriteEvent.class);
        assertEquals(17, event.handle());
        
        event = EventLock.singleLock(IOEvent.class, main).waitForEvent(300);
        assertEquals(19, event.handle());
        
        event = EventLock.singleLock(IOEvent.class, main).waitForEvent(200);
        assertNull(event);
        
        event = EventLock.singleLock(IOEvent.class, main).waitForEvent(ReadEvent.class, 600);
        assertEquals(21, event.handle());
        
        event = EventLock.singleLock(IOEvent.class, main).waitForEvent(ReadEvent.class, 200);
        assertNull(event);

        assertEquals(0, main.getEventListenersCount());
    }
    
    /**
     * MUSIMY tu mieć duże opóźnienia, jeśli jednocześnie chcemy sprawdzić
     * naprawdę EventLock oraz uniknąć race-condition. Moglibyśmy wprowadzić
     * jakąś synchronizację... ale wtedy z definicji byśmy testowali tę 
     * smart-synchronizację, a nie nasz EventLock.
     */
    @Test
    public void testUnsafeLock() throws InterruptedException {
        final EventReactor main = EventReactor.newInstance("name", 32, 1000);
        
        final EventLock<Event> lock = EventLock.unsafeLock(Event.class, main);

        new Thread(() -> {
            // sleep unikają race condition (event wysłany przed rozpoczęciem waitForEvent)
            sleep(100);
            main.signalEvent(new WriteEvent(11));
            sleep(100);
            main.signalEvent(new ReadEvent(12));
            sleep(100);
            main.signalEvent(new ReadEvent(13));
            sleep(100);
            main.signalEvent(new ReadEvent(14));
            sleep(100);
            main.signalEvent(new Event(){});
            sleep(100);
            main.signalEvent(new ReadEvent(16));
            sleep(100);
            main.signalEvent(new WriteEvent(17));
            sleep(100);
            
            main.signalEvent(new WriteEvent(19));
            sleep(400);
            main.signalEvent(new WriteEvent(20));
            sleep(100);
            
            main.signalEvent(new ReadEvent(21));
            sleep(400);
            main.signalEvent(new ReadEvent(22));
            sleep(100);
            
            main.stop();
        }).start();
        
        
        IOEvent event;
        
        // taka konstrukcja normalnie jest podatna na race condition
        event = (IOEvent)lock.waitForEvent();
        assertEquals(11, event.handle());
        
        event = lock.waitForEvent(ReadEvent.class);
        assertEquals(12, event.handle());
        
        event = lock.waitForEvent(WriteEvent.class);
        assertEquals(17, event.handle());
        
        event = (IOEvent)lock.waitForEvent(300);
        assertEquals(19, event.handle());
        
        event = (IOEvent)lock.waitForEvent(200);
        assertNull(event);
        
        event = lock.waitForEvent(ReadEvent.class, 600);
        assertEquals(21, event.handle());
        
        event = lock.waitForEvent(ReadEvent.class, 200);
        assertNull(event);

        assertEquals(1, main.getEventListenersCount());
        lock.waitForEvent(Events.Shutdown.class);
        sleep(100);
        assertEquals(0, main.getEventListenersCount());
        
        NewAssert.assertThrows(UnsupportedOperationException.class, ()->{
            lock.counter();
        });
        NewAssert.assertThrows(UnsupportedOperationException.class, ()->{
            lock.discarded();
        });
        NewAssert.assertThrows(UnsupportedOperationException.class, ()->{
            lock.pending();
        });
    }
    
    @Test
    public void testEventLock() throws InterruptedException {
        final EventReactor main = EventReactor.newInstance("name", 32, 1000);

        final EventLock<Event> lock = EventLock.lock(Event.class, main);
        
        new Thread(()->{
            // w tym teście race-condition nie ma prawa się wydarzyć :) Lock niczego nie przeoczy :)
            main.signalEvent(new WriteEvent(11));
            main.signalEvent(new ReadEvent(12));
            main.signalEvent(new ReadEvent(13));
            main.signalEvent(new ReadEvent(14));
            main.signalEvent(new Event(){});
            main.signalEvent(new ReadEvent(16));
            main.signalEvent(new WriteEvent(17));
            
            main.signalEvent(new WriteEvent(19));
            sleep(500);
            main.signalEvent(new WriteEvent(20));
            
            main.signalEvent(new ReadEvent(21));
            sleep(500);
            main.signalEvent(new ReadEvent(22));
            
            main.stop();
        }).start();
        
        
        IOEvent event;
        
        event = (IOEvent)lock.waitForEvent();
        assertEquals(11, event.handle());
        
        event = lock.waitForEvent(ReadEvent.class);
        assertEquals(12, event.handle());
        
        event = lock.waitForEvent(ReadEvent.class);
        assertEquals(13, event.handle());
        
        event = lock.waitForEvent(WriteEvent.class);
        assertEquals(17, event.handle());
        
        event = (IOEvent)lock.waitForEvent(500);
        assertEquals(19, event.handle());
        
        event = (IOEvent)lock.waitForEvent(200);
        assertNull(event);
        
        event = lock.waitForEvent(ReadEvent.class, 500);
        assertEquals(21, event.handle());
        
        event = lock.waitForEvent(ReadEvent.class, 100);
        assertNull(event);

        assertEquals(1, main.getEventListenersCount());
        lock.waitForEvent(Events.Shutdown.class);
        sleep(100);
        assertEquals(0, main.getEventListenersCount());
    }
    
    @Test
    public void testEventLockDiscard() throws InterruptedException {
        final EventReactor main = EventReactor.newInstance("name", 32, 1000);

        final EventLock<Event> lock = EventLock.lock(Event.class, main, 4);
        
        new Thread(()->{
            main.signalEvent(new ReadEvent(101));
            main.signalEvent(new ReadEvent(102));
            main.signalEvent(new ReadEvent(103));
            main.signalEvent(new ReadEvent(104));
            main.signalEvent(new ReadEvent(105));
            main.signalEvent(new ReadEvent(106));
            main.signalEvent(new WriteEvent(201));
            main.signalEvent(new WriteEvent(202));
            main.signalEvent(new WriteEvent(203));
            main.signalEvent(new WriteEvent(204));
            
            sleep(1000);
            main.signalEvent(new ReadEvent(107));
            main.signalEvent(new ReadEvent(108));
            main.signalEvent(new ReadEvent(109));
            main.signalEvent(new ReadEvent(110));
             main.stop();
        }).start();
        
        sleep(750);

        assertEquals(201, lock.waitForEvent(IOEvent.class).handle());
        assertEquals(202, lock.waitForEvent(IOEvent.class).handle());
        
        assertEquals(6, lock.discarded());
        assertEquals(10, lock.counter());
        assertEquals(2, lock.pending());
        
        assertEquals(203, lock.waitForEvent(IOEvent.class).handle());
        assertEquals(204, lock.waitForEvent(IOEvent.class).handle());
        
        assertEquals(6, lock.discarded());
        assertEquals(10, lock.counter());
        assertEquals(0, lock.pending());
        
        assertEquals(1, main.getEventListenersCount());
        lock.waitForEvent(Events.Shutdown.class);
        sleep(100);
        assertEquals(0, main.getEventListenersCount());

        assertTrue(lock.discarded() >=6 );
        assertTrue(lock.counter() >= 17);
        assertTrue(lock.pending() >= 1);
        
        lock.reset();
        assertEquals(0, lock.pending());
        
        lock.release();
    }
    
    @Test
    public void testRelease() throws InterruptedException {
        EventDispatcher main = new EventDispatcher();
        
        EventLock<Event> lock1 = EventLock.lock(Event.class, main);
        EventLock<Event> lock2 = EventLock.lock(Event.class, main, 1000);
        EventLock<Event> lock3 = EventLock.unsafeLock(Event.class, main);
        
        assertEquals(3, main.getEventListenersCount());
        
        lock1.release();
        assertEquals(2, main.getEventListenersCount());
        lock2.release();
        assertEquals(1, main.getEventListenersCount());
        lock3.release();
        assertEquals(0, main.getEventListenersCount());
    }
    
    class IOEvent implements Event { 
        private final int handle;

        public IOEvent(int handle) {
            this.handle = handle;
        }

        public int handle() {
            return handle;
        }
        
    }

    @SuppressWarnings("PMD")
    class ReadEvent extends IOEvent {

        public ReadEvent(int handle) {
            super(handle);
        }
    }

    @SuppressWarnings("PMD")
    class WriteEvent extends IOEvent {

        public WriteEvent(int handle) {
            super(handle);
        }
    }
    
}
