/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */


// Unit-tests for ProxyFetch and related classes

#include "pagespeed/automatic/proxy_fetch.h"

#include "base/logging.h"
#include "pagespeed/opt/logging/log_record.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/mock_callback.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "pagespeed/kernel/base/abstract_mutex.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/null_message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/request_headers.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
#include "pagespeed/kernel/thread/thread_synchronizer.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
#include "pagespeed/kernel/util/platform.h"

namespace net_instaweb {

// A stripped-down mock of ProxyFetch, used for testing PropertyCacheComplete().
class MockProxyFetch : public ProxyFetch {
 public:
  MockProxyFetch(AsyncFetch* async_fetch,
                 ProxyFetchFactory* factory,
                 ServerContext* server_context)
      : ProxyFetch("http://www.google.com", false,
                   NULL,  // callback
                   async_fetch,
                   NULL,  // no original content fetch
                   GetNewRewriteDriver(server_context, async_fetch),
                   server_context,
                   NULL,  // timer
                   factory),
        complete_(false) {
    response_headers()->set_status_code(HttpStatus::kOK);
  }

  ~MockProxyFetch() { }

  static RewriteDriver* GetNewRewriteDriver(ServerContext* server_context,
                                     AsyncFetch* async_fetch) {
    RewriteDriver* driver = server_context->NewRewriteDriver(
                                async_fetch->request_context());
    RequestHeaders headers;
    driver->SetRequestHeaders(headers);
    return driver;
  }

  void PropertyCacheComplete(
      ProxyFetchPropertyCallbackCollector* callback_collector) {
    complete_ = true;
  }

  void Done(bool success) {
    HandleDone(success);
  }

  bool complete() const { return complete_; }

  RewriteDriver* driver() const { return driver_; }

 private:
  bool complete_;
  DISALLOW_COPY_AND_ASSIGN(MockProxyFetch);
};

// A wrapper around MockProxyFetch that manages all objects on the heap and
// initializes things for no parsing and then parses a token script. You must
// NOT call Done() on the MockProxyFetch object as we do it in our destructor.
class ManagedMockProxyFetch {
 public:
  ManagedMockProxyFetch(ServerContext* server_context,
                        bool allow_options_to_be_set_by_cookies,
                        StringPiece sticky_query_parameters,
                        StringPiece sticky_query_parameters_token,
                        StringPiece query_params, StringPiece option_cookies) {
    server_context->global_options()->ClearSignatureForTesting();
    server_context->global_options()->set_allow_options_to_be_set_by_cookies(
        allow_options_to_be_set_by_cookies);
    server_context->global_options()->set_sticky_query_parameters(
        sticky_query_parameters);
    // Suppresses parsing and the invocation of mock_proxy_fetch_->Done().
    server_context->global_options()->set_max_html_parse_bytes(0L);
    server_context->global_options()->ComputeSignature();
    message_handler_.reset(new NullMessageHandler());
    async_fetch_.reset(new StringAsyncFetch(
        RequestContext::NewTestRequestContext(
            server_context->thread_system())));
    async_fetch_->response_headers()->Add("Content-Type", "text/html");
    async_fetch_->response_headers()->ComputeCaching();
    async_fetch_->request_context()->set_sticky_query_parameters_token(
        sticky_query_parameters_token);
    fetch_factory_.reset(new ProxyFetchFactory(server_context));
    mock_proxy_fetch_ = new MockProxyFetch(async_fetch_.get(),
                                           fetch_factory_.get(),
                                           server_context);
    mock_proxy_fetch_->driver()->set_pagespeed_query_params(query_params);
    mock_proxy_fetch_->driver()->set_pagespeed_option_cookies(option_cookies);
    mock_proxy_fetch_->Write("<html>HTML</html>.", message_handler_.get());
    mock_proxy_fetch_->Flush(message_handler_.get());
  }

  ~ManagedMockProxyFetch() {
    mock_proxy_fetch_->Done(true);
  }

  MockProxyFetch* mock_proxy_fetch() const { return mock_proxy_fetch_; }

