/*
 * 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.
 */


#include "pagespeed/automatic/proxy_interface_test_base.h"

#include <cstddef>

#include "base/logging.h"
#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/mock_callback.h"
#include "net/instaweb/http/public/mock_url_fetcher.h"
#include "net/instaweb/rewriter/public/mock_critical_images_finder.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/util/public/cache_property_store.h"
#include "pagespeed/automatic/proxy_fetch.h"
#include "pagespeed/automatic/proxy_interface.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/cache/delay_cache.h"
#include "pagespeed/kernel/cache/lru_cache.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/http/google_url.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/http/user_agent_matcher.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
#include "pagespeed/kernel/thread/queued_worker_pool.h"
#include "pagespeed/kernel/thread/thread_synchronizer.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
#include "pagespeed/opt/logging/request_timing_info.h"

namespace net_instaweb {

namespace {

// Like ExpectStringAsyncFetch but for asynchronous invocation -- it lets
// one specify a WorkerTestBase::SyncPoint to help block until completion.
class AsyncExpectStringAsyncFetch : public ExpectStringAsyncFetch {
 public:
  AsyncExpectStringAsyncFetch(bool expect_success,
                              bool log_flush,
                              GoogleString* buffer,
                              ResponseHeaders* response_headers,
                              bool* done_value,
                              WorkerTestBase::SyncPoint* notify,
                              ThreadSynchronizer* sync,
                              const RequestContextPtr& request_context)
      : ExpectStringAsyncFetch(expect_success, request_context),
        buffer_(buffer),
        done_value_(done_value),
        notify_(notify),
        sync_(sync),
        log_flush_(log_flush) {
    buffer->clear();
    response_headers->Clear();
    *done_value = false;
    set_response_headers(response_headers);
  }

  virtual ~AsyncExpectStringAsyncFetch() {}

  virtual void HandleHeadersComplete() {
    // Make sure we have cleaned the headers in ProxyInterface.
    EXPECT_FALSE(
        request_headers()->Has(HttpAttributes::kAcceptEncoding));

    sync_->Wait(ProxyFetch::kHeadersSetupRaceWait);
    response_headers()->Add("HeadersComplete", "1");  // Dirties caching info.
    sync_->Signal(ProxyFetch::kHeadersSetupRaceFlush);
  }

  virtual void HandleDone(bool success) {
    *buffer_ = buffer();
    *done_value_ = success;
    ExpectStringAsyncFetch::HandleDone(success);
    WorkerTestBase::SyncPoint* notify = notify_;
    delete this;
    notify->Notify();
  }

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

 private:
  GoogleString* buffer_;
  bool* done_value_;
  WorkerTestBase::SyncPoint* notify_;
  ThreadSynchronizer* sync_;
  bool log_flush_;

