/*
 * 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/css_summarizer_base.h"

#include "net/instaweb/rewriter/public/css_minify.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/gtest.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/string_writer.h"
#include "pagespeed/kernel/html/html_name.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/html/html_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/semantic_type.h"

namespace net_instaweb {

namespace {

const char kExpectedResult[] =
    "OK/*{display:|OK/div{displa/rel=stylesheet|ParseOrCloseStyleTagError//"
    "rel=stylesheet|ParseOrCloseStyleTagError//rel=stylesheet|"
    "ParseOrCloseStyleTagError//rel=stylesheet|FetchError//rel=stylesheet|"
    "ResourceError/|";

// Extracts first 10 characters of minified form of every stylesheet.
class MinifyExcerptFilter : public CssSummarizerBase {
 public:
  explicit MinifyExcerptFilter(RewriteDriver* driver)
      : CssSummarizerBase(driver),
        render_summaries_in_place_(false),
        will_not_render_summaries_in_place_(false),
        include_base_(false) {}

  virtual const char* Name() const { return "Minify10"; }
  virtual const char* id() const { return "csr"; }

  virtual bool MustSummarize(HtmlElement* element) const {
    return (!element->FindAttribute(HtmlName::kDataPagespeedNoDefer));
  }

  virtual void Summarize(Css::Stylesheet* stylesheet,
                         GoogleString* out) const {
    StringWriter write_out(out);
    CssMinify::Stylesheet(*stylesheet, &write_out, driver()->message_handler());
    if (out->length() > 10) {
      out->resize(10);
    }
  }

  GoogleString EncodeState(SummaryState state) {
    switch (state) {
      case kSummaryOk:
        return "OK";
      case kSummaryStillPending:
        return "Pending";
      case kSummaryCssParseError:
        return "ParseOrCloseStyleTagError";
      case kSummaryResourceCreationFailed:
        return "ResourceError";
      case kSummaryInputUnavailable:
        return "FetchError";
      case kSummarySlotRemoved:
        return "SlotRemoved";
    }
  }

  virtual void RenderSummary(int pos,
                             HtmlElement* element,
                             HtmlCharactersNode* char_node,
                             bool* is_element_deleted) {
    if (!render_summaries_in_place_) {
      return;
    }

    const SummaryInfo& summary = GetSummaryForStyle(pos);

    if (char_node != NULL) {
      *char_node->mutable_contents() = summary.data;
    } else {
      // Replace link with style. Note: real one should also keep media,
      // test code does not have to.
      HtmlElement* style_element = driver()->NewElement(NULL, HtmlName::kStyle);
      driver()->InsertNodeBeforeNode(element, style_element);

      HtmlCharactersNode* content =
          driver()->NewCharactersNode(style_element, summary.data);
      driver()->AppendChild(style_element, content);
      EXPECT_TRUE(driver()->DeleteNode(element));
      *is_element_deleted = true;
    }
  }

  void WillNotRenderSummary(int pos,
                            HtmlElement* element,
                            HtmlCharactersNode* char_node) override {
    // Note that these should not normally mutate the DOM, we only
    // get away with this because the tests we use this in don't really do
    // any flushing.
    if (!will_not_render_summaries_in_place_) {
      return;
    }

    const SummaryInfo& sum = GetSummaryForStyle(pos);
    GoogleString annotation = StrCat("WillNotRender:", IntegerToString(pos),
                                     " --- ", EncodeState(sum.state));
    driver()->InsertNodeBeforeNode(
        element, driver()->NewCommentNode(NULL, annotation));
  }

  virtual void SummariesDone() {
    result_.clear();
    for (int i = 0; i < NumStyles(); ++i) {
      const SummaryInfo& sum = GetSummaryForStyle(i);
      StrAppend(&result_, EncodeState(sum.state), "/", sum.data,
                (sum.is_inside_noscript ? "/noscr" : ""),
                (sum.rel.empty() ? "" : StrCat("/rel=", sum.rel)),
                (include_base_ ? StrCat("/base=", sum.base) : ""),
                "|");
    }
    InsertNodeAtBodyEnd(driver()->NewCommentNode(NULL, result_));
  }

  const GoogleString& result() { return result_; }

  // Whether we should note the RenderSummary calls in place.
  void set_render_summaries_in_place(bool x) {
    render_summaries_in_place_ = x;
  }

  // Whether we should note the WillNotRenderSummary calls in place.
  void set_will_not_render_summaries_in_place(bool x) {
    will_not_render_summaries_in_place_ = x;
  }

  // Whether we should include the base URL in the output string we compute.
  void set_include_base(bool x) {
    include_base_ = x;
  }

 private:
  GoogleString result_;
  bool render_summaries_in_place_;
  bool will_not_render_summaries_in_place_;
  bool include_base_;
};

class CssSummarizerBaseTest : public RewriteTestBase {
 public:
  CssSummarizerBaseTest()
      : head_(StrCat("<html>\n",
                     "<style>* {display: none; }</style>",
                     CssLinkHref("a.css"),  // ok
                     CssLinkHref("b.css"),  // parse error
                     CssLinkHref("c.css"),  // parse error due to bad URL
                     CssLinkHref("close_style_tag.css"),  // closing style tag
                     CssLinkHref("404.css"),  // fetch error
                     CssLinkHref("http://evil.com/d.css"))) { }
  virtual ~CssSummarizerBaseTest() { }

 protected:
  virtual void SetUp() {
    RewriteTestBase::SetUp();
    rewrite_driver()->AddFilters();
    filter_ = new MinifyExcerptFilter(rewrite_driver());
    rewrite_driver()->AppendOwnedPreRenderFilter(filter_);
    server_context()->ComputeSignature(options());

    // Valid CSS.
    SetResponseWithDefaultHeaders("a.css", kContentTypeCss,
                                  "div { display: block; }", 100);

    // Parse error.
    SetResponseWithDefaultHeaders("b.css", kContentTypeCss,
                                  "div { ", 100);
    SetResponseWithDefaultHeaders(
        "c.css", kContentTypeCss,
        ".z{background-image:url(\"</style>\");", 100);

    // Contents that include a closing style tag.
    SetResponseWithDefaultHeaders("close_style_tag.css",
                                  kContentTypeCss,
                                  ".x </style> {color: white }", 100);

    // Permit testing a 404.
    SetFetchFailOnUnexpected(false);

    // An inline div? evil indeed.
    SetResponseWithDefaultHeaders("http://evil.com/d.css", kContentTypeCss,
                                  "div { display: inline; }", 100);
  }

  void StartTest(StringPiece name, StringPiece pre_comment) {
    SetupWriter();
    GoogleString url = StrCat(kTestDomain, name);
    rewrite_driver()->StartParse(url);
    rewrite_driver()->ParseText(head_);
    rewrite_driver()->ParseText(pre_comment);
  }

  const GoogleString FinishTest(
      StringPiece pre_comment, StringPiece post_comment) {
    const GoogleString expected_html = StrCat(
        head_, pre_comment, "<!--", kExpectedResult, "-->", post_comment);
    rewrite_driver()->ParseText(post_comment);
    rewrite_driver()->FinishParse();
    return expected_html;
  }

  const GoogleString FullTest(
      StringPiece name, StringPiece pre_comment, StringPiece post_comment) {
    StartTest(name, pre_comment);
    return FinishTest(pre_comment, post_comment);
  }

  const GoogleString FlushTest(
      StringPiece name, StringPiece pre_flush,
      StringPiece pre_comment, StringPiece post_comment) {
    StartTest(name, pre_flush);
    rewrite_driver()->Flush();
    rewrite_driver()->ParseText(pre_comment);
    GoogleString full_pre_comment = StrCat(pre_flush, pre_comment);
    return FinishTest(full_pre_comment, post_comment);
  }

  void VerifyUnauthNotRendered(StringPiece summary_comment) {
    FullTest("will_not_render", "", "");
    EXPECT_STREQ(
        StrCat("<html>\n"
               "<style>* {display: none; }</style>",
               CssLinkHref("a.css"),
               StrCat("<!--WillNotRender:2 --- ParseOrCloseStyleTagError-->",
                       CssLinkHref("b.css"),
                      "<!--WillNotRender:3 --- ParseOrCloseStyleTagError-->",
                       CssLinkHref("c.css")),
               StrCat("<!--WillNotRender:4 --- ParseOrCloseStyleTagError-->",
                       CssLinkHref("close_style_tag.css"),
                      "<!--WillNotRender:5 --- FetchError-->",
                       CssLinkHref("404.css")),
               StrCat("<!--WillNotRender:6 --- ResourceError-->",
                       CssLinkHref("http://evil.com/d.css")),
               summary_comment,
               StrCat("<!--", kExpectedResult, "-->")),
        output_buffer_);
  }

  MinifyExcerptFilter* filter_;  // owned by the driver;
  const GoogleString head_;

 private:
  DISALLOW_COPY_AND_ASSIGN(CssSummarizerBaseTest);
};

TEST_F(CssSummarizerBaseTest, BasicOperation) {
  GoogleString expected =
      FullTest("basic", "<body> <p>some content</p> ", "</body></html>");
  EXPECT_STREQ(expected, output_buffer_);
  EXPECT_STREQ(kExpectedResult, filter_->result());

  // Re-test to make sure we behave OK with the result cached.
  expected = FullTest("basic", "<body> <p>some content</p> ", "</body></html>");
  EXPECT_STREQ(expected, output_buffer_);
  EXPECT_STREQ(kExpectedResult, filter_->result());
}

TEST_F(CssSummarizerBaseTest, RenderSummary) {
  filter_->set_render_summaries_in_place(true);
  Parse("link", StrCat(CssLinkHref("a.css"),
                       "<style>* { background: blue; }</style>"));
  EXPECT_STREQ("<html>\n<style>div{displa</style><style>*{backgrou</style>"
               "<!--OK/div{displa/rel=stylesheet|"
                   "OK/*{backgrou|--></html>", output_buffer_);
}

TEST_F(CssSummarizerBaseTest, WillNotRenderSummary) {
  filter_->set_will_not_render_summaries_in_place(true);
  VerifyUnauthNotRendered(/* summary_comment= */ "");
}

