/*
 * 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/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/gtest.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/opt/http/request_context.h"

namespace net_instaweb {

namespace {

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

class PushPreloadFilterTest : public RewriteTestBase {
 protected:
  void SetUp() override {
    RewriteTestBase::SetUp();
    options()->EnableFilter(RewriteOptions::kHintPreloadSubresources);

    // Setup pcache.
    pcache_ = rewrite_driver()->server_context()->page_property_cache();
    const PropertyCache::Cohort* deps_cohort =
        SetupCohort(pcache_, RewriteDriver::kDependenciesCohort);
    server_context()->set_dependencies_cohort(deps_cohort);
    ResetDriver();

    SetResponseWithDefaultHeaders("a.css", kContentTypeCss,
                                  " *  { display: block }", 100);
    SetResponseWithDefaultHeaders("b.js",  kContentTypeJavascript,
                                  " var b  = 42", 200);
  }

  void ResetDriver() {
    rewrite_driver()->Clear();
    rewrite_driver()->set_request_context(
        RequestContext::NewTestRequestContext(factory()->thread_system()));
    page_ = NewMockPage(kRequestUrl);
    rewrite_driver()->set_property_page(page_);
    pcache_->Read(page_);
    rewrite_driver()->PropertyCacheSetupDone();
    SetHtmlMimetype();  // Don't wrap scripts in <![CDATA[ ]]>
  }

  PropertyCache* pcache_;
  PropertyPage* page_;
};

TEST_F(PushPreloadFilterTest, WeirdTiming) {
  // Event buffering causes us to clear mutable_response_headers()
  // at first flush window even if we haven't yet even delivered StartDocument.
  // At the very least, that shouldn't cause us to crash.
  // TODO(morlovich): Discuss what the API should be with Josh.
  rewrite_driver()->AddFilters();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->Flush();
  rewrite_driver()->FinishParse();
}

TEST_F(PushPreloadFilterTest, BasicOperation) {
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver()->AddFilters();

  static const char kInput[] = "<link rel=stylesheet href=a.css>"
                               "<script src=b.js></script>";

  static const char kOutput[] =
    "<link rel=stylesheet href=A.a.css.pagespeed.cf.0.css>"
    "<script src=b.js.pagespeed.jm.0.js></script>";

  ValidateExpected("basic_res", kInput, kOutput);

  // Now that we've collected stuff, see if it produces proper headers.
  ResetDriver();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText("<!doctype html><html>");
  rewrite_driver()->Flush();   // Run filters
  ConstStringStarVector links;

  EXPECT_TRUE(rewrite_driver()->response_headers()->Lookup(
                  HttpAttributes::kLink, &links));
  rewrite_driver()->FinishParse();

  ASSERT_EQ(2, links.size());
  EXPECT_STREQ(
      "</A.a.css.pagespeed.cf.0.css>; rel=preload; as=style; nopush",
      *links[0]);
  EXPECT_STREQ(
      "</b.js.pagespeed.jm.0.js>; rel=preload; as=script; nopush",
      *links[1]);
}

TEST_F(PushPreloadFilterTest, Invalidation) {
  // Test for keeping track of us remembering when things expire.
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver()->AddFilters();

  // b.js expires in 200, a.css expires in 100.
  static const char kInput[] = "<script src=b.js></script>"
                               "<link rel=stylesheet href=a.css>";
  static const char kOutput[] =
    "<script src=b.js.pagespeed.jm.0.js></script>"
    "<link rel=stylesheet href=A.a.css.pagespeed.cf.0.css>";

  ValidateExpected("invalidation", kInput, kOutput);

  // Enough to invalidate a, but not b.
  AdvanceTimeMs(150 * 1000);
  ResetDriver();
  ValidateExpected("invalidation2", kInput, kOutput);

  ConstStringStarVector links;
  EXPECT_TRUE(rewrite_driver()->response_headers()->Lookup(
                  HttpAttributes::kLink, &links));

  // Only b.js should be pushed --- or rather the .pagespeed version.
  ASSERT_EQ(1, links.size());
  EXPECT_STREQ("</b.js.pagespeed.jm.0.js>; rel=preload; as=script; nopush",
               *links[0]);
}

TEST_F(PushPreloadFilterTest, Invalidation2) {
  // Test for keeping track of us remembering when things expire.
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver()->AddFilters();

  // b.js expires in 200, a.css expires in 100.
  static const char kInput[] = "<script src=b.js></script>"
                               "<link rel=stylesheet href=a.css>";
  static const char kOutput[] =
    "<script src=b.js.pagespeed.jm.0.js></script>"
    "<link rel=stylesheet href=A.a.css.pagespeed.cf.0.css>";

  ValidateExpected("invalidation", kInput, kOutput);

  // Enough to invalidate both a and b.
  AdvanceTimeMs(250 * 1000);
  ResetDriver();
  ValidateExpected("invalidation2", kInput, kOutput);

  ConstStringStarVector links;
  EXPECT_FALSE(rewrite_driver()->response_headers()->Lookup(
                   HttpAttributes::kLink, &links));
}

TEST_F(PushPreloadFilterTest, InvalidationOrder) {
  // Test for keeping track of us remembering when things expire.
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kRewriteJavascriptExternal);
  rewrite_driver()->AddFilters();

  // b.js expires in 200, a.css expires in 100.
  static const char kInput[] = "<link rel=stylesheet href=a.css>"
                               "<script src=b.js></script>";
  static const char kOutput[] =
    "<link rel=stylesheet href=A.a.css.pagespeed.cf.0.css>"
    "<script src=b.js.pagespeed.jm.0.js></script>";

  ValidateExpected("invalidation", kInput, kOutput);

  // Enough to invalidate a, but not b. However, since a is before b, nothing
  // will be hinted.
  AdvanceTimeMs(150 * 1000);
  ResetDriver();
  ValidateExpected("invalidation2", kInput, kOutput);

  ConstStringStarVector links;
  EXPECT_FALSE(rewrite_driver()->response_headers()->Lookup(
                   HttpAttributes::kLink, &links));
}

TEST_F(PushPreloadFilterTest, IndirectCollected) {
  SetResponseWithDefaultHeaders("c.css", kContentTypeCss,
                                "@import \"i1.css\" all;\n"
                                "@import \"i2.css\" print, screen;\n"
                                "@import \"i3.css\" print;         ", 100);
  SetResponseWithDefaultHeaders("d.css", kContentTypeCss,
                                "@import \"i1.css\" all;    \n"
                                "@import \"i4.css\";    ", 100);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  rewrite_driver()->AddFilters();

  const char kInput[] = "<link rel=stylesheet href=c.css>"
                        "<link rel=stylesheet href=d.css>";
  const char kOutput[] =
    "<link rel=stylesheet href=A.c.css.pagespeed.cf.0.css>"
    "<link rel=stylesheet href=A.d.css.pagespeed.cf.0.css>";

  ValidateExpected("basic_res", kInput, kOutput);

  // Now that we've collected stuff, see if it produces proper headers.
  ResetDriver();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText("<!doctype html><html>");
  rewrite_driver()->Flush();   // Run filters
  ConstStringStarVector links;

  EXPECT_TRUE(rewrite_driver()->response_headers()->Lookup(
                  HttpAttributes::kLink, &links));
  rewrite_driver()->FinishParse();

  ASSERT_EQ(5, links.size());
  // These should be in preorder wrt to the dependencies between
  // CSS and things in it
  EXPECT_STREQ(
        "</A.c.css.pagespeed.cf.0.css>; rel=preload; as=style; nopush",
        *links[0]);
  EXPECT_STREQ(
        "</i1.css>; rel=preload; as=style; nopush",
        *links[1]);
  EXPECT_STREQ(
        "</i2.css>; rel=preload; as=style; nopush",
        *links[2]);
  EXPECT_STREQ(
        "</A.d.css.pagespeed.cf.0.css>; rel=preload; as=style; nopush",
        *links[3]);
  // not i3, since it's print only.

  // i1 already hinted.
  // i4 isn't, though.
  EXPECT_STREQ("</i4.css>; rel=preload; as=style; nopush", *links[4]);
}

}  // namespace

}  // namespace net_instaweb

