package com.twelvemonkeys.servlet.cache;

import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.net.NetUtil;
import org.jmock.Mock;
import org.jmock.cglib.MockObjectTestCase;
import org.jmock.core.Invocation;
import org.jmock.core.stub.CustomStub;

import javax.servlet.ServletContext;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.*;

/**
 * CacheManagerTestCase
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @author last modified by $Author: haku $
 * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java#2 $
 */
public class HTTPCacheTestCase extends MockObjectTestCase {
    // TODO: Clean up!

    private static final File TEMP_ROOT = new File(FileUtil.getTempDirFile(), "cache-test");

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        assertTrue("Could not create temp dir, tests can not run", (TEMP_ROOT.exists() && TEMP_ROOT.isDirectory()) || TEMP_ROOT.mkdirs());
        // Clear temp dir
        File[] files = TEMP_ROOT.listFiles();
        for (File file : files) {
            file.delete();
        }
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    public void testCreateNegativeNoName() {
        try {
            new HTTPCache(null, (ServletContext) newDummy(ServletContext.class), 500, 0, 10, true);
            fail("Expected creation failure, no name");
        }
        catch (IllegalArgumentException expected) {
            String message = expected.getMessage().toLowerCase();
            assertTrue(message.contains("name"));
            assertTrue(message.contains("null"));
        }

        try {
            new HTTPCache("", (ServletContext) newDummy(ServletContext.class), 500, 0, 10, true);
            fail("Expected creation failure, empty name");
        }
        catch (IllegalArgumentException expected) {
            String message = expected.getMessage().toLowerCase();
            assertTrue(message.contains("name"));
            assertTrue(message.contains("empty"));
        }
    }

    public void testCreateNegativeNoContext() {
        try {
            new HTTPCache("Dummy", null, 500, 0, 10, true);
            fail("Expected creation failure, no context");
        }
        catch (IllegalArgumentException expected) {
            String message = expected.getMessage().toLowerCase();
            assertTrue(message.contains("context"));
            assertTrue(message.contains("null"));
        }

    }

    public void testCreateNegativeNoTempFolder() {
        try {
            new HTTPCache(null, 500, 0, 10, true);
            fail("Expected creation failure, no temp folder");
        }
        catch (IllegalArgumentException expected) {
            String message = expected.getMessage().toLowerCase();
            assertTrue(message.contains("temp"));
            assertTrue(message.contains("folder"));
            assertTrue(message.contains("null"));
        }
    }

    public void testCreateNegativeValues() {
        try {
            new HTTPCache(TEMP_ROOT, -1, 0, 10, true);
            fail("Expected creation failure");
        }
        catch (IllegalArgumentException expected) {
            String message = expected.getMessage().toLowerCase();
            assertTrue(message.contains("negative"));
            assertTrue(message.contains("expiry time"));
        }

        try {
            new HTTPCache(TEMP_ROOT, 1000, -1, 10, false);
            fail("Expected creation failure");
        }
        catch (IllegalArgumentException expected) {
            String message = expected.getMessage().toLowerCase();
            assertTrue(message.contains("negative"));
            assertTrue(message.contains("cache size"));
        }

        try {
            new HTTPCache(TEMP_ROOT, 1000, 128, -1, true);
            fail("Expected creation failure");
        }
        catch (IllegalArgumentException expected) {
            String message = expected.getMessage().toLowerCase();
            assertTrue(message.contains("negative"));
            assertTrue(message.contains("number"));
        }
    }

    public void testCreate() {
        new HTTPCache(TEMP_ROOT, 500, 0, 10, true);
    }

    public void testCreateServletContext() {
        Mock mockContext = mock(ServletContext.class);
        // Currently context is used for tempdir and logging
        mockContext.stubs().method("getAttribute").with(eq("javax.servlet.context.tempdir")).will(returnValue(TEMP_ROOT));
        new HTTPCache("cache", (ServletContext) mockContext.proxy(), 500, 0, 10, true);
    }

