//
//  ========================================================================
//  Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.io;

import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.TimerScheduler;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

public class SelectChannelEndPointTest
{
    private static final Logger LOG = Log.getLogger(SelectChannelEndPointTest.class);
    protected CountDownLatch _lastEndPointLatch;
    protected volatile EndPoint _lastEndPoint;
    protected ServerSocketChannel _connector;
    protected QueuedThreadPool _threadPool = new QueuedThreadPool();
    protected Scheduler _scheduler = new TimerScheduler();
    protected SelectorManager _manager = new SelectorManager(_threadPool, _scheduler)
    {
        @Override
        public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment)
        {
            return SelectChannelEndPointTest.this.newConnection(channel, endpoint);
        }

        @Override
        protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) throws IOException
        {
            SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler());
            endp.setIdleTimeout(60000);
            _lastEndPoint = endp;
            _lastEndPointLatch.countDown();
            return endp;
        }
        
    };

    // Must be volatile or the test may fail spuriously
    protected volatile int _blockAt = 0;
    private volatile int _writeCount = 1;

    @Before
    public void startManager() throws Exception
    {
        _writeCount = 1;
        _lastEndPoint = null;
        _lastEndPointLatch = new CountDownLatch(1);
        _connector = ServerSocketChannel.open();
        _connector.socket().bind(null);
        _scheduler.start();
        _threadPool.start();
        _manager.start();
    }

    @After
    public void stopManager() throws Exception
    {
        _scheduler.stop();
        _manager.stop();
        _threadPool.stop();
        _connector.close();
    }

    protected Socket newClient() throws IOException
    {
        return new Socket(_connector.socket().getInetAddress(), _connector.socket().getLocalPort());
    }

    protected Connection newConnection(SelectableChannel channel, EndPoint endpoint)
    {
        return new TestConnection(endpoint);
    }

    public class TestConnection extends AbstractConnection
    {
        volatile FutureCallback _blockingRead;
        ByteBuffer _in = BufferUtil.allocate(32 * 1024);
        ByteBuffer _out = BufferUtil.allocate(32 * 1024);
        long _last = -1;
        final CountDownLatch _latch;

        public TestConnection(EndPoint endp)
        {
            super(endp, _threadPool);
            _latch=null;
        }
        
        public TestConnection(EndPoint endp,CountDownLatch latch)
        {
            super(endp, _threadPool);
            _latch=latch;
        }

        @Override
        public void onOpen()
        {
            super.onOpen();
            fillInterested();
        }

        @Override
        public void onFillInterestedFailed(Throwable cause)
        {
            Callback blocking = _blockingRead;
            if (blocking!=null)
            {
                _blockingRead=null;
                blocking.failed(cause);
                return;
            }
            super.onFillInterestedFailed(cause);
        }
        
        @Override
        public void onFillable()
        {
            if (_latch!=null)
            {
                try
                {
                    _latch.await();
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
            
            Callback blocking = _blockingRead;
            if (blocking!=null)
            {
                _blockingRead=null;
                blocking.succeeded();
                return;
            }
            
            EndPoint _endp = getEndPoint();
            try
            {
                _last = System.currentTimeMillis();
                boolean progress = true;
                while (progress)
                {
                    progress = false;

                    // Fill the input buffer with everything available
                    BufferUtil.compact(_in);
                    if (BufferUtil.isFull(_in))
                        throw new IllegalStateException("FULL " + BufferUtil.toDetailString(_in));
                    int filled = _endp.fill(_in);
                    if (filled > 0)
                        progress = true;

                    // If the tests wants to block, then block
                    while (_blockAt > 0 && _endp.isOpen() && _in.remaining() < _blockAt)
                    {
                        FutureCallback future = _blockingRead = new FutureCallback();
                        fillInterested();
                        future.get();
                        filled = _endp.fill(_in);
                        progress |= filled > 0;
                    }

                    // Copy to the out buffer
                    if (BufferUtil.hasContent(_in) && BufferUtil.append(_out, _in) > 0)
                        progress = true;

                    // Blocking writes
                    if (BufferUtil.hasContent(_out))
                    {
                        ByteBuffer out = _out.duplicate();
                        BufferUtil.clear(_out);
                        for (int i = 0; i < _writeCount; i++)
                        {
                            FutureCallback blockingWrite = new FutureCallback();
                            _endp.write(blockingWrite, out.asReadOnlyBuffer());
                            blockingWrite.get();
                        }
                        progress = true;
                    }

                    // are we done?
                    if (_endp.isInputShutdown())
                        _endp.shutdownOutput();
                }

                if (_endp.isOpen())
                    fillInterested();
            }
            catch (ExecutionException e)
            {
                // Timeout does not close, so echo exception then shutdown
                try
                {
                    FutureCallback blockingWrite = new FutureCallback();
                    _endp.write(blockingWrite, BufferUtil.toBuffer("EE: " + BufferUtil.toString(_in)));
                    blockingWrite.get();
                    _endp.shutdownOutput();
                }
                catch (Exception e2)
                {
                    // e2.printStackTrace();
                }
            }
            catch (InterruptedException | EofException e)
            {
                Log.getRootLogger().ignore(e);
            }
            catch (Exception e)
            {
                Log.getRootLogger().warn(e);
            }
            finally
            {
            }
        }
    }

    @Test
    public void testEcho() throws Exception
    {
        Socket client = newClient();

        client.setSoTimeout(60000);

        SocketChannel server = _connector.accept();
        server.configureBlocking(false);

        _manager.accept(server);

        // Write client to server
        client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));

        // Verify echo server to client
        for (char c : "HelloWorld".toCharArray())
        {
            int b = client.getInputStream().read();
            assertTrue(b > 0);
            assertEquals(c, (char)b);
        }

        // wait for read timeout
        client.setSoTimeout(500);
        long start = System.currentTimeMillis();
        try
        {
            client.getInputStream().read();
            Assert.fail();
        }
        catch (SocketTimeoutException e)
        {
            long duration = System.currentTimeMillis() - start;
            Assert.assertThat("timeout duration", duration, greaterThanOrEqualTo(400L));
        }

        // write then shutdown
        client.getOutputStream().write("Goodbye Cruel TLS".getBytes(StandardCharsets.UTF_8));

        // Verify echo server to client
        for (char c : "Goodbye Cruel TLS".toCharArray())
        {
            int b = client.getInputStream().read();
            Assert.assertThat("expect valid char integer", b, greaterThan(0));
            assertEquals("expect characters to be same", c, (char)b);
        }
        client.close();

        for (int i = 0; i < 10; ++i)
        {
            if (server.isOpen())
                Thread.sleep(10);
            else
                break;
        }
        assertFalse(server.isOpen());
    }

    @Test
    public void testShutdown() throws Exception
    {
        Socket client = newClient();

        client.setSoTimeout(500);

        SocketChannel server = _connector.accept();
        server.configureBlocking(false);

        _manager.accept(server);

        // Write client to server
        client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));

        // Verify echo server to client
        for (char c : "HelloWorld".toCharArray())
        {
            int b = client.getInputStream().read();
            assertTrue(b > 0);
            assertEquals(c, (char)b);
        }

        // wait for read timeout
        long start = System.currentTimeMillis();
        try
        {
            client.getInputStream().read();
            Assert.fail();
        }
        catch (SocketTimeoutException e)
        {
            assertTrue(System.currentTimeMillis() - start >= 400);
        }

        // write then shutdown
        client.getOutputStream().write("Goodbye Cruel TLS".getBytes(StandardCharsets.UTF_8));
        client.shutdownOutput();

        // Verify echo server to client
        for (char c : "Goodbye Cruel TLS".toCharArray())
        {
            int b = client.getInputStream().read();
            assertTrue(b > 0);
            assertEquals(c, (char)b);
        }

        // Read close
        assertEquals(-1, client.getInputStream().read());
    }

    @Test
    public void testReadBlocked() throws Exception
    {
        Socket client = newClient();

        SocketChannel server = _connector.accept();
        server.configureBlocking(false);

        _manager.accept(server);

        OutputStream clientOutputStream = client.getOutputStream();
        InputStream clientInputStream = client.getInputStream();

        int specifiedTimeout = 1000;
        client.setSoTimeout(specifiedTimeout);

        // Write 8 and cause block waiting for 10
        _blockAt = 10;
        clientOutputStream.write("12345678".getBytes(StandardCharsets.UTF_8));
        clientOutputStream.flush();

        Assert.assertTrue(_lastEndPointLatch.await(1, TimeUnit.SECONDS));
        _lastEndPoint.setIdleTimeout(10 * specifiedTimeout);
        Thread.sleep((11 * specifiedTimeout) / 10);

        long start = System.currentTimeMillis();
        try
        {
            int b = clientInputStream.read();
            Assert.fail("Should have timed out waiting for a response, but read " + b);
        }
        catch (SocketTimeoutException e)
        {
            int elapsed = Long.valueOf(System.currentTimeMillis() - start).intValue();
            Assert.assertThat("Expected timeout", elapsed, greaterThanOrEqualTo(3 * specifiedTimeout / 4));
        }

        // write remaining characters
        clientOutputStream.write("90ABCDEF".getBytes(StandardCharsets.UTF_8));
        clientOutputStream.flush();

        // Verify echo server to client
        for (char c : "1234567890ABCDEF".toCharArray())
        {
            int b = clientInputStream.read();
            assertTrue(b > 0);
            assertEquals(c, (char)b);
        }
    }

    @Test
    public void testIdle() throws Exception
    {
        Socket client = newClient();

        client.setSoTimeout(3000);

        SocketChannel server = _connector.accept();
        server.configureBlocking(false);

        _manager.accept(server);

        // Write client to server
        client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));

        // Verify echo server to client
        for (char c : "HelloWorld".toCharArray())
        {
            int b = client.getInputStream().read();
            assertTrue(b > 0);
            assertEquals(c, (char)b);
        }

        Assert.assertTrue(_lastEndPointLatch.await(1, TimeUnit.SECONDS));
        int idleTimeout = 500;
        _lastEndPoint.setIdleTimeout(idleTimeout);

        // read until idle shutdown received
        long start = System.currentTimeMillis();
        int b = client.getInputStream().read();
        assertEquals(-1, b);
        long idle = System.currentTimeMillis() - start;
        assertTrue(idle > idleTimeout / 2);
        assertTrue(idle < idleTimeout * 2);

        // But endpoint may still be open for a little bit.
        for (int i = 0; i < 10; ++i)
        {
            if (_lastEndPoint.isOpen())
                Thread.sleep(2 * idleTimeout / 10);
            else
                break;
        }
        assertFalse(_lastEndPoint.isOpen());
    }

    @Test
    public void testBlockedReadIdle() throws Exception
    {
        Socket client = newClient();
        InputStream clientInputStream = client.getInputStream();
        OutputStream clientOutputStream = client.getOutputStream();

        client.setSoTimeout(5000);

        SocketChannel server = _connector.accept();
        server.configureBlocking(false);

        _manager.accept(server);

        // Write client to server
        clientOutputStream.write("HelloWorld".getBytes(StandardCharsets.UTF_8));

        // Verify echo server to client
        for (char c : "HelloWorld".toCharArray())
        {
            int b = clientInputStream.read();
            assertTrue(b > 0);
            assertEquals(c, (char)b);
        }

        Assert.assertTrue(_lastEndPointLatch.await(1, TimeUnit.SECONDS));
        int idleTimeout = 500;
        _lastEndPoint.setIdleTimeout(idleTimeout);

        // Write 8 and cause block waiting for 10
        _blockAt = 10;
        clientOutputStream.write("12345678".getBytes(StandardCharsets.UTF_8));
        clientOutputStream.flush();

        // read until idle shutdown received
        long start = System.currentTimeMillis();
        int b = clientInputStream.read();
        assertEquals('E', b);
        long idle = System.currentTimeMillis() - start;
        assertTrue(idle > idleTimeout / 2);
        assertTrue(idle < idleTimeout * 2);

        for (char c : "E: 12345678".toCharArray())
        {
            b = clientInputStream.read();
            assertTrue(b > 0);
            assertEquals(c, (char)b);
        }
        b = clientInputStream.read();
        assertEquals(-1,b);

        // But endpoint is still open.
        if(_lastEndPoint.isOpen())
            // Wait for another idle callback
            Thread.sleep(idleTimeout * 2);

        // endpoint is closed.
        assertFalse(_lastEndPoint.isOpen());
    }

    @Test
    public void testStress() throws Exception
    {
        Socket client = newClient();
        client.setSoTimeout(30000);

        SocketChannel server = _connector.accept();
        server.configureBlocking(false);

        _manager.accept(server);
        final int writes = 200000;

        final byte[] bytes = "HelloWorld-".getBytes(StandardCharsets.UTF_8);
        byte[] count = "0\n".getBytes(StandardCharsets.UTF_8);
        BufferedOutputStream out = new BufferedOutputStream(client.getOutputStream());
        final CountDownLatch latch = new CountDownLatch(writes);
        final InputStream in = new BufferedInputStream(client.getInputStream());
        final long start = System.currentTimeMillis();
        out.write(bytes);
        out.write(count);
        out.flush();

        Assert.assertTrue(_lastEndPointLatch.await(1, TimeUnit.SECONDS));
        _lastEndPoint.setIdleTimeout(5000);

        new Thread()
        {
            @Override
            public void run()
            {
                Thread.currentThread().setPriority(MAX_PRIORITY);
                long last = -1;
                int count = -1;
                try
                {
                    while (latch.getCount() > 0)
                    {
                        // Verify echo server to client
                        for (byte b0 : bytes)
                        {
                            int b = in.read();
                            Assert.assertThat(b, greaterThan(0));
                            assertEquals(0xff & b0, b);
                        }

                        count = 0;
                        int b = in.read();
                        while (b > 0 && b != '\n')
                        {
                            count = count * 10 + (b - '0');
                            b = in.read();
                        }
                        last = System.currentTimeMillis();

                        //if (latch.getCount()%1000==0)
                        //    System.out.println(writes-latch.getCount());

                        latch.countDown();
                    }
                }
                catch (Throwable e)
                {

                    long now = System.currentTimeMillis();
                    System.err.println("count=" + count);
                    System.err.println("latch=" + latch.getCount());
                    System.err.println("time=" + (now - start));
                    System.err.println("last=" + (now - last));
                    System.err.println("endp=" + _lastEndPoint);
                    System.err.println("conn=" + _lastEndPoint.getConnection());

                    e.printStackTrace();
                }
            }
        }.start();

        // Write client to server
        for (int i = 1; i < writes; i++)
        {
            out.write(bytes);
            out.write(Integer.toString(i).getBytes(StandardCharsets.ISO_8859_1));
            out.write('\n');
            if (i % 1000 == 0)
            {
                //System.err.println(i+"/"+writes);
                out.flush();
            }
            Thread.yield();
        }
        out.flush();

        long last = latch.getCount();
        while (!latch.await(5, TimeUnit.SECONDS))
        {
            //System.err.println(latch.getCount());
            if (latch.getCount() == last)
                Assert.fail();
            last = latch.getCount();
        }

        assertEquals(0, latch.getCount());
    }

    @Test
    public void testWriteBlocked() throws Exception
    {
        Socket client = newClient();

        client.setSoTimeout(10000);

        SocketChannel server = _connector.accept();
        server.configureBlocking(false);

        _manager.accept(server);

        // Write client to server
        _writeCount = 10000;
        String data = "Now is the time for all good men to come to the aid of the party";
        client.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
        BufferedInputStream in = new BufferedInputStream(client.getInputStream());

        int byteNum = 0;
        try
        {
            for (int i = 0; i < _writeCount; i++)
            {
                if (i % 1000 == 0)
                    TimeUnit.MILLISECONDS.sleep(200);

                // Verify echo server to client
                for (int j = 0; j < data.length(); j++)
                {
                    char c = data.charAt(j);
                    int b = in.read();
                    byteNum++;
                    assertTrue(b > 0);
                    assertEquals("test-" + i + "/" + j,c,(char)b);
                }

                if (i == 0)
                    _lastEndPoint.setIdleTimeout(60000);
            }
        }
        catch (SocketTimeoutException e)
        {
            System.err.println("SelectorManager.dump() = " + _manager.dump());
            LOG.warn("Server: " + server);
            LOG.warn("Error reading byte #" + byteNum,e);
            throw e;
        }

        client.close();

        for (int i = 0; i < 10; ++i)
        {
            if (server.isOpen())
                Thread.sleep(10);
            else
                break;
        }
        assertFalse(server.isOpen());
    }
    

    // TODO make this test reliable
    @Test
    @Ignore
    public void testRejectedExecution() throws Exception
    {
        _manager.stop();
        _threadPool.stop();
        
        final CountDownLatch latch = new CountDownLatch(1);
        
        BlockingQueue<Runnable> q = new ArrayBlockingQueue<>(4);
        _threadPool = new QueuedThreadPool(4,4,60000,q);
        _manager = new SelectorManager(_threadPool, _scheduler, 1)
        {

            @Override
            protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException
            {
                SocketChannelEndPoint endp = new SocketChannelEndPoint(channel,selector,selectionKey,getScheduler());
                _lastEndPoint = endp;
                _lastEndPointLatch.countDown();
                return endp;
            }

            @Override
            public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException
            {
                return new TestConnection(endpoint,latch);
            }
        };
        
        _threadPool.start();
        _manager.start();
        
        AtomicInteger timeout = new AtomicInteger();
        AtomicInteger rejections = new AtomicInteger();
        AtomicInteger echoed = new AtomicInteger();
        
        CountDownLatch closed = new CountDownLatch(20);
        for (int i=0;i<20;i++)
        {
            new Thread()
            {
                public void run()
                {
                    try(Socket client = newClient();)
                    {
                        client.setSoTimeout(5000);

                        SocketChannel server = _connector.accept();
                        server.configureBlocking(false);

                        _manager.accept(server);

                        // Write client to server
                        client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));
                        client.getOutputStream().flush();
                        client.shutdownOutput();

                        // Verify echo server to client
                        for (char c : "HelloWorld".toCharArray())
                        {
                            int b = client.getInputStream().read();
                            assertTrue(b > 0);
                            assertEquals(c, (char)b);
                        }
                        assertEquals(-1,client.getInputStream().read());
                        echoed.incrementAndGet();
                    }
                    catch(SocketTimeoutException x)
                    {
                        x.printStackTrace();
                        timeout.incrementAndGet();
                    }
                    catch(Throwable x)
                    {
                        rejections.incrementAndGet();
                    }
                    finally
                    {
                        closed.countDown();
                    }
                }
            }.start();
        }

        // unblock the handling
        latch.countDown();
        
        // wait for all clients to complete or fail
        closed.await();
        
        // assert some clients must have been rejected
        Assert.assertThat(rejections.get(),Matchers.greaterThan(0));
        // but not all of them
        Assert.assertThat(rejections.get(),Matchers.lessThan(20));
        // none should have timed out
        Assert.assertThat(timeout.get(),Matchers.equalTo(0));
        // and the rest should have worked
        Assert.assertThat(echoed.get(),Matchers.equalTo(20-rejections.get())); 
        
        // and the selector is still working for new requests
        try(Socket client = newClient();)
        {
            client.setSoTimeout(5000);

            SocketChannel server = _connector.accept();
            server.configureBlocking(false);

            _manager.accept(server);

            // Write client to server
            client.getOutputStream().write("HelloWorld".getBytes(StandardCharsets.UTF_8));
            client.getOutputStream().flush();
            client.shutdownOutput();

            // Verify echo server to client
            for (char c : "HelloWorld".toCharArray())
            {
                int b = client.getInputStream().read();
                assertTrue(b > 0);
                assertEquals(c, (char)b);
            }
            assertEquals(-1,client.getInputStream().read());
        }
        
    }
}