 private:
  scoped_ptr<NullMessageHandler> message_handler_;
  scoped_ptr<StringAsyncFetch> async_fetch_;
  scoped_ptr<ProxyFetchFactory> fetch_factory_;
  MockProxyFetch* mock_proxy_fetch_;  // Not scoped_ptr as it self-deletes.
};

class ProxyFetchPropertyCallbackCollectorTest : public RewriteTestBase {
 public:
  void PostLookupTask() {
    post_lookup_called_ = true;
  }
  void CheckPageNotNullPostLookupTask(
      ProxyFetchPropertyCallbackCollector* collector,
      WorkerTestBase::SyncPoint* sync_point) {
    EXPECT_TRUE(collector->fallback_property_page() != NULL);
    sync_point->Notify();
  }

  void AddCheckPageNotNullPostLookupTask(
      ProxyFetchPropertyCallbackCollector* collector,
      WorkerTestBase::SyncPoint* sync_point) {
    collector->AddPostLookupTask(MakeFunction(
        this,
        &ProxyFetchPropertyCallbackCollectorTest::
            CheckPageNotNullPostLookupTask,
        collector,
        sync_point));
  }

 protected:
  ProxyFetchPropertyCallbackCollectorTest() :
    thread_system_(Platform::CreateThreadSystem()),
    server_context_(server_context()),
    post_lookup_called_(false) {
  }

  scoped_ptr<ThreadSystem> thread_system_;
  ServerContext* server_context_;

  // Create a collector.
  ProxyFetchPropertyCallbackCollector* MakeCollector() {
    ProxyFetchPropertyCallbackCollector* collector =
        new ProxyFetchPropertyCallbackCollector(
            server_context_, RewriteTestBase::kTestDomain,
            RequestContext::NewTestRequestContext(thread_system_.get()),
            options(), UserAgentMatcher::kDesktop);
    // Collector should not contain any PropertyPages
    EXPECT_EQ(NULL, collector->ReleasePropertyPage(
        ProxyFetchPropertyCallback::kPropertyCachePage));

    return collector;
  }

  void EnableCollectorPrefix() {
    ThreadSynchronizer* sync = server_context()->thread_synchronizer();
    sync->EnableForPrefix(ProxyFetch::kCollectorDoneFinish);
    sync->EnableForPrefix(ProxyFetch::kCollectorDetachFinish);
    sync->EnableForPrefix(ProxyFetch::kCollectorConnectProxyFetchFinish);
    sync->EnableForPrefix(ProxyFetch::kCollectorRequestHeadersCompleteFinish);
  }

  // Add a callback to the collector.
  ProxyFetchPropertyCallback* AddCallback(
      ProxyFetchPropertyCallbackCollector* collector,
      ProxyFetchPropertyCallback::PageType page_type) {
    AbstractMutex* mutex = thread_system_->NewMutex();
    DCHECK(page_type == ProxyFetchPropertyCallback::kPropertyCachePage);
    ProxyFetchPropertyCallback* callback =
        new ProxyFetchPropertyCallback(
            page_type, page_property_cache(), RewriteTestBase::kTestDomain,
            "hash", UserAgentMatcher::kDesktop, collector, mutex);
    EXPECT_EQ(page_type, callback->page_type());
    collector->AddCallback(callback);
    return callback;
  }

  void AddPostLookupConnectProxyFetchCallDone(
      ProxyFetchPropertyCallbackCollector* collector,
      MockProxyFetch* mock_proxy_fetch,
      ProxyFetchPropertyCallback* callback) {
    collector->AddPostLookupTask(MakeFunction(
        this, &ProxyFetchPropertyCallbackCollectorTest::PostLookupTask));
    collector->ConnectProxyFetch(mock_proxy_fetch);
    callback->Done(true);
  }

  void ConnectProxyFetchAddPostLookupCallDone(
      ProxyFetchPropertyCallbackCollector* collector,
      MockProxyFetch* mock_proxy_fetch,
      ProxyFetchPropertyCallback* callback) {
    collector->ConnectProxyFetch(mock_proxy_fetch);
    collector->AddPostLookupTask(MakeFunction(
        this, &ProxyFetchPropertyCallbackCollectorTest::PostLookupTask));
    callback->Done(true);
  }

  void CallDoneAddPostLookupConnectProxyFetch(
      ProxyFetchPropertyCallbackCollector* collector,
      MockProxyFetch* mock_proxy_fetch,
      ProxyFetchPropertyCallback* callback) {
    callback->Done(true);
    collector->AddPostLookupTask(MakeFunction(
        this, &ProxyFetchPropertyCallbackCollectorTest::PostLookupTask));
    collector->ConnectProxyFetch(mock_proxy_fetch);
  }

