/*
 * 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 "net/instaweb/rewriter/public/critical_images_beacon_filter.h"

#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/public/global_constants.h"
#include "net/instaweb/rewriter/public/beacon_critical_images_finder.h"
#include "net/instaweb/rewriter/public/critical_images_finder.h"
#include "net/instaweb/rewriter/public/lazyload_images_filter.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 "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/util/public/mock_property_page.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/escaping.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_hash.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/user_agent_matcher_test_base.h"
#include "pagespeed/opt/logging/enums.pb.h"

using testing::HasSubstr;
using testing::Not;

namespace {

const char kChefGifFile[] = "IronChef2.gif";
// Set image dimensions such that image will be inlined.
const char kChefGifDims[] = "width=48 height=64";

const char kRequestUrl[] = "http://www.example.com";

}  // namespace

namespace net_instaweb {

class CriticalImagesBeaconFilterTest : public RewriteTestBase {
 protected:
  CriticalImagesBeaconFilterTest() {}

  virtual void SetUp() {
    options()->set_beacon_url("http://example.com/beacon");
    CriticalImagesBeaconFilter::InitStats(statistics());
    // Enable a filter that uses critical images, which in turn will enable
    // beacon insertion.
    factory()->set_use_beacon_results_in_filters(true);
    options()->EnableFilter(RewriteOptions::kDelayImages);
    RewriteTestBase::SetUp();
    https_mode_ = false;
    // Setup the property cache. The DetermineEnable logic for the
    // CriticalImagesBeaconFinder will only inject the beacon if the property
    // cache is enabled, since beaconed results are intended to be stored in the
    // pcache.
    PropertyCache* pcache = page_property_cache();
    server_context_->set_enable_property_cache(true);
    const PropertyCache::Cohort* beacon_cohort =
        SetupCohort(pcache, RewriteDriver::kBeaconCohort);
    const PropertyCache::Cohort* dom_cohort =
        SetupCohort(pcache, RewriteDriver::kDomCohort);
    server_context()->set_beacon_cohort(beacon_cohort);
    server_context()->set_dom_cohort(dom_cohort);

    server_context()->set_critical_images_finder(
        new BeaconCriticalImagesFinder(
            beacon_cohort, factory()->nonce_generator(), statistics()));

    GoogleUrl base(GetTestUrl());
    image_gurl_.Reset(base, kChefGifFile);
    ResetDriver();
  }

  void ResetDriver() {
    rewrite_driver()->Clear();
    SetCurrentUserAgent(UserAgentMatcherTestBase::kChrome18UserAgent);
    rewrite_driver()->set_request_context(
        RequestContext::NewTestRequestContext(factory()->thread_system()));
    MockPropertyPage* page = NewMockPage(kRequestUrl);
    rewrite_driver()->set_property_page(page);
    PropertyCache* pcache = server_context_->page_property_cache();
    pcache->Read(page);
  }

  void WriteToPropertyCache() {
    rewrite_driver()->property_page()->WriteCohort(
        server_context()->beacon_cohort());
  }

  void PrepareInjection() {
    rewrite_driver()->AddFilters();
    AddFileToMockFetcher(image_gurl_.Spec(), kChefGifFile,
                         kContentTypeJpeg, 100);
  }

  void AddImageTags(GoogleString* html) {
    // Add the relative image URL.
    StrAppend(html, "<img src=\"", kChefGifFile, "\" ", kChefGifDims, ">");
    // Add the absolute image URL.
    StrAppend(html, "<img src=\"", image_gurl_.Spec(), "\" ",
              kChefGifDims, ">");
  }

  void RunInjection() {
    PrepareInjection();
    SetupAndProcessUrl();
  }

  void SetupAndProcessUrl() {
    GoogleString html = "<head></head><body>";
    AddImageTags(&html);
    StrAppend(&html, "</body>");
    ParseUrl(GetTestUrl(), html);
  }

  void RunInjectionNoBody() {
    // As above, but we omit <head> and (more relevant) <body> tags.  We should
    // still inject the script at the end of the document.  The filter used to
    // get this wrong.
    PrepareInjection();
    GoogleString html;
    AddImageTags(&html);
    ParseUrl(GetTestUrl(), html);
  }

  void VerifyInjection(int expected_beacon_count) {
    EXPECT_EQ(expected_beacon_count, statistics()->GetVariable(
        CriticalImagesBeaconFilter::kCriticalImagesBeaconAddedCount)->Get());
    EXPECT_THAT(output_buffer_, HasSubstr(CreateInitString()));
  }

  void VerifyNoInjection(int expected_beacon_count) {
    EXPECT_EQ(expected_beacon_count, statistics()->GetVariable(
        CriticalImagesBeaconFilter::kCriticalImagesBeaconAddedCount)->Get());
    EXPECT_THAT(output_buffer_, Not(HasSubstr("pagespeed.CriticalImages.Run")));
  }

  void VerifyWithNoImageRewrite() {
    const GoogleString hash_str = ImageUrlHash(kChefGifFile);
    EXPECT_THAT(output_buffer_,
                HasSubstr(StrCat("data-pagespeed-url-hash=\"", hash_str)));
  }

  void AssumeHttps() {
    https_mode_ = true;
  }

  GoogleString GetTestUrl() {
    return StrCat((https_mode_ ? "https://example.com/" : kTestDomain),
                  "index.html?a&b");
  }

  GoogleString ImageUrlHash(StringPiece url) {
    // Absolutify the URL before hashing.
    unsigned int hash_val = HashString<CasePreserve, unsigned int>(
        image_gurl_.spec_c_str(), strlen(image_gurl_.spec_c_str()));
    return UintToString(hash_val);
  }


  GoogleString CreateInitString() {
    GoogleString url;
    EscapeToJsStringLiteral(rewrite_driver()->google_url().Spec(), false, &url);
    StringPiece beacon_url = https_mode_ ? options()->beacon_url().https :
        options()->beacon_url().http;
    GoogleString options_signature_hash =
        rewrite_driver()->server_context()->hasher()->Hash(
            rewrite_driver()->options()->signature());
    bool lazyload_will_run_beacon =
        rewrite_driver()->options()->Enabled(RewriteOptions::kLazyloadImages) &&
        LazyloadImagesFilter::ShouldApply(rewrite_driver()) ==
            RewriterHtmlApplication::ACTIVE;
    GoogleString str = "pagespeed.CriticalImages.Run(";
    StrAppend(&str, "'", beacon_url, "',");
    StrAppend(&str, "'", url, "',");
    StrAppend(&str, "'", options_signature_hash, "',");
    StrAppend(&str, BoolToString(!lazyload_will_run_beacon), ",");
    StrAppend(&str, BoolToString(rewrite_driver()->options()->Enabled(
                        RewriteOptions::kResizeToRenderedImageDimensions)),
              ",");
    StrAppend(&str, "'", ExpectedNonce(), "');");
    return str;
  }

  bool https_mode_;
  GoogleUrl image_gurl_;
};

TEST_F(CriticalImagesBeaconFilterTest, ScriptInjection) {
  RunInjection();
  VerifyInjection(1);

  // Verify that image onload criticality check has been added.
  int img_begin = output_buffer_.find("IronChef2");
  EXPECT_TRUE(img_begin != GoogleString::npos);
  int img_end = output_buffer_.substr(img_begin).find(">");
  EXPECT_TRUE(img_end != GoogleString::npos);
  EXPECT_TRUE(output_buffer_.substr(img_begin, img_end).find(
      "onload=\"pagespeed.CriticalImages."
      "checkImageForCriticality(this);\"") !=
      GoogleString::npos);
  VerifyWithNoImageRewrite();
}

TEST_F(CriticalImagesBeaconFilterTest, ScriptInjectionNoBody) {
  RunInjectionNoBody();
  VerifyInjection(1);
  VerifyWithNoImageRewrite();
}

TEST_F(CriticalImagesBeaconFilterTest, ScriptInjectionWithHttps) {
  AssumeHttps();
  RunInjection();
  VerifyInjection(1);
  VerifyWithNoImageRewrite();
}

TEST_F(CriticalImagesBeaconFilterTest, ScriptInjectionWithImageInlining) {
  // Verify that the URL hash is applied to the absolute image URL, and not to
  // the rewritten URL. In this case, make sure that an image inlined to a data
  // URI has the correct hash. We need to add the image hash to the critical
  // image set to make sure that the image is inlined.
  GoogleString hash_str = ImageUrlHash(kChefGifFile);
  StringSet* crit_img_set = server_context()->critical_images_finder()->
      mutable_html_critical_images(rewrite_driver());
  crit_img_set->insert(hash_str);
  options()->set_image_inline_max_bytes(10000);
  options()->EnableFilter(RewriteOptions::kResizeImages);
  options()->EnableFilter(RewriteOptions::kResizeToRenderedImageDimensions);
  options()->EnableFilter(RewriteOptions::kInlineImages);
  options()->EnableFilter(RewriteOptions::kInsertImageDimensions);
  options()->EnableFilter(RewriteOptions::kConvertGifToPng);
  options()->DisableFilter(RewriteOptions::kDelayImages);
  RunInjection();
  VerifyInjection(1);

  EXPECT_TRUE(output_buffer_.find("data:") != GoogleString::npos);
  EXPECT_TRUE(output_buffer_.find(hash_str) != GoogleString::npos);
  EXPECT_EQ(-1, logging_info()->num_html_critical_images());
  EXPECT_EQ(-1, logging_info()->num_css_critical_images());
}

TEST_F(CriticalImagesBeaconFilterTest, NoScriptInjectionWithNoScript) {
  PrepareInjection();
  GoogleString html = "<head></head><body><noscript>";
  AddImageTags(&html);
  StrAppend(&html, "</noscript></body>");
  ParseUrl(GetTestUrl(), html);
  VerifyNoInjection(0);
  VerifyWithNoImageRewrite();
}

TEST_F(CriticalImagesBeaconFilterTest, DontRebeaconBeforeTimeout) {
  RunInjection();
  VerifyInjection(1);
  VerifyWithNoImageRewrite();

  // Write a dummy value to the property cache.
  WriteToPropertyCache();

  // No beacon injection happens on the immediately succeeding request.
  ResetDriver();
  SetDriverRequestHeaders();
  SetupAndProcessUrl();
  VerifyNoInjection(1);

  // Beacon injection happens when the pcache value expires or when the
  // reinstrumentation time interval is exceeded.
  factory()->mock_timer()->AdvanceMs(
      options()->beacon_reinstrument_time_sec() * 1000);
  ResetDriver();
  SetDriverRequestHeaders();
  SetupAndProcessUrl();
  VerifyInjection(2);
}

TEST_F(CriticalImagesBeaconFilterTest, BeaconReinstrumentationWithHeader) {
  RunInjection();
  VerifyInjection(1);
  VerifyWithNoImageRewrite();

  // Write a dummy value to the property cache.
  WriteToPropertyCache();

  // Beacon injection happens when the PS-ShouldBeacon header is present even
  // when the pcache value has not expired and the reinstrumentation time
  // interval has not been exceeded.
  ResetDriver();
  SetDownstreamCacheDirectives("", "localhost:80", "random_rebeaconing_key");
  AddRequestAttribute(kPsaShouldBeacon, "random_rebeaconing_key");
  SetDriverRequestHeaders();
  SetupAndProcessUrl();
  VerifyInjection(2);
}

TEST_F(CriticalImagesBeaconFilterTest, UnsupportedUserAgent) {
  // Test that the filter is not applied for unsupported user agents.
  SetCurrentUserAgent("Firefox/1.0");
  RunInjection();
  VerifyNoInjection(0);
}

TEST_F(CriticalImagesBeaconFilterTest, Googlebot) {
  // Verify that the filter is not applied for bots.
  SetCurrentUserAgent(UserAgentMatcherTestBase::kGooglebotUserAgent);
  RunInjection();
  VerifyNoInjection(0);
}

// Verify that the init string is set correctly to not run the beacon's onload
// handler when lazyload is enabled. The lazyload JS will take care of running
// the beacon when all images have been loaded.
TEST_F(CriticalImagesBeaconFilterTest, LazyloadEnabled) {
  options()->EnableFilter(RewriteOptions::kLazyloadImages);
  // On the first page access, there will be no critical image data and lazyload
  // will be disabled.
  RunInjection();
  VerifyInjection(1);

  // Advance time to force re-beaconing.  Now there are extant non-critical
  // images, and lazyload ought to be enabled.
  factory()->mock_timer()->AdvanceMs(
      options()->beacon_reinstrument_time_sec() * 1000);
  ResetDriver();
  SetupAndProcessUrl();
  VerifyInjection(2);
}

}  // namespace net_instaweb
