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

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.ranides.assira.collection.SetUtils;
import net.ranides.assira.events.Event;
import net.ranides.assira.events.EventListener;
import net.ranides.assira.text.TextEncoding;
import net.ranides.assira.text.TextInputStream;
import net.ranides.assira.text.TextOutputStream;
import net.ranides.assira.time.TimeUtils;
import org.junit.Test;
import static org.junit.Assert.*;

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

    @Test
    public void testWriter() throws IOException {
        OutputStream ostream = CheckedStream.wrap(new TextOutputStream());
        AsyncStream.OStream astream = AsyncStream.wrap(ostream);
        EventCollector<IOEvent> events1 = new EventCollector<>();
        EventCollector<IORequest> events2 = new EventCollector<>();
        
        astream.addEventListener(IOEvent.class, events1);
        astream.addEventListener(IORequest.class, events2);
        
        astream.write('H');
        astream.write("ello ".getBytes(TextEncoding.NIO_UTF8));
        astream.write("...world!...".getBytes(TextEncoding.NIO_UTF8), 3, 6);
        astream.write("???".getBytes(TextEncoding.NIO_UTF8), 100, 100);
        astream.flush();
        astream.close();
        
        // @todo (prev # 8) issue: exception will be lost
        // CStream won't write anything but exception will be lost
        astream.write('?');
        
        TimeUtils.sleep(250);
        assertEquals("Hello world!",ostream.toString());
        
        // @todo (prev # 8) issue: exception will be lost
        astream.close();
        
        TimeUtils.sleep(250);
        assertEquals("Hello world!",ostream.toString());
        
        assertTrue(events1.eq(SetUtils.asHashSet(
            "IOEvent.WriteByte: 72",
            "IOEvent.WriteByteArray: 5",
            "IOEvent.WriteByteArray: 6",
            "IOEvent.Failure: java.io.IOException: java.lang.ArrayIndexOutOfBoundsException",
            "IOEvent.Flush",
            "IOEvent.Close"
        )));
        assertTrue(events2.eq(Arrays.asList(
            "IORequest.WriteByte: 72",
            "IORequest.WriteByteArray: 5",
            "IORequest.WriteByteArray: 6",
            "IORequest.Flush",
            "IORequest.Close",
            "IORequest.WriteByte: 63"
        )));
    }
    
    @Test
    public void testWriterDispose() throws IOException {
        
        OutputStream ostream = CheckedStream.wrap(new TextOutputStream());
        AsyncStream.OStream astream = AsyncStream.wrap(ostream);
        EventCollector<IOEvent> events1 = new EventCollector<>();
        EventCollector<IORequest> events2 = new EventCollector<>();
        
        astream.addEventListener(IOEvent.class, events1);
        astream.addEventListener(IORequest.class, events2);
        
        astream.dispose();
        
        // @todo (prev # 8) issue: exception will be lost
        TimeUtils.sleep(250);
        astream.dispose();
        
        TimeUtils.sleep(250);

        assertTrue(events1.eq(SetUtils.asHashSet(
            "IOEvent.Close"
        )));
        assertTrue(events2.eq(Collections.<String>emptyList()));
        
    }
    
    @Test
    public void testWriterRouter() throws IOException {
        
        OutputStream ostream = CheckedStream.wrap(new TextOutputStream());
        AsyncStream.OStream astream = AsyncStream.wrap(ostream);
        EventListener<Event> e1 = new EventCollector<>();
        EventListener<Event> e2 = new EventCollector<>();
        
        int prev = astream.getEventListenersCount();
        
        astream.addEventListener(IOEvent.class, e1);
        astream.addEventListener(IORequest.class, e2);
        assertEquals(prev+2, astream.getEventListenersCount());
        
        astream.removeEventListener(IOEvent.class, e2);
        astream.removeEventListener(IORequest.class, e1);
        assertEquals(prev+2, astream.getEventListenersCount());
        
        astream.removeEventListener(IOEvent.class, e1);
        assertEquals(prev+1, astream.getEventListenersCount());
        astream.removeEventListener(IORequest.class, e2);
        assertEquals(prev+0, astream.getEventListenersCount());
        
        // @todo (prev # 8) issue: AsyncStream will be broken
        // "hidden" listener is required to implement functionality
        astream.removeAllEventListeners();
        assertEquals(0, astream.getEventListenersCount());
        
        astream.close();
    }
    
    @Test
    public void testReader() throws IOException {
        InputStream istream = CheckedStream.wrap(new TextInputStream("Hello world"));
        AsyncStream.IStream astream = AsyncStream.wrap(istream);
        EventCollector<IOEvent> events1 = new EventCollector<>();
        
        astream.addEventListener(IOEvent.class, events1);

        assertEquals(11, astream.available());
        assertTrue(astream.markSupported());
        astream.mark(8);
        assertEquals('H', astream.read());
        astream.reset();
        
        byte[] target= new byte[6];
        assertEquals(6, astream.read(target));
        assertArrayEquals("Hello ".getBytes(), target);
        
        assertEquals(2, astream.skip(2));
        assertEquals(2, astream.read(target, 1, 2));
        assertArrayEquals("Hrllo ".getBytes(), target);
        
        astream.close();
        // @todo (prev # 8) issue: exceptions will be lost
        try {
            astream.close();
            fail("IOException expected");
        } catch(IOException ex) { assertTrue(true); }
        
        try {
            astream.read();
            fail("IOException expected");
        } catch(IOException ex) { assertTrue(true); }
        
        try {
            astream.read(target);
            fail("IOException expected");
        } catch(IOException ex) { assertTrue(true); }
        
        try {
            astream.read(target,1,2);
            fail("IOException expected");
        } catch(IOException ex) { assertTrue(true); }
        
        try {
            astream.reset();
            fail("IOException expected");
        } catch(IOException ex) { assertTrue(true); }
        
        try {
            astream.skip(5);
            fail("IOException expected");
        } catch(IOException ex) { assertTrue(true); }
        
		TimeUtils.sleep(250);
        assertTrue(events1.eq(Arrays.asList(
            "IOEvent.Mark", 
            "IOEvent.ReadByte: 72", 
            "IOEvent.Reset", 
            "IOEvent.ReadByteArray: 6:48 65 6C 6C 6F 20", 
            "IOEvent.Skip", 
            "IOEvent.ReadByteArray: 2:72 6C", 
            "IOEvent.Close"
        )));
    }
    
    @Test
    public void testReaderDispose() throws IOException {
        InputStream istream = CheckedStream.wrap(new TextInputStream("Hello world"));
        AsyncStream.IStream astream = AsyncStream.wrap(istream);
        EventCollector<IOEvent> events1 = new EventCollector<>();
        
        astream.addEventListener(IOEvent.class, events1);
        
        astream.dispose();
        astream.dispose();
        TimeUtils.sleep(250);

        assertTrue(events1.eq(SetUtils.asHashSet(
            "IOEvent.Close"
        )));
    }
    
    @Test
    public void testReaderRouter() throws IOException {
        InputStream istream = CheckedStream.wrap(new TextInputStream("Hello world"));
        AsyncStream.IStream astream = AsyncStream.wrap(istream);
        EventCollector<IOEvent> e1 = new EventCollector<>();
        EventCollector<IOEvent> e2 = new EventCollector<>();
        
        astream.addEventListener(IOEvent.Close.class, e1);
        astream.addEventListener(IOEvent.Read.class, e2);
        assertEquals(2, astream.getEventListenersCount());
        
        astream.removeEventListener(IOEvent.Read.class, e1);
        astream.removeEventListener(IOEvent.Close.class, e2);
        assertEquals(2, astream.getEventListenersCount());
        
        astream.removeEventListener(IOEvent.Close.class, e1);
        assertEquals(1, astream.getEventListenersCount());
        astream.removeEventListener(IOEvent.Read.class, e2);
        assertEquals(0, astream.getEventListenersCount());
        
        astream.removeAllEventListeners();
        assertEquals(0, astream.getEventListenersCount());
        
        astream.close();
    }
    
    private static final class EventCollector<E extends Event> implements EventListener<E> {
        
        private final ConcurrentLinkedQueue<String> events = new ConcurrentLinkedQueue<>();

        @Override
        public void handleEvent(E event) {
            events.add(event.toString());
        }
        
        public boolean eq(Set<String> collection) {
            return collection.equals(new HashSet<>(events));
        }
        
        public boolean eq(List<String> collection) {
            return collection.equals(new ArrayList<>(events));
        }
        
    }
    
    
}