TEST_F(CssSummarizerBaseTest, WillNotRenderSummaryWithUnauthEnabled) {
  filter_->set_will_not_render_summaries_in_place(true);
  options()->ClearSignatureForTesting();
  options()->AddInlineUnauthorizedResourceType(semantic_type::kStylesheet);
  server_context()->ComputeSignature(options());
  VerifyUnauthNotRendered(/* summary_comment= */ "");
}

TEST_F(CssSummarizerBaseTest, WillNotRenderSummaryWithDebug) {
  filter_->set_will_not_render_summaries_in_place(true);
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kDebug);
  server_context()->ComputeSignature(options());
  const char kDebugSummary[] =
      "<!--Summary computation status for Minify10\n"
      "Resource 0 http://test.com/will_not_render:2: Computed OK\n"
      "Resource 1 http://test.com/a.css: Computed OK\n"
      "Resource 2 http://test.com/b.css: "
      "Unrecoverable CSS parse error or resource contains closing style tag\n"
      "Resource 3 http://test.com/c.css: "
      "Unrecoverable CSS parse error or resource contains closing style tag\n"
      "Resource 4 http://test.com/close_style_tag.css: "
      "Unrecoverable CSS parse error or resource contains closing style tag\n"
      "Resource 5 http://test.com/404.css: "
      "Fetch failed or resource not publicly cacheable\n"
      "Resource 6 http://evil.com/d.css: Cannot create resource: either its "
      "domain is unauthorized and InlineUnauthorizedResources is not enabled, "
      "or it cannot be fetched (check the server logs)\n"
      "-->";
  VerifyUnauthNotRendered(kDebugSummary);
}

