/**
 * Copyright 2007-2016, Kaazing Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.mina.filter.reqres;

import java.util.NoSuchElementException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

import junit.framework.TestCase;

import org.apache.mina.core.filterchain.IoFilterChain;
import org.apache.mina.core.filterchain.IoFilter.NextFilter;
import org.apache.mina.core.session.DummySession;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.DefaultWriteRequest;
import org.apache.mina.core.write.WriteRequest;
import org.easymock.AbstractMatcher;
import org.easymock.MockControl;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

/**
 * Tests {@link RequestResponseFilter}.
 *
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 */
public class RequestResponseFilterTest extends TestCase {

    private ScheduledExecutorService scheduler;

    private RequestResponseFilter filter;

    private IoSession session;

    private IoFilterChain chain;

    private NextFilter nextFilter;

    private MockControl nextFilterControl;

    private final WriteRequestMatcher matcher = new WriteRequestMatcher();

    @Override
    @Before
    public void setUp() throws Exception {
        scheduler = Executors.newScheduledThreadPool(1);
        filter = new RequestResponseFilter(new MessageInspector(), scheduler);

        // Set up mock objects.
        session = new DummySession();
        chain = session.getFilterChain();
        nextFilterControl = MockControl.createControl(NextFilter.class);
        nextFilter = (NextFilter) nextFilterControl.getMock();

        // Initialize the filter.
        filter.onPreAdd(chain, "reqres", nextFilter);
        filter.onPostAdd(chain, "reqres", nextFilter);
        Assert.assertFalse(session.getAttributeKeys().isEmpty());
    }

    @Override
    @After
    public void tearDown() throws Exception {
        // Destroy the filter.
        filter.onPreRemove(chain, "reqres", nextFilter);
        filter.onPostRemove(chain, "reqres", nextFilter);
        filter.destroy();
        filter = null;
        scheduler.shutdown();
    }

    @Test
    public void testWholeResponse() throws Exception {
        Request req = new Request(1, new Object(), Long.MAX_VALUE);
        Response res = new Response(req, new Message(1, ResponseType.WHOLE),
                ResponseType.WHOLE);
        WriteRequest rwr = new DefaultWriteRequest(req);

        // Record
        nextFilter.filterWrite(session, new DefaultWriteRequest(req
                .getMessage()));
        nextFilterControl.setMatcher(matcher);
        nextFilter.messageSent(session, rwr);
        nextFilter.messageReceived(session, res);

        // Replay
        nextFilterControl.replay();
        filter.filterWrite(nextFilter, session, rwr);
        filter.messageSent(nextFilter, session, matcher.getLastWriteRequest());
        filter.messageReceived(nextFilter, session, res.getMessage());
        filter.messageReceived(nextFilter, session, res.getMessage()); // Ignored

        // Verify
        nextFilterControl.verify();
        Assert.assertEquals(res, req.awaitResponse());
        assertNoSuchElementException(req);
    }

    private void assertNoSuchElementException(Request req)
            throws InterruptedException {
        // Make sure if an exception is thrown if a user waits one more time.
        try {
            req.awaitResponse();
            Assert.fail();
        } catch (NoSuchElementException e) {
            // Signifies a successful test execution
            Assert.assertTrue(true);
        }
    }

    @Test
    public void testPartialResponse() throws Exception {
        Request req = new Request(1, new Object(), Long.MAX_VALUE);
        Response res1 = new Response(req, new Message(1, ResponseType.PARTIAL),
                ResponseType.PARTIAL);
        Response res2 = new Response(req, new Message(1,
                ResponseType.PARTIAL_LAST), ResponseType.PARTIAL_LAST);
        WriteRequest rwr = new DefaultWriteRequest(req);

        // Record
        nextFilter.filterWrite(session, new DefaultWriteRequest(req
                .getMessage()));
        nextFilterControl.setMatcher(matcher);
        nextFilter.messageSent(session, rwr);
        nextFilter.messageReceived(session, res1);
        nextFilter.messageReceived(session, res2);

        // Replay
        nextFilterControl.replay();
        filter.filterWrite(nextFilter, session, rwr);
        filter.messageSent(nextFilter, session, matcher.getLastWriteRequest());
        filter.messageReceived(nextFilter, session, res1.getMessage());
        filter.messageReceived(nextFilter, session, res2.getMessage());
        filter.messageReceived(nextFilter, session, res1.getMessage()); // Ignored
        filter.messageReceived(nextFilter, session, res2.getMessage()); // Ignored

        // Verify
        nextFilterControl.verify();
        Assert.assertEquals(res1, req.awaitResponse());
        Assert.assertEquals(res2, req.awaitResponse());
        assertNoSuchElementException(req);
    }

    @Test
    public void testWholeResponseTimeout() throws Exception {
        Request req = new Request(1, new Object(), 10); // 10ms timeout
        Response res = new Response(req, new Message(1, ResponseType.WHOLE),
                ResponseType.WHOLE);
        WriteRequest rwr = new DefaultWriteRequest(req);

        // Record
        nextFilter.filterWrite(session, new DefaultWriteRequest(req
                .getMessage()));
        nextFilterControl.setMatcher(matcher);
        nextFilter.messageSent(session, rwr);
        nextFilter.exceptionCaught(session, new RequestTimeoutException(req));
        nextFilterControl.setMatcher(new ExceptionMatcher());

        // Replay
        nextFilterControl.replay();
        filter.filterWrite(nextFilter, session, rwr);
        filter.messageSent(nextFilter, session, matcher.getLastWriteRequest());
        Thread.sleep(300); // Wait until the request times out.
        filter.messageReceived(nextFilter, session, res.getMessage()); // Ignored

        // Verify
        nextFilterControl.verify();
        assertRequestTimeoutException(req);
        assertNoSuchElementException(req);
    }