  DISALLOW_COPY_AND_ASSIGN(AsyncExpectStringAsyncFetch);
};

}  // namespace

// ProxyUrlNamer.
const char ProxyUrlNamer::kProxyHost[] = "proxy_host.com";
bool ProxyUrlNamer::Decode(const GoogleUrl& gurl,
                           const RewriteOptions* rewrite_options,
                           GoogleString* decoded) const {
  if (gurl.Host() != kProxyHost) {
    return false;
  }
  StringPieceVector path_vector;
  SplitStringPieceToVector(gurl.PathAndLeaf(), "/", &path_vector, false);
  if (path_vector.size() < 3) {
    return false;
  }

  // [0] is "" because PathAndLeaf returns a string with a leading slash
  *decoded = StrCat(gurl.Scheme(), ":/");
  for (size_t i = 2, n = path_vector.size(); i < n; ++i) {
    StrAppend(decoded, "/", path_vector[i]);
  }
  return true;
}

// MockFilter.
void MockFilter::StartDocument() {
  num_elements_ = 0;
  PropertyCache* page_cache =
      driver_->server_context()->page_property_cache();
  const PropertyCache::Cohort* cohort =
      page_cache->GetCohort(RewriteDriver::kDomCohort);
  PropertyPage* page = driver_->property_page();
  if (page != NULL) {
    num_elements_property_ = page->GetProperty(cohort, "num_elements");
  } else {
    num_elements_property_ = NULL;
  }
}

void MockFilter::StartElement(HtmlElement* element) {
  if (num_elements_ == 0) {
    // Before the start of the first element, print out the number
    // of elements that we expect based on the cache.
    GoogleString comment = " ";
    PropertyCache* page_cache =
        driver_->server_context()->page_property_cache();

    if ((num_elements_property_ != NULL) &&
               num_elements_property_->has_value()) {
      StrAppend(&comment, num_elements_property_->value(),
                " elements ",
                page_cache->IsStable(num_elements_property_)
                ? "stable " : "unstable ");
    }
    HtmlNode* node = driver_->NewCommentNode(element->parent(), comment);
    driver_->InsertNodeBeforeCurrent(node);
  }
  ++num_elements_;
}

void MockFilter::EndDocument() {
  // We query IsBrowserCacheable for the HTML file only to ensure that
  // the test will crash if ComputeCaching() was never called.
  //
  // All these HTML responses are Cache-Control: private.
  EXPECT_TRUE(driver_->response_headers()->IsBrowserCacheable());
  PropertyPage* page = driver_->property_page();
  if (page != NULL) {
    page->UpdateValue(
        driver_->server_context()->dom_cohort(),
        "num_elements",
        IntegerToString(num_elements_));
    num_elements_property_ = NULL;
  }
}

// ProxyInterfaceTestBase.
ProxyInterfaceTestBase::ProxyInterfaceTestBase()
    : callback_done_value_(false),
      header_latency_ms_(0),
      mock_critical_images_finder_(
          new MockCriticalImagesFinder(statistics())) {
}

void ProxyInterfaceTestBase::TestHeadersSetupRace() {
  mock_url_fetcher()->SetResponseFailure(AbsolutifyUrl(kPageUrl));
  TestPropertyCache(kPageUrl, true, true, false);
}

void ProxyInterfaceTestBase::SetUp() {
  RewriteTestBase::SetUp();
  ThreadSynchronizer* sync = server_context()->thread_synchronizer();
  sync->EnableForPrefix(ProxyFetch::kCollectorFinish);
  sync->AllowSloppyTermination(ProxyFetch::kCollectorFinish);
  ProxyInterface::InitStats("test-", statistics());
  proxy_interface_.reset(new ProxyInterface(
      "test-", "localhost", 80, server_context(), statistics()));
  server_context()->set_critical_images_finder(
      mock_critical_images_finder_);
}

void ProxyInterfaceTestBase::TearDown() {
  // Make sure all the jobs are over before we check for leaks ---
  // someone might still be trying to clean themselves up.
  mock_scheduler()->AwaitQuiescence();
  EXPECT_EQ(0, server_context()->num_active_rewrite_drivers());
  RewriteTestBase::TearDown();
}

void ProxyInterfaceTestBase::SetCriticalImagesInFinder(
    StringSet* critical_images) {
  mock_critical_images_finder_->set_critical_images(critical_images);
}

void ProxyInterfaceTestBase::SetCssCriticalImagesInFinder(
    StringSet* css_critical_images) {
  mock_critical_images_finder_->set_css_critical_images(css_critical_images);
}

void ProxyInterfaceTestBase::FetchFromProxy(
    const StringPiece& url,
    const RequestHeaders& request_headers,
    bool expect_success,
    GoogleString* string_out,
    ResponseHeaders* headers_out,
    bool proxy_fetch_property_callback_collector_created) {
  FetchFromProxyNoWait(url, request_headers, expect_success,
                       false /* log_flush*/, headers_out);
  WaitForFetch(proxy_fetch_property_callback_collector_created);
  *string_out = callback_buffer_;
}

// Initiates a fetch using the proxy interface, and waits for it to
// complete.
void ProxyInterfaceTestBase::FetchFromProxy(
    const StringPiece& url,
    const RequestHeaders& request_headers,
    bool expect_success,
    GoogleString* string_out,
    ResponseHeaders* headers_out) {
  FetchFromProxy(url, request_headers, expect_success, string_out,
                 headers_out, true);
}

// TODO(jmarantz): eliminate this interface as it's annoying to have
// the function overload just to save an empty RequestHeaders arg.
void ProxyInterfaceTestBase::FetchFromProxy(const StringPiece& url,
                                            bool expect_success,
                                            GoogleString* string_out,
                                            ResponseHeaders* headers_out) {
  RequestHeaders request_headers;
  FetchFromProxy(url, request_headers, expect_success, string_out,
                 headers_out);
}

void ProxyInterfaceTestBase::FetchFromProxyLoggingFlushes(
    const StringPiece& url, bool expect_success, GoogleString* string_out) {
  RequestHeaders request_headers;
  ResponseHeaders response_headers;
  FetchFromProxyNoWait(url, request_headers, expect_success,
                       true /* log_flush*/, &response_headers);
  WaitForFetch(true);
  *string_out = callback_buffer_;
}

// Initiates a fetch using the proxy interface, without waiting for it to
// complete.  The usage model here is to delay callbacks and/or fetches
// to control their order of delivery, then call WaitForFetch.
void ProxyInterfaceTestBase::FetchFromProxyNoWait(
    const StringPiece& url,
    const RequestHeaders& request_headers,
    bool expect_success,
    bool log_flush,
    ResponseHeaders* headers_out) {
  sync_.reset(new WorkerTestBase::SyncPoint(
      server_context()->thread_system()));
  request_context_.reset(CreateRequestContext());
  if (header_latency_ms_ != 0) {
    RequestTimingInfo* timing_info = mutable_timing_info();
    timing_info->FetchStarted();
    AdvanceTimeMs(header_latency_ms_);
    timing_info->FetchHeaderReceived();
  }
  AsyncFetch* fetch = new AsyncExpectStringAsyncFetch(
      expect_success, log_flush, &callback_buffer_,
      &callback_response_headers_, &callback_done_value_, sync_.get(),
      server_context()->thread_synchronizer(),
      request_context());
  fetch->set_response_headers(headers_out);
  fetch->request_headers()->CopyFrom(request_headers);
  proxy_interface_->Fetch(AbsolutifyUrl(url), message_handler(), fetch);
}

// This must be called after FetchFromProxyNoWait, once all of the required
// resources (fetches, cache lookups) have been released.
void ProxyInterfaceTestBase::WaitForFetch(
    bool proxy_fetch_property_callback_collector_created) {
  sync_->Wait();
  mock_scheduler()->AwaitQuiescence();
  if (proxy_fetch_property_callback_collector_created) {
    ThreadSynchronizer* thread_synchronizer =
        server_context()->thread_synchronizer();
    thread_synchronizer->Wait(ProxyFetch::kCollectorFinish);
  }
}

// Tests a single flow through the property-cache, optionally delaying or
// threading property-cache lookups, and using the ThreadSynchronizer to
// tease out race conditions.
//
// delay_pcache indicates that we will suspend the PropertyCache lookup
// until after the FetchFromProxy call.  This is used in the
// PropCacheNoWritesIfNonHtmlDelayedCache below, which tests the flow where
// we have already detached the ProxyFetchPropertyCallbackCollector before
// Done() is called.
//
// thread_pcache forces the property-cache to issue the lookup
// callback in a different thread.  This lets us reproduce a
// potential race conditions where a context switch in
// ProxyFetchPropertyCallbackCollector::Done() would lead to a
// double-deletion of the collector object.
void ProxyInterfaceTestBase::TestPropertyCache(const StringPiece& url,
                                               bool delay_pcache,
                                               bool thread_pcache,
                                               bool expect_success) {
  RequestHeaders request_headers;
  ResponseHeaders response_headers;
  GoogleString output;
  TestPropertyCacheWithHeadersAndOutput(
      url, delay_pcache, thread_pcache, expect_success, true, true, false,
      request_headers, &response_headers, &output);
}

void ProxyInterfaceTestBase::TestPropertyCacheWithHeadersAndOutput(
    const StringPiece& url, bool delay_pcache, bool thread_pcache,
    bool expect_success, bool check_stats, bool add_create_filter_callback,
    bool expect_detach_before_pcache, const RequestHeaders& request_headers,
    ResponseHeaders* response_headers, GoogleString* output) {
  scoped_ptr<QueuedWorkerPool> pool;
  QueuedWorkerPool::Sequence* sequence = NULL;

  GoogleString delay_pcache_key, delay_http_cache_key;
  if (delay_pcache || thread_pcache) {
    PropertyCache* pcache = page_property_cache();
    const PropertyCache::Cohort* cohort =
        pcache->GetCohort(RewriteDriver::kDomCohort);
    delay_http_cache_key = AbsolutifyUrl(url);
    delay_pcache_key = factory()->cache_property_store()->CacheKey(
        delay_http_cache_key,
        "",
        UserAgentMatcher::DeviceTypeSuffix(UserAgentMatcher::kDesktop),
        cohort);
    delay_cache()->DelayKey(delay_pcache_key);
    if (thread_pcache) {
      delay_cache()->DelayKey(delay_http_cache_key);
      pool.reset(new QueuedWorkerPool(
          1, "pcache", server_context()->thread_system()));
      sequence = pool->NewSequence();
    }
  }

  CreateFilterCallback create_filter_callback;
  if (add_create_filter_callback) {
    factory()->AddCreateFilterCallback(&create_filter_callback);
  }

  if (thread_pcache) {
    FetchFromProxyNoWait(url, request_headers, expect_success,
                         false /* don't log flushes*/, response_headers);
    delay_cache()->ReleaseKeyInSequence(delay_pcache_key, sequence);

    // Now release the HTTPCache lookup, which allows the mock-fetch
    // to stream the bytes in the ProxyFetch and call HandleDone().
    // Note that we release this key in mainline, so that call
    // sequence happens directly from ReleaseKey.
    delay_cache()->ReleaseKey(delay_http_cache_key);

    WaitForFetch(true);
    *output = callback_buffer_;
    pool->ShutDown();
  } else {
    FetchFromProxyNoWait(url, request_headers, expect_success, false,
                         response_headers);
    if (expect_detach_before_pcache) {
      WaitForFetch(false);
    }
    if (delay_pcache) {
      delay_cache()->ReleaseKey(delay_pcache_key);
    }
    if (!expect_detach_before_pcache) {
      WaitForFetch(false);
    }
    ThreadSynchronizer* thread_synchronizer =
        server_context()->thread_synchronizer();
    thread_synchronizer->Wait(ProxyFetch::kCollectorFinish);
    *output = callback_buffer_;
  }

  if (check_stats) {
    EXPECT_EQ(1, lru_cache()->num_inserts());  // http-cache
    // We expect 2 misses. 1 for http-cache and 1 for prop-cache which
    // correspond to each different device type in
    // UserAgentMatcher::DeviceType.
    EXPECT_EQ(2, lru_cache()->num_misses());
  }
}

RequestContextPtr ProxyInterfaceTestBase::request_context() {
  CHECK(request_context_.get() != NULL);
  return request_context_;
}

}  // namespace net_instaweb