TEST_F(CssSummarizerBaseTest, WillNotRenderSummaryWait) {
  filter_->set_will_not_render_summaries_in_place(true);
  SetupWaitFetcher();
  Parse("link", CssLinkHref("a.css"));
  EXPECT_STREQ(StrCat("<html>\n",
                      "<!--WillNotRender:0 --- Pending-->",
                      CssLinkHref("a.css"),
                      "</html>"),
               output_buffer_);
  CallFetcherCallbacks();
}

TEST_F(CssSummarizerBaseTest, Base) {
  filter_->set_include_base(true);
  GoogleString css =
      StrCat(CssLinkHref("a.css"), "<style>*{display:block;}</style>");
  Parse("base", css);
  EXPECT_STREQ(
      StrCat("<html>\n", css,
             StrCat("<!--OK/div{displa/rel=stylesheet/base=",
                    kTestDomain, "a.css"),
             StrCat("|OK/*{display:/base=", kTestDomain, "base.html|-->"),
             "</html>"),
      output_buffer_);
}

TEST_F(CssSummarizerBaseTest, AlternateHandling) {
  // CssSummarizerBase itself handles alternate stylesheets, just keeps
  // the rel around inside the SummaryInfo
  Parse("alternate", "<link rel=\"stylesheet alternate\" href=\"a.css\">");
  EXPECT_STREQ("OK/div{displa/rel=stylesheet alternate|", filter_->result());
}