  void TestAddPostlookupTask(bool add_before_done,
                             bool add_before_proxy_fetch) {
    EnableCollectorPrefix();
    scoped_ptr<ProxyFetchPropertyCallbackCollector> collector;
    collector.reset(MakeCollector());
    ProxyFetchPropertyCallback* page_callback = AddCallback(
        collector.get(), ProxyFetchPropertyCallback::kPropertyCachePage);
    ExpectStringAsyncFetch async_fetch(
        true, RequestContext::NewTestRequestContext(thread_system_.get()));
    ProxyFetchFactory factory(server_context_);
    MockProxyFetch* mock_proxy_fetch = new MockProxyFetch(
        &async_fetch, &factory, server_context_);
    if (add_before_done && add_before_proxy_fetch) {
      AddPostLookupConnectProxyFetchCallDone(
          collector.get(), mock_proxy_fetch, page_callback);
    } else if (add_before_done && !add_before_proxy_fetch) {
      ConnectProxyFetchAddPostLookupCallDone(
          collector.get(), mock_proxy_fetch, page_callback);
    } else if (!add_before_done && add_before_proxy_fetch) {
      CallDoneAddPostLookupConnectProxyFetch(
          collector.get(), mock_proxy_fetch, page_callback);
    } else {
      // Not handled. Make this fail.
    }
    collector->RequestHeadersComplete();
    EXPECT_TRUE(post_lookup_called_);
    mock_proxy_fetch->Done(true);
  }

 private:
  bool post_lookup_called_;

  DISALLOW_COPY_AND_ASSIGN(ProxyFetchPropertyCallbackCollectorTest);
};


// Test fixture for ProxyFetch.
class ProxyFetchTest : public RewriteTestBase {
};

TEST_F(ProxyFetchTest, TestInhibitParsing) {
  NullMessageHandler handler;
  server_context()->global_options()->ClearSignatureForTesting();
  server_context()->global_options()->set_max_html_parse_bytes(0L);
  server_context()->global_options()->ComputeSignature();
  StringAsyncFetch fetch(
      RequestContext::NewTestRequestContext(
          server_context()->thread_system()));
  fetch.response_headers()->Add("Content-Type", "text/html");
  fetch.response_headers()->ComputeCaching();
  ProxyFetchFactory factory(server_context_);
  MockProxyFetch* mock_proxy_fetch = new MockProxyFetch(
      &fetch, &factory, server_context());
  mock_proxy_fetch->Write("<html>HTML</html>.", &handler);
  mock_proxy_fetch->Flush(&handler);

  // We never parsed the HTML, but we did log HTML content type.
  EXPECT_FALSE(mock_proxy_fetch->started_parse_);
  {
    AbstractLogRecord* log_record =
        mock_proxy_fetch->request_context()->log_record();
    ScopedMutex lock(log_record->mutex());
    LoggingInfo* info = log_record->logging_info();
    EXPECT_TRUE(info->is_html_response());
  }

  mock_proxy_fetch->Done(true);
}

namespace {

class FlushLoggingStringAsyncFetch : public StringAsyncFetch {
 public:
  explicit FlushLoggingStringAsyncFetch(
      const RequestContextPtr& request_context)
      : StringAsyncFetch(request_context) {}

  virtual ~FlushLoggingStringAsyncFetch() {}

  bool HandleFlush(MessageHandler* handler) override {
    HandleWrite("|Flush|", handler);
    return true;
  }

