/*
 * Rsync channel integration tests
 *
 * Copyright (C) 2016 Per Lundqvist
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.github.perlundq.yajsync.channels;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.LinkedList;
import java.util.Queue;

import org.junit.Before;
import org.junit.Test;

import com.github.perlundq.yajsync.internal.channels.ChannelEOFException;
import com.github.perlundq.yajsync.internal.channels.ChannelException;
import com.github.perlundq.yajsync.internal.channels.Message;
import com.github.perlundq.yajsync.internal.channels.MessageCode;
import com.github.perlundq.yajsync.internal.channels.MessageHandler;
import com.github.perlundq.yajsync.internal.channels.RsyncInChannel;
import com.github.perlundq.yajsync.internal.channels.RsyncOutChannel;
import com.github.perlundq.yajsync.internal.util.Environment;

class ReadableByteBufferChannel implements ReadableByteChannel
{
    private final ByteBuffer _buf;
    private boolean _isOpen = true;

    ReadableByteBufferChannel(ByteBuffer buf)
    {
        _buf = buf;
    }

    @Override
    public boolean isOpen()
    {
        return _isOpen;
    }

    @Override
    public void close()
    {
        _isOpen = false;
    }

    @Override
    public int read(ByteBuffer dst)
    {
        int n = 0;
        while (dst.hasRemaining()) {
            if (_buf.hasRemaining()) {
                byte b = _buf.get();
                dst.put(b);
                n++;
            } else if (n == 0) {
                return -1;
            } else {
                return n;
            }
        }
        return n;
    }
}

class WritableByteBufferChannel implements WritableByteChannel
{
    private final ByteBuffer _buf;
    private boolean _isOpen = true;

    WritableByteBufferChannel(ByteBuffer buf)
    {
        _buf = buf;
    }

    @Override
    public boolean isOpen()
    {
        return _isOpen;
    }

    @Override
    public void close()
    {
        _isOpen = false;
    }

    @Override
    public int write(ByteBuffer src)
    {
        int n = 0;
        while (src.hasRemaining()) {
            if (_buf.hasRemaining()) {
                byte b = src.get();
                _buf.put(b);
                n++;
            } else if (n == 0) {
                return -1;
            } else {
                return n;
            }
        }
        return n;
    }
}

public class ChannelTest implements MessageHandler
{
    private final Queue<Message> _messages = new LinkedList<>();

    @Override
    public void handleMessage(Message message)
    {
        _messages.add(message);
    }

    @Before
    public void initializeTest()
    {
        _messages.clear();
    }

    @Test(expected=ChannelEOFException.class)
    public void testEOFRead() throws ChannelException
    {
        ByteBuffer b = ByteBuffer.allocate(0);
        ReadableByteChannel r = new ReadableByteBufferChannel(b);
        RsyncInChannel _in = new RsyncInChannel(r, this);
        _in.getByte();
    }

    @Test
    public void testTransferSingleMinByte() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(8);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);

        byte testByte = Byte.MIN_VALUE;
        _out.putByte(testByte);
        _out.flush();
        assertTrue(wb.position() > 0);

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        byte resultByte = _in.getByte();
        assertTrue(wb.position() > 0);
        assertTrue(resultByte == testByte);
    }

    @Test
    public void testTransferSingleMaxByte() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(8);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);

        byte testByte = Byte.MAX_VALUE;
        _out.putByte(testByte);
        _out.flush();
        assertTrue(wb.position() > 0);

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        byte resultByte = _in.getByte();
        assertTrue(wb.position() > 0);
        assertTrue(resultByte == testByte);
    }

    @Test
    public void testTransferSingleMinInt() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(8);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);

        int testInt = Integer.MIN_VALUE;
        _out.putInt(testInt);
        _out.flush();
        assertTrue(wb.position() > 0);

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        int resultInt = _in.getInt();
        assertTrue(wb.position() > 0);
        assertTrue(resultInt == testInt);
    }

    @Test
    public void testTransferSingleMaxInt() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(8);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);

        int testInt = Integer.MAX_VALUE;
        _out.putInt(testInt);
        _out.flush();
        assertTrue(wb.position() > 0);

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        int resultInt = _in.getInt();
        assertTrue(wb.position() > 0);
        assertTrue(resultInt == testInt);
    }


    @Test
    public void testTransferString() throws ChannelException
    {
        Environment.setAllocateDirect(false);

        ByteBuffer wb = ByteBuffer.allocate(32);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);

        String testString = "abcdefghijklm รถ\\";
        _out.put(ByteBuffer.wrap(testString.getBytes()));
        _out.flush();
        assertTrue(wb.position() > 0);

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        ByteBuffer res = _in.get(testString.getBytes().length);
        assertTrue(wb.position() > 0);
        assertTrue(res.position() == 0);

        String resultString = new String(res.array(), 0, res.remaining());
        assertEquals(testString, resultString);
    }

    private static Message toMessage(MessageCode code, String text)
    {
        ByteBuffer payload = ByteBuffer.wrap(text.getBytes());
        Message msg = new Message(code, payload);
        return msg;
    }

    @Test
    public void testSingleMessageNoData() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(128);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);
        Message testMessage = toMessage(MessageCode.INFO, "test message");
        testMessage.payload().mark();
        _out.putMessage(testMessage);
        testMessage.payload().reset();
        _out.flush();

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        try {
            _in.getByte();
            fail();
        } catch (ChannelEOFException e) {
            // expected
        }
        Message resultMessage = _messages.poll();
        assertEquals(testMessage, resultMessage);
        assertTrue(_messages.isEmpty());
    }

    @Test
    public void testSingleMessageAndSingleData() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(128);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);
        Message testMessage = toMessage(MessageCode.INFO, "test message");
        testMessage.payload().mark();
        int testInt = 4;
        _out.putInt(testInt);
        _out.putMessage(testMessage);
        testMessage.payload().reset();
        _out.flush();

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        int resultInt = _in.getInt();
        assertTrue(resultInt == testInt);
        try {
            _in.getByte();
            fail();
        } catch (ChannelEOFException e) {
            // expected
        }
        Message resultMessage = _messages.poll();
        assertEquals(testMessage, resultMessage);
        assertTrue(_messages.isEmpty());
    }

    @Test
    public void testManyMessagesNoData() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(1024);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);
        Message[] msgs = { toMessage(MessageCode.INFO, "INFO message"),
                           toMessage(MessageCode.WARNING, "WARNING message"),
                           toMessage(MessageCode.ERROR, "ERROR message"),
                           toMessage(MessageCode.ERROR_XFER, "XFER message"),
                           new Message(
                                   MessageCode.NO_SEND,
                                   ByteBuffer.allocate(4).
                                              order(ByteOrder.LITTLE_ENDIAN).
                                              putInt(0, 127)),
                           new Message(
                                   MessageCode.IO_ERROR,
                                   ByteBuffer.allocate(4)
                                             .order(ByteOrder.LITTLE_ENDIAN).
                                             putInt(0, 31)),
        };
        for (Message msg : msgs) {
            msg.payload().mark();
            _out.putMessage(msg);
            msg.payload().reset();
        }
        _out.flush();

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        try {
            _in.getByte();
            fail();
        } catch (ChannelEOFException e) {
            // expected
        }

        for (Message msg : msgs) {
            Message resultMessage = _messages.poll();
            assertEquals(msg, resultMessage);
        }
        assertTrue(_messages.isEmpty());
    }

    @Test
    public void testManyMessagesManyData() throws ChannelException
    {
        ByteBuffer wb = ByteBuffer.allocate(1024);
        WritableByteBufferChannel w = new WritableByteBufferChannel(wb);
        RsyncOutChannel _out = new RsyncOutChannel(w);
        Message[] msgs = { toMessage(MessageCode.INFO, "INFO message"),
                           toMessage(MessageCode.WARNING, "WARNING message"),
                           toMessage(MessageCode.ERROR, "ERROR message"),
                           toMessage(MessageCode.ERROR_XFER, "XFER message"),
                           new Message(
                                   MessageCode.NO_SEND,
                                   ByteBuffer.allocate(4).
                                              order(ByteOrder.LITTLE_ENDIAN).
                                              putInt(0, 127)),
                           new Message(
                                   MessageCode.IO_ERROR,
                                   ByteBuffer.allocate(4)
                                             .order(ByteOrder.LITTLE_ENDIAN).
                                             putInt(0, 31)),
        };

        byte b = 0;
        for (Message msg : msgs) {
            msg.payload().mark();
            _out.putMessage(msg);
            _out.putByte(b++);
            msg.payload().reset();
        }
        _out.flush();

        wb.flip();
        ReadableByteChannel r = new ReadableByteBufferChannel(wb);
        RsyncInChannel _in = new RsyncInChannel(r, this);

        b = 0;
        for (Message msg : msgs) {
            byte result_b  = _in.getByte();
            assertTrue(result_b == b++);
            Message resultMessage = _messages.poll();
            assertEquals(msg, resultMessage);
        }
        assertTrue(_messages.isEmpty());
    }
}