TEST_F(CssSummarizerBaseTest, NoScriptHandling) {
  Parse("ns", StrCat(CssLinkHref("a.css"),
                     "<noscript>", CssLinkHref("a.css"), "</noscript>"));
  EXPECT_STREQ("OK/div{displa/rel=stylesheet|"
                   "OK/div{displa/noscr/rel=stylesheet|",
               filter_->result());
}

TEST_F(CssSummarizerBaseTest, IgnoreNonSummarizable) {
  filter_->set_render_summaries_in_place(true);
  Parse("non-summarizable",
        "<style>* { background: blue; }</style>"
        "<style data-pagespeed-no-defer>div {display:none;}</style>"
        "<style scoped>p {display:none;}</style>"
        "<link rel=stylesheet href='b.css' data-pagespeed-no-defer>"
        "<link rel=stylesheet href='a.css'>");
  EXPECT_STREQ("<html>\n"
               "<style>*{backgrou</style>"
               "<style data-pagespeed-no-defer>div {display:none;}</style>"
               "<style scoped>p {display:none;}</style>"
               "<link rel=stylesheet href='b.css' data-pagespeed-no-defer>"
               "<style>div{displa</style>"
               "<!--OK/*{backgrou|OK/div{displa/rel=stylesheet|--></html>",
               output_buffer_);
}

class CssSummarizerBaseWithCombinerFilterTest : public CssSummarizerBaseTest {
 protected:
  virtual void SetUp() {
    options()->EnableFilter(RewriteOptions::kCombineCss);
    CssSummarizerBaseTest::SetUp();
    SetHtmlMimetype();  // no <link />, just <link>
  }
};

TEST_F(CssSummarizerBaseWithCombinerFilterTest, Interaction) {
  SetResponseWithDefaultHeaders("a2.css", kContentTypeCss,
                                 "span { display: inline; }", 100);
  GoogleString combined_url = Encode("", "cc", "0",
                                     MultiUrl("a.css", "a2.css"), "css");

  Parse("with_combine", StrCat(CssLinkHref("a.css"), CssLinkHref("a2.css")));
  EXPECT_EQ(StrCat("<html>\n", CssLinkHref(combined_url),
                   "<!--OK/div{displa/rel=stylesheet|"
                   "SlotRemoved//rel=stylesheet|--></html>"),
            output_buffer_);
}

TEST_F(CssSummarizerBaseWithCombinerFilterTest, InteractionWithFlush) {
  // Make sure that SummariesDone is called once only, at the actual end of the
  // document, and not for every flush window.
  SetResponseWithDefaultHeaders("a2.css", kContentTypeCss,
                                 "span { display: inline; }", 100);
  GoogleString combined_url = Encode("", "cc", "0",
                                     MultiUrl("a.css", "a2.css"), "css");
  GoogleString css = StrCat(CssLinkHref("a.css"), CssLinkHref("a2.css"));

  SetupWriter();
  html_parse()->StartParse(StrCat(kTestDomain, "example.html"));
  html_parse()->ParseText(css);
  html_parse()->Flush();
  html_parse()->ParseText(css);
  html_parse()->FinishParse();

  // Should only see the comment once, since SummariesDone is supposed to be
  // called only at document end.
  EXPECT_EQ(StrCat(CssLinkHref(combined_url), CssLinkHref(combined_url),
                   StrCat("<!--",
                          "OK/div{displa/rel=stylesheet|",
                          "SlotRemoved//rel=stylesheet|",
                          "OK/div{displa/rel=stylesheet|",
                          "SlotRemoved//rel=stylesheet|",
                          "-->")),
            output_buffer_);
}

TEST_F(CssSummarizerBaseWithCombinerFilterTest, BaseAcrossPaths) {
  // Make sure base is updated if a previous filter moves a resource across
  // directories.
  filter_->set_include_base(true);
  SetResponseWithDefaultHeaders("b/a2.css", kContentTypeCss,
                                 "span { display: inline; }", 100);
  GoogleString combined_url = "b,_a2.css+a.css.pagespeed.cc.0.css";

  Parse("base_accross_paths",
        StrCat(CssLinkHref("b/a2.css"), CssLinkHref("a.css")));
  EXPECT_EQ(StrCat(
      "<html>\n", CssLinkHref(combined_url),
      "<!--OK/span{displ/rel=stylesheet/base=", kTestDomain, combined_url,
      "|SlotRemoved//rel=stylesheet/base=", kTestDomain, "a.css"
      "|--></html>"),
            output_buffer_);
}

}  // namespace

}  // namespace net_instaweb