    private void assertRequestTimeoutException(Request req)
            throws InterruptedException {
        try {
            req.awaitResponse();
            Assert.fail();
        } catch (RequestTimeoutException e) {
            // Signifies a successful test execution
            Assert.assertTrue(true);
        }
    }

    @Test
    public void testPartialResponseTimeout() throws Exception {
        Request req = new Request(1, new Object(), 10); // 10ms timeout
        Response res1 = new Response(req, new Message(1, ResponseType.PARTIAL),
                ResponseType.PARTIAL);
        Response res2 = new Response(req, new Message(1,
                ResponseType.PARTIAL_LAST), ResponseType.PARTIAL_LAST);
        WriteRequest rwr = new DefaultWriteRequest(req);

        // Record
        nextFilter.filterWrite(session, new DefaultWriteRequest(req
                .getMessage()));
        nextFilterControl.setMatcher(matcher);
        nextFilter.messageSent(session, rwr);
        nextFilter.messageReceived(session, res1);
        nextFilter.exceptionCaught(session, new RequestTimeoutException(req));
        nextFilterControl.setMatcher(new ExceptionMatcher());

        // Replay
        nextFilterControl.replay();
        filter.filterWrite(nextFilter, session, rwr);
        filter.messageSent(nextFilter, session, matcher.getLastWriteRequest());
        filter.messageReceived(nextFilter, session, res1.getMessage());
        Thread.sleep(300); // Wait until the request times out.
        filter.messageReceived(nextFilter, session, res2.getMessage()); // Ignored
        filter.messageReceived(nextFilter, session, res1.getMessage()); // Ignored

        // Verify
        nextFilterControl.verify();
        Assert.assertEquals(res1, req.awaitResponse());
        assertRequestTimeoutException(req);
        assertNoSuchElementException(req);
    }

    @Test
    public void testTimeoutByDisconnection() throws Exception {
        // We run a test case that doesn't raise a timeout to make sure
        // the timeout is not raised again by disconnection.
        testWholeResponse();
        nextFilterControl.reset();

        Request req1 = new Request(1, new Object(), Long.MAX_VALUE);
        Request req2 = new Request(2, new Object(), Long.MAX_VALUE);
        WriteRequest rwr1 = new DefaultWriteRequest(req1);
        WriteRequest rwr2 = new DefaultWriteRequest(req2);

        // Record
        nextFilter.filterWrite(session, new DefaultWriteRequest(req1
                .getMessage()));
        nextFilterControl.setMatcher(matcher);
        nextFilter.messageSent(session, rwr1);
        nextFilter.filterWrite(session, new DefaultWriteRequest(req2
                .getMessage()));
        nextFilter.messageSent(session, rwr2);
        nextFilter.exceptionCaught(session, new RequestTimeoutException(req1));
        nextFilterControl.setMatcher(new ExceptionMatcher());
        nextFilter.exceptionCaught(session, new RequestTimeoutException(req2));
        nextFilter.sessionClosed(session);

        // Replay
        nextFilterControl.replay();
        filter.filterWrite(nextFilter, session, rwr1);
        filter.messageSent(nextFilter, session, matcher.getLastWriteRequest());
        filter.filterWrite(nextFilter, session, rwr2);
        filter.messageSent(nextFilter, session, matcher.getLastWriteRequest());
        filter.sessionClosed(nextFilter, session);

        // Verify
        nextFilterControl.verify();
        assertRequestTimeoutException(req1);
        assertRequestTimeoutException(req2);
    }

    static class Message {
        private final int id;

        private final ResponseType type;

        Message(int id, ResponseType type) {
            this.id = id;
            this.type = type;
        }

        public int getId() {
            return id;
        }

        public ResponseType getType() {
            return type;
        }
    }

    private static class MessageInspector implements ResponseInspector {
        /**
         * Default constructor
         */
        public MessageInspector() {
            super();
        }
        
        public Object getRequestId(Object message) {
            if (!(message instanceof Message)) {
                return null;
            }

            return ((Message) message).getId();
        }

        public ResponseType getResponseType(Object message) {
            if (!(message instanceof Message)) {
                return null;
            }

            return ((Message) message).getType();
        }
    }

    private static class WriteRequestMatcher extends AbstractMatcher {
        private WriteRequest lastWriteRequest;

        /**
         * Default constructor
         */
        public WriteRequestMatcher() {
            super();
        }
        
        public WriteRequest getLastWriteRequest() {
            return lastWriteRequest;
        }

        @Override
        protected boolean argumentMatches(Object expected, Object actual) {
            if (actual instanceof WriteRequest
                    && expected instanceof WriteRequest) {
                boolean answer = ((WriteRequest) expected).getMessage().equals(
                        ((WriteRequest) actual).getMessage());
                lastWriteRequest = (WriteRequest) actual;
                return answer;
            }
            return super.argumentMatches(expected, actual);
        }
    }

    static class ExceptionMatcher extends AbstractMatcher {
        @Override
        protected boolean argumentMatches(Object expected, Object actual) {
            if (actual instanceof RequestTimeoutException
                    && expected instanceof RequestTimeoutException) {
                return ((RequestTimeoutException) expected)
                        .getRequest()
                        .equals(((RequestTimeoutException) actual).getRequest());
            }
            return super.argumentMatches(expected, actual);
        }
    }
}