    public void testCacheableRequest() throws IOException, CacheException {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);
                
                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);
        
        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    private String createRequestURI() {
        return "http://www.foo.com/" + getName() + ".bar";
    }

    public void testCacheableRequestWithParameters() throws IOException, CacheException {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        Map<String, List<String>> parameters = new HashMap<String, List<String>>();
        parameters.put("foo", Collections.singletonList("bar"));
        parameters.put("params", Arrays.asList("une", "due", "tres"));
        CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), parameters, null);

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    private CacheRequest configureRequest(Mock pMockRequest, String pRequestURI) {
        return configureRequest(pMockRequest, "GET", pRequestURI, null, null);
    }

    private CacheRequest configureRequest(Mock pMockRequest, String pMethod, String pRequestURI, Map<String, List<String>> pParameters, final Map<String, List<String>> pHeaders) {
        pMockRequest.reset();
        pMockRequest.stubs().method("getRequestURI").will(returnValue(URI.create(pRequestURI)));
        pMockRequest.stubs().method("getParameters").will(returnValue(pParameters == null ? Collections.emptyMap() : pParameters));
        pMockRequest.stubs().method("getHeaders").will(returnValue(pHeaders == null ? Collections.emptyMap() : pHeaders));
        pMockRequest.stubs().method("getMethod").will(returnValue(pMethod));
        return (CacheRequest) pMockRequest.proxy();
    }

    public void testCacheablePersistentRepeatedRequest() throws IOException, CacheException {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
        mockRequest.verify();
        mockResponse.verify();
        mockResolver.verify();

        // Reset
        result.reset();
        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        // Test request again, make sure resolve is executed exactly once
        HTTPCache cache2 = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);
        cache2.doCached(request, response, resolver);

        // Test that second response is equal to first
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testCacheableRepeatedRequest() throws IOException, CacheException {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
        mockRequest.verify();
        mockResponse.verify();
        mockResolver.verify();

        // Reset
        result.reset();
        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        // Test request again, make sure resolve is executed exactly once
        cache.doCached(request, response, resolver);

        // Test that second response is equal to first
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testNonCacheableRequestHeader() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), null, Collections.singletonMap("Cache-Control", Collections.singletonList("no-store")));

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // TODO: How do we know that the response was NOT cached?
        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testNonCacheableRequestHeaderRepeated() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);
        String requestURI = createRequestURI();
        ByteArrayOutputStream result = new ByteArrayOutputStream();

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, requestURI);

        Mock mockResponse = mock(CacheResponse.class);
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockResolver.verify();

        // Reset
        result.reset();

        mockRequest.reset();
        mockRequest.stubs().method("getRequestURI").will(returnValue(URI.create(requestURI)));
        mockRequest.stubs().method("getParameters").will(returnValue(Collections.emptyMap()));
        mockRequest.stubs().method("getHeaders").will(returnValue(Collections.singletonMap("Cache-Control", Collections.singletonList("no-cache")))); // Force non-cached version of cached content
        mockRequest.stubs().method("getMethod").will(returnValue("GET"));

        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        mockResolver.reset();
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub2") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        value[3] = 'B';

        // This cache should not be cached
        cache.doCached(request, response, resolver);

         // Verify that second reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testNonCacheableResponseHeader() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-cache"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Cache-Control", "no-cache");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testNonCacheableResponseHeaderRepeated() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-store"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Cache-Control", "no-store");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockRequest.verify();

        // Reset
        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Cache-Control"), eq("no-store"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        mockResolver.reset();
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Cache-Control", "no-store");
                res.getOutputStream().write(value);

                return null;
            }
        });
        result.reset();
        value[3] = 'B';

        // Repeat invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    // Test non-cacheable response
    public void testNonCacheableResponse() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        ByteArrayOutputStream result = new ByteArrayOutputStream();
        Mock mockResponse = mock(CacheResponse.class);
        mockResponse.expects(once()).method("setStatus").with(eq(500));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(500);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    // Test non-cacheable response
    public void testNonCacheableResponseRepeated() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        ByteArrayOutputStream result = new ByteArrayOutputStream();
        Mock mockResponse = mock(CacheResponse.class);
        mockResponse.expects(once()).method("setStatus").with(eq(500));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(500);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockResolver.verify();

        // Test request again, should do new resolve...
        result.reset();
        value[3] = 'B';

        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(500));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        mockResolver.reset();
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(500);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.getOutputStream().write(value);

                return null;
            }
        });

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }


    // Test that request headers are forwarded to resolver...
    public void testRequestHeadersForwarded() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        final Map<String, List<String>> headers = new LinkedHashMap<String, List<String>>();
        headers.put("Cache-Control", Arrays.asList("no-cache"));
        headers.put("X-Custom", Arrays.asList("FOO", "BAR"));

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, "HEAD", createRequestURI(), null, headers);

        Mock mockResponse = mock(CacheResponse.class);
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);

                Map<String, List<String>> reqHeaders = req.getHeaders();
                assertEquals(headers, reqHeaders);

                // Make sure that we preserve insertion order
                Set<Map.Entry<String, List<String>>> expected = headers.entrySet();
                Iterator<Map.Entry<String,List<String>>> actual = reqHeaders.entrySet().iterator();
                for (Map.Entry<String, List<String>> entry : expected) {
                    assertEquals(entry, actual.next());
                }

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);
    }

    // Test that response headers are preserved
    public void testCacheablePreserveResponseHeaders() throws IOException, CacheException {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        ByteArrayOutputStream result = new ByteArrayOutputStream();
        Mock mockResponse = mock(CacheResponse.class);
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("Cache-Control"), eq("public"));
        mockResponse.expects(atLeastOnce()).method(or(eq("setHeader"), eq("addHeader"))).with(eq("X-Custom"), eq("FOO")).id("firstCustom");
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("addHeader").with(eq("X-Custom"), eq("BAR")).after("firstCustom");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.stubs().method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Cache-Control", "public");
                res.addHeader("X-Custom", "FOO");
                res.addHeader("X-Custom", "BAR");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    // Test Vary
    public void testVaryMissingRequestHeader() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("X-Foo"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader(HTTPCache.HEADER_CONTENT_TYPE, "x-foo/bar");
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "X-Foo");
                res.setHeader("X-Foo", "foobar header");
                res.setHeader("X-Other", "don't care");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockRequest.verify();

        // Reset
        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("X-Foo"), ANYTHING);
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        result.reset();

        // Repeat invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testVaryMissingRequestHeaderRepeated() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);
        String requestURI = createRequestURI();

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, requestURI);

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader(HTTPCache.HEADER_CONTENT_TYPE, "x-foo/bar");
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "X-Foo");
                res.setHeader("X-Other", "don't care");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();


        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockRequest.verify();

        // Reset
        request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar")));

        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        mockResolver.reset();
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "X-Foo");
                res.setHeader("X-Other", "don't care");
                res.getOutputStream().write(value);

                return null;
            }
        });
        result.reset();
        value[3] = 'B';

        // Repeat invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testVarySameResourceIsCached() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, "GET", createRequestURI(), null, Collections.singletonMap("X-Foo", Collections.singletonList("foobar value")));

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "X-Foo");
                res.setHeader("X-Other", "don't care");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockRequest.verify();

        // Reset
        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        result.reset();

        // Repeat invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testVaryDifferentResources() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);
        String requestURI = createRequestURI();

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("foo")));

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foo".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "X-Foo");
                res.setHeader("X-Other", "don't care");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockRequest.verify();

        // Reset
        request = configureRequest(mockRequest, "GET", requestURI, null, Collections.singletonMap("X-Foo", Collections.singletonList("bar")));

        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("X-Foo"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        mockResolver.reset();
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "X-Foo");
                res.setHeader("Cache-Control", "no-store");
                res.getOutputStream().write(value);

                return null;
            }
        });
        result.reset();
        value[0] = 'b';
        value[1] = 'a';
        value[2] = 'r';

        // Repeat invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    public void testVaryStarNonCached() throws Exception {
        HTTPCache cache = new HTTPCache(TEMP_ROOT, 60000, 1024 * 1024, 10, true);

        // Custom setup
        Mock mockRequest = mock(CacheRequest.class);
        CacheRequest request = configureRequest(mockRequest, createRequestURI());

        Mock mockResponse = mock(CacheResponse.class);
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("*"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));
        CacheResponse response = (CacheResponse) mockResponse.proxy();

        final byte[] value = "foobar".getBytes("UTF-8");

        Mock mockResolver = mock(ResponseResolver.class);
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "*");
                res.getOutputStream().write(value);

                return null;
            }
        });
        ResponseResolver resolver = (ResponseResolver) mockResolver.proxy();

        // Do the invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));

        mockRequest.verify();
        mockResponse.verify();
        mockRequest.verify();

        // Reset
        mockResponse.reset();
        mockResponse.expects(once()).method("setStatus").with(eq(HTTPCache.STATUS_OK));
        mockResponse.stubs().method("setHeader");
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Date"), ANYTHING);
        mockResponse.expects(atLeastOnce()).method("setHeader").with(eq("Vary"), eq("*"));
        mockResponse.stubs().method("addHeader");
        mockResponse.expects(atLeastOnce()).method("getOutputStream").will(returnValue(result));

        mockResolver.reset();
        mockResolver.expects(once()).method("resolve").will(new CustomStub("request resolver stub") {
            public Void invoke(Invocation invocation) throws Throwable {
                CacheRequest req = (CacheRequest) invocation.parameterValues.get(0);
                CacheResponse res = (CacheResponse) invocation.parameterValues.get(1);

                res.setStatus(HTTPCache.STATUS_OK);
                res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis()));
                res.setHeader("Vary", "*");
                res.getOutputStream().write(value);

                return null;
            }
        });
        result.reset();
        value[3] = 'B';

        // Repeat invocation
        cache.doCached(request, response, resolver);

        // Verify that reponse is ok
        assertEquals(value.length, result.size());
        assertEquals(new String(value, "UTF-8"), new String(result.toByteArray(), "UTF-8"));
        assertTrue(Arrays.equals(value, result.toByteArray()));
    }

    /*
    public void testVaryVariations() {
        fail("TODO");
    }

    public void testVarationsWithSameContentType() {
        // I believe there is a bug if two variations has same content type...
        fail("TODO");
    }

    // Test failing request (IOException)
    public void testIOException() {
        fail("TODO");
    }

    public void testCacheException() {
        fail("TODO");
    }

    public void testRuntimeException() {
        fail("TODO");
    }

    // Test failing (negative) HTTP response (401, 404, 410, 500, etc)
    public void testNegativeCache() {
        fail("TODO");
    }

    public void testNegativeCacheExpires() {
        fail("TODO");
    }

    // Test If-None-Match/ETag support
    public void testIfNoneMatch() {
        fail("TODO");
    }

    // Test If-Modified-Since support
    public void testIfModifiedSince() {
        fail("TODO");
    }

    // Test that data really expires when TTL is over
    public void testTimeToLive() {
        fail("TODO");
    }

    public void testMaxAgeRequest() {
        fail("TODO");    
    }

    // Test that for requests with authorization, responses are not shared between different authorized users, unless response is marked as Cache-Control: public 
    public void testAuthorizedRequestPublic() {
        fail("TODO");
    }

    public void testAuthorizedRequestPrivate() {
        fail("TODO");
    }

    public void testPutPostDeleteInvalidatesCache() {
        fail("TODO");
    }

    // TODO: Move out to separate package/test, just keep it here for PoC
    public void testClientRequest() {
        fail("Not implemented");
    }

    // TODO: Move out to separate package/test, just keep it here for PoC
    public void testServletRequest() {
        fail("Not implemented");
    }
    */
}