 private:
  DISALLOW_COPY_AND_ASSIGN(FlushLoggingStringAsyncFetch);
};

}  // namespace

TEST_F(ProxyFetchTest, TestFollowFlushes) {
  NullMessageHandler handler;
  RewriteOptions* options = server_context()->global_options();
  options->ClearSignatureForTesting();
  options->DisableFilter(RewriteOptions::kAddHead);
  options->set_follow_flushes(true);
  options->ComputeSignature();
  FlushLoggingStringAsyncFetch fetch(
      RequestContext::NewTestRequestContext(
          server_context()->thread_system()));
  fetch.response_headers()->Add("Content-Type", "text/html");
  ProxyFetchFactory factory(server_context_);
  MockProxyFetch* mock_proxy_fetch = new MockProxyFetch(
      &fetch, &factory, server_context());
  mock_proxy_fetch->response_headers()->ComputeCaching();

  // To get a more predictable output with regard to flush markers, we wait
  // for flushes to be fully processed before continuing. ProxyFetch may
  // otherwise aggregate, reposition, or ignore induced flushes.
  mock_proxy_fetch->Write("<html><d>1</d>", &handler);
  mock_proxy_fetch->Flush(&handler);
  mock_scheduler()->AwaitQuiescence();
  mock_proxy_fetch->Write("<d>2</d></html>", &handler);
  mock_proxy_fetch->Flush(&handler);
  mock_scheduler()->AwaitQuiescence();

  // Verify we are parsing HTML.
  EXPECT_TRUE(mock_proxy_fetch->started_parse_);

  mock_proxy_fetch->Done(true);
  mock_scheduler()->AwaitQuiescence();
  EXPECT_EQ(0, server_context()->num_active_rewrite_drivers());
  EXPECT_EQ("<html><d>1</d>|Flush|<d>2</d></html>|Flush||Flush|",
            fetch.buffer());

  // Test following flushes off behaves as expected.
  options->ClearSignatureForTesting();
  options->set_follow_flushes(false);
  options->ComputeSignature();

  FlushLoggingStringAsyncFetch fetch_off(
      RequestContext::NewTestRequestContext(
          server_context()->thread_system()));
  fetch_off.response_headers()->Add("Content-Type", "text/html");

  mock_proxy_fetch = new MockProxyFetch(
      &fetch_off, &factory, server_context());
  mock_proxy_fetch->response_headers()->ComputeCaching();

  mock_proxy_fetch->Write("<html><d>1</d>", &handler);
  mock_proxy_fetch->Flush(&handler);
  mock_scheduler()->AwaitQuiescence();
  mock_proxy_fetch->Write("<d>2</d></html>", &handler);
  mock_proxy_fetch->Flush(&handler);
  mock_scheduler()->AwaitQuiescence();

  EXPECT_TRUE(mock_proxy_fetch->started_parse_);

  mock_proxy_fetch->Done(true);
  mock_scheduler()->AwaitQuiescence();
  EXPECT_EQ(0, server_context()->num_active_rewrite_drivers());
  EXPECT_EQ("<html><d>1</d><d>2</d></html>|Flush|", fetch_off.buffer());
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, EmptyCollectorTest) {
  // Test that creating an empty collector works.
  EnableCollectorPrefix();
  scoped_ptr<ProxyFetchPropertyCallbackCollector> collector;
  collector.reset(MakeCollector());
  // This should not fail
  collector->Detach(HttpStatus::kUnknownStatusCode);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, DoneBeforeDetach) {
  EnableCollectorPrefix();
  // Test that calling Done() before Detach() works.
  ProxyFetchPropertyCallbackCollector* collector = MakeCollector();
  ProxyFetchPropertyCallback* callback = AddCallback(
      collector, ProxyFetchPropertyCallback::kPropertyCachePage);

  // callback->IsCacheValid could be called anytime before callback->Done.
  // Will return true because there are no cache invalidation URL patterns.
  EXPECT_TRUE(callback->IsCacheValid(1L));

  // Invoke the callback.
  callback->Done(true);

  // Collector should now have a page property.
  scoped_ptr<AbstractPropertyPage> page;
  page.reset(collector->ReleaseFallbackPropertyPage());
  EXPECT_TRUE(NULL != page.get());

  // This should not fail - will also delete the collector.
  collector->Detach(HttpStatus::kUnknownStatusCode);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, UrlInvalidDoneBeforeDetach) {
  EnableCollectorPrefix();
  // Invalidate all URLs cached before timestamp 2.
  options_->AddUrlCacheInvalidationEntry("*", 2L, true);
  // Test that calling Done() before Detach() works.
  ProxyFetchPropertyCallbackCollector* collector = MakeCollector();
  ProxyFetchPropertyCallback* callback = AddCallback(
      collector, ProxyFetchPropertyCallback::kPropertyCachePage);

  // callback->IsCacheValid could be called anytime before callback->Done.
  // Will return false due to the invalidation entry.
  EXPECT_FALSE(callback->IsCacheValid(1L));

  // Invoke the callback.
  callback->Done(true);

  // Collector should now have a page property.
  scoped_ptr<AbstractPropertyPage> page;
  page.reset(collector->ReleaseFallbackPropertyPage());
  EXPECT_TRUE(NULL != page.get());

  // This should not fail - will also delete the collector.
  collector->Detach(HttpStatus::kUnknownStatusCode);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, DetachBeforeDone) {
  EnableCollectorPrefix();
  // Test that calling Detach() before Done() works.
  ProxyFetchPropertyCallbackCollector* collector = MakeCollector();
  collector->RequestHeadersComplete();
  ProxyFetchPropertyCallback* callback = AddCallback(
      collector, ProxyFetchPropertyCallback::kPropertyCachePage);

  // callback->IsCacheValid could be called anytime before callback->Done.
  // Will return true because there are no cache invalidation URL patterns.
  EXPECT_TRUE(callback->IsCacheValid(1L));

  // Will not delete the collector since we did not call Done yet.
  collector->Detach(HttpStatus::kUnknownStatusCode);

  // This call is after Detach (but before callback->Done).
  // ProxyFetchPropertyCallbackCollector::IsCacheValid returns false if
  // detached.
  EXPECT_FALSE(callback->IsCacheValid(1L));

  // This will delete the collector.
  callback->Done(true);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, DoneBeforeSetProxyFetch) {
  EnableCollectorPrefix();
  // Test that calling Done() before SetProxyFetch() works.
  scoped_ptr<ProxyFetchPropertyCallbackCollector> collector;
  collector.reset(MakeCollector());
  ProxyFetchPropertyCallback* callback = AddCallback(
      collector.get(), ProxyFetchPropertyCallback::kPropertyCachePage);

  // callback->IsCacheValid could be called anytime before callback->Done.
  // Will return true because there are no cache invalidation URL patterns.
  EXPECT_TRUE(callback->IsCacheValid(1L));

  // Invoke the callback.
  callback->Done(true);

  // Construct mock ProxyFetch to test SetProxyFetch().
  ExpectStringAsyncFetch async_fetch(
      true, RequestContext::NewTestRequestContext(thread_system_.get()));
  ProxyFetchFactory factory(server_context_);
  MockProxyFetch* mock_proxy_fetch = new MockProxyFetch(
      &async_fetch, &factory, server_context_);

  // Should not be complete since SetProxyFetch() called first.
  EXPECT_FALSE(mock_proxy_fetch->complete());

  // Collector should now have a page property.
  scoped_ptr<AbstractPropertyPage> page;
  page.reset(collector->ReleaseFallbackPropertyPage());
  EXPECT_TRUE(NULL != page.get());

  collector.get()->ConnectProxyFetch(mock_proxy_fetch);
  // Should be complete since SetProxyFetch() called after Done().
  EXPECT_TRUE(mock_proxy_fetch->complete());

  // Needed for cleanup.
  mock_proxy_fetch->Done(true);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, SetProxyFetchBeforeDone) {
  EnableCollectorPrefix();
  // Test that calling SetProxyFetch() before Done() works.
  scoped_ptr<ProxyFetchPropertyCallbackCollector> collector;
  collector.reset(MakeCollector());
  ProxyFetchPropertyCallback* callback = AddCallback(
      collector.get(), ProxyFetchPropertyCallback::kPropertyCachePage);

  // Construct mock ProxyFetch to test SetProxyFetch().
  ExpectStringAsyncFetch async_fetch(
      true, RequestContext::NewTestRequestContext(thread_system_.get()));
  ProxyFetchFactory factory(server_context_);
  MockProxyFetch* mock_proxy_fetch = new MockProxyFetch(
      &async_fetch, &factory, server_context_);

  // callback->IsCacheValid could be called anytime before callback->Done.
  // Will return true because there are no cache invalidation URL patterns.
  EXPECT_TRUE(callback->IsCacheValid(1L));

  collector.get()->ConnectProxyFetch(mock_proxy_fetch);
  // Should not be complete since SetProxyFetch() called first.
  EXPECT_FALSE(mock_proxy_fetch->complete());

  EXPECT_TRUE(callback->IsCacheValid(1L));

  // Now invoke the callback.
  callback->Done(true);

  // Collector should now have a page property.
  scoped_ptr<AbstractPropertyPage> page;
  page.reset(collector->ReleaseFallbackPropertyPage());
  EXPECT_TRUE(NULL != page.get());

  // Not yet complete since RequestHeadersComplete() not called yet.
  EXPECT_FALSE(mock_proxy_fetch->complete());

  collector->RequestHeadersComplete();

  // Should be complete since both Done() and RequestHeadersComplete() called.
  EXPECT_TRUE(mock_proxy_fetch->complete());

  // Needed for cleanup.
  mock_proxy_fetch->Done(true);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, PostLookupProxyFetchDone) {
  TestAddPostlookupTask(true, true);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, DonePostLookupProxyFetch) {
  TestAddPostlookupTask(false, true);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, ProxyFetchPostLookupDone) {
  TestAddPostlookupTask(true, false);
}

TEST_F(ProxyFetchPropertyCallbackCollectorTest, FallbackPagePostLookupRace) {
  EnableCollectorPrefix();
  // This test will check PostLookup tasks should not have Null
  // fallback_property_page.
  scoped_ptr<ProxyFetchPropertyCallbackCollector> collector;
  collector.reset(MakeCollector());
  collector->RequestHeadersComplete();
  ProxyFetchPropertyCallback* page_callback = AddCallback(
      collector.get(), ProxyFetchPropertyCallback::kPropertyCachePage);
  ExpectStringAsyncFetch async_fetch(
      true, RequestContext::NewTestRequestContext(thread_system_.get()));
  ProxyFetchFactory factory(server_context_);
  MockProxyFetch* mock_proxy_fetch = new MockProxyFetch(
      &async_fetch, &factory, server_context_);

  ThreadSystem* thread_system = server_context()->thread_system();
  QueuedWorkerPool pool(1, "test", thread_system);
  QueuedWorkerPool::Sequence* sequence = pool.NewSequence();
  WorkerTestBase::SyncPoint sync_point(thread_system);
  sequence->Add(
      MakeFunction(
          static_cast<ProxyFetchPropertyCallbackCollectorTest*>(this),
          &ProxyFetchPropertyCallbackCollectorTest::
              AddCheckPageNotNullPostLookupTask,
          collector.get(), &sync_point));
  page_callback->Done(true);
  sync_point.Wait();
  collector->ConnectProxyFetch(mock_proxy_fetch);
  mock_proxy_fetch->Done(true);
}


TEST_F(ProxyFetchPropertyCallbackCollectorTest, TestOptionsValid) {
  RewriteOptions* options = new RewriteOptions(thread_system_.get());
  ProxyFetchPropertyCallbackCollector* collector =
      new ProxyFetchPropertyCallbackCollector(
          server_context_,
          RewriteTestBase::kTestDomain,
          RequestContext::NewTestRequestContext(thread_system_.get()),
          options,
          UserAgentMatcher::kDesktop);
  ThreadSynchronizer* sync = server_context()->thread_synchronizer();
  sync->EnableForPrefix(ProxyFetch::kCollectorFinish);
  sync->EnableForPrefix(ProxyFetch::kCollectorDetachStart);
  sync->EnableForPrefix(ProxyFetch::kCollectorRequestHeadersCompleteFinish);
  collector->RequestHeadersComplete();
  ProxyFetchPropertyCallback* page_callback = AddCallback(
      collector, ProxyFetchPropertyCallback::kPropertyCachePage);

  collector->Detach(HttpStatus::kUnknownStatusCode);
  delete options;
  collector->IsCacheValid(1L);
  sync->Signal(ProxyFetch::kCollectorDetachStart);
  page_callback->Done(true);
  sync->Wait(ProxyFetch::kCollectorFinish);
}

TEST_F(ProxyFetchTest, TestStickyPageSpeedOptions) {
  // The various options we set as cookies.
  StringPieceVector pagespeed_options;
  pagespeed_options.push_back("PageSpeedImageRecompressionQuality=77");
  pagespeed_options.push_back("ModPagespeedImageLimitOptimizedPercent=55");
  pagespeed_options.push_back("ModPagespeedCssInlineMaxBytes=19");
  GoogleString cookies = JoinCollection(pagespeed_options, "&");
  // Add the sticky query param as a QP to show that it's not set as a cookie.
  GoogleString params = StrCat("PageSpeedStickyQueryParameters=on&", cookies);
  // For producing the expected response headers cookies string.
  const char kCookiesPrefix[] = "[\"";
  const char kCookieSetExpires[] = "; Expires=Tue, 02 Feb 2010 19:01:26 GMT";
  const char kCookieClearExpires[] = "; Expires=Thu, 01 Jan 1970 00:00:00 GMT";
  const char kCookieAttrs[] = "; Domain=www.google.com; Path=/; HttpOnly";
  const char kCookiesSeparator[] = "\",\"";
  const char kCookiesSuffix[] = "\"]";
  GoogleString current_cookies =
      StrCat(kCookiesPrefix,
             JoinCollection(pagespeed_options, StrCat(kCookieSetExpires,
                                                      kCookieAttrs,
                                                      kCookiesSeparator)),
             kCookieSetExpires, kCookieAttrs, kCookiesSuffix);
  GoogleString expired_cookies =
      StrCat(kCookiesPrefix,
             JoinCollection(pagespeed_options, StrCat(kCookieClearExpires,
                                                      kCookieAttrs,
                                                      kCookiesSeparator)),
             kCookieClearExpires, kCookieAttrs, kCookiesSuffix);
  // Strip out the values from the expiration cookies. Pardon the hackiness.
  GlobalReplaceSubstring("=77;", ";", &expired_cookies);
  GlobalReplaceSubstring("=55;", ";", &expired_cookies);
  GlobalReplaceSubstring("=19;", ";", &expired_cookies);

  // 0. Allow options to be set by cookie but don't allow them to be sticky.
  {
    ManagedMockProxyFetch managed_mock(server_context_, true, "", "right value",
                                       params, "");
    MockProxyFetch* mock_proxy_fetch = managed_mock.mock_proxy_fetch();
    GoogleString response_cookies;
    EXPECT_FALSE(mock_proxy_fetch->response_headers()->GetCookieString(
        &response_cookies));
    EXPECT_STREQ("", response_cookies);
  }
  // 1. Allow options to be set by cookie, allow them to be sticky, but don't
  //    ask for stickiness in the request.
  {
    ManagedMockProxyFetch managed_mock(server_context_, true, "right value", "",
                                       params, "");
    MockProxyFetch* mock_proxy_fetch = managed_mock.mock_proxy_fetch();
    GoogleString response_cookies;
    EXPECT_FALSE(mock_proxy_fetch->response_headers()->GetCookieString(
        &response_cookies));
    EXPECT_STREQ("", response_cookies);
  }
  // 2. Don't allow options to be set by cookie, allow them to be made sticky,
  //    don't ask for stickiness in the request, but pass in some already-set
  //    cookies - these should be expired for us.
  {
    ManagedMockProxyFetch managed_mock(server_context_, false, "right value",
                                       "", "", cookies);
    MockProxyFetch* mock_proxy_fetch = managed_mock.mock_proxy_fetch();
    GoogleString response_cookies;
    EXPECT_TRUE(mock_proxy_fetch->response_headers()->GetCookieString(
        &response_cookies));
    EXPECT_STREQ(expired_cookies, response_cookies);
  }
  // 3. Allow options to be set by cookie, allow them to be made sticky, and
  //    ask for stickiness in the request.
  {
    ManagedMockProxyFetch managed_mock(server_context_, true, "right value",
                                       "right value", params, "");
    MockProxyFetch* mock_proxy_fetch = managed_mock.mock_proxy_fetch();
    GoogleString response_cookies;
    EXPECT_TRUE(mock_proxy_fetch->response_headers()->GetCookieString(
        &response_cookies));
    EXPECT_STREQ(current_cookies, response_cookies);
  }
  // 4. Pass in some options-as-cookies without any query params or changes.
  //    We expect no cookies in the response as the browser has them already.
  {
    ManagedMockProxyFetch managed_mock(server_context_, true, "right value", "",
                                       "", cookies);
    MockProxyFetch* mock_proxy_fetch = managed_mock.mock_proxy_fetch();
    GoogleString response_cookies;
    EXPECT_FALSE(mock_proxy_fetch->response_headers()->GetCookieString(
        &response_cookies));
    EXPECT_STREQ("", response_cookies);
  }
  // 5. Explicitly ask for sticky options-by-cookies to be cleared.
  {
    ManagedMockProxyFetch managed_mock(server_context_, true, "right value",
                                       "different value", "", cookies);
    MockProxyFetch* mock_proxy_fetch = managed_mock.mock_proxy_fetch();
    GoogleString response_cookies;
    EXPECT_TRUE(mock_proxy_fetch->response_headers()->GetCookieString(
        &response_cookies));
    EXPECT_STREQ(expired_cookies, response_cookies);
  }
}

}  // namespace net_instaweb
