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

#include "net/instaweb/rewriter/public/debug_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/support_noscript_filter.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/hasher.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/google_url.h"
#include "pagespeed/kernel/http/response_headers.h"

namespace net_instaweb {

namespace {

class CssOutlineFilterTest : public RewriteTestBase {
 protected:
  void SetupOutliner() {
    DisableGzip();
    options()->set_css_outline_min_bytes(0);
    options()->SoftEnableFilterForTesting(RewriteOptions::kOutlineCss);
    rewrite_driver()->AddFilters();
  }

  void SetupDebug(StringPiece debug_message) {
    options()->EnableFilter(RewriteOptions::kDebug);
    SetupOutliner();

    // For some reason SupportNoScript filter is disabled here.
    StringVector expected_disabled_filters;
    SupportNoscriptFilter support_noscript_filter(rewrite_driver());
    expected_disabled_filters.push_back(support_noscript_filter.Name());

    debug_message.CopyToString(&debug_message_);
    debug_suffix_ = DebugFilter::FormatEndDocumentMessage(
        0, 0, 0, 0, 0, false, StringSet(),
        expected_disabled_filters);
  }

  void TestOutlineCss(StringPiece html_url,
                      StringPiece base_ref,
                      StringPiece css_original_body,
                      bool expect_outline,
                      StringPiece css_rewritten_body,
                      // css_url_base only needed if different from html_url,
                      // e.g. domain rewriting.
                      StringPiece css_url_base) {
    // TODO(sligocki): Test with outline threshold > 0.

    // Figure out outline_url.
    GoogleString hash = hasher()->Hash(css_rewritten_body);
    GoogleUrl css_gurl_base;
    if (css_url_base.empty()) {
      css_gurl_base.Reset(html_url);
    } else {
      css_gurl_base.Reset(css_url_base);
    }
    GoogleUrl base_ref_gurl;
    if (base_ref.empty()) {
      base_ref_gurl.Reset(html_url);
    } else {
      base_ref_gurl.Reset(base_ref);
    }
    GoogleString css_gurl_base_origin = StrCat(css_gurl_base.Origin(), "/");
    GoogleString base_ref_gurl_origin = StrCat(base_ref_gurl.Origin(), "/");
    GoogleString outline_url = EncodeWithBase(base_ref_gurl_origin,
                                              css_gurl_base_origin,
                                              CssOutlineFilter::kFilterId,
                                              hash, "_", "css");
    // Add a base href to the HTML iff specified.
    GoogleString other_content;
    if (!base_ref.empty()) {
      other_content = StrCat("  <base href=\"", base_ref, "\">\n");
    }

    const GoogleString html_input = StrCat(
        "<head>\n",
        other_content,
        "  <style>", css_original_body, "</style>\n"
        "</head>\n"
        "<body>Hello, world!</body>");

    // Check output HTML.
    GoogleString expected_output;
    if (expect_outline) {
      expected_output = StrCat(
          "<head>\n",
          other_content,
          "  <link rel=\"stylesheet\" href=\"",  outline_url,  "\">\n"
          "</head>\n"
          "<body>Hello, world!</body>");
    } else {
      expected_output = StrCat(
          "<head>\n",
          other_content,
          "  <style>", css_original_body, "</style>", debug_message_, "\n"
          "</head>\n"
          "<body>Hello, world!</body>");
    }

    ParseUrl(html_url, html_input);
    EXPECT_HAS_SUBSTR(expected_output, output_buffer_);
    if (!debug_suffix_.empty()) {
      EXPECT_HAS_SUBSTR(debug_suffix_, output_buffer_);
    }

    if (expect_outline) {
      // Expected headers.
      GoogleString expected_headers;
      AppendDefaultHeaders(kContentTypeCss, &expected_headers);

      // Check fetched resource.
      GoogleString actual_outline;
      ResponseHeaders actual_headers;
      EXPECT_TRUE(FetchResourceUrl(outline_url, &actual_outline,
                                   &actual_headers));
      EXPECT_EQ(expected_headers, actual_headers.ToString());
      EXPECT_EQ(css_rewritten_body, actual_outline);
    }
  }

  void OutlineStyle(const StringPiece& id) {
    GoogleString html_url = StrCat("http://outline_style.test/", id, ".html");
    GoogleString style_text = "background_blue { background-color: blue; }\n"
                              "foreground_yellow { color: yellow; }\n";
    TestOutlineCss(html_url, "", style_text, true, style_text, "");
  }

  GoogleString debug_message_;
  GoogleString debug_suffix_;
};

// Tests for Outlining styles.
TEST_F(CssOutlineFilterTest, OutlineStyle) {
  SetupOutliner();
  OutlineStyle("outline_styles_no_hash");
}

TEST_F(CssOutlineFilterTest, OutlineStyleMD5) {
  SetupOutliner();
  UseMd5Hasher();
  OutlineStyle("outline_styles_md5");
}

TEST_F(CssOutlineFilterTest, CssOutlinePreserveURLsOn) {
  options()->set_css_preserve_urls(true);
  options()->set_css_outline_min_bytes(0);
  SetupOutliner();
  const char kStyleText[] = "background_blue { background-color: blue; }\n"
                            "foreground_yellow { color: yellow; }\n";
  TestOutlineCss("http://outline_style.test/outline_styles_md5.html", "",
                 kStyleText, false, "", "");
}


TEST_F(CssOutlineFilterTest, NoAbsolutifySameDir) {
  SetupOutliner();
  const GoogleString css = "body { background-image: url('bg.png'); }";
  TestOutlineCss("http://outline_style.test/index.html", "",
                 css, true, css, "");
}

TEST_F(CssOutlineFilterTest, AbsolutifyDifferentDir) {
  SetupOutliner();
  const GoogleString css1 = "body { background-image: url('bg.png'); }";
  const GoogleString css2 =
      "body { background-image: url('http://other_site.test/foo/bg.png'); }";
  TestOutlineCss("http://outline_style.test/index.html",
                 "http://other_site.test/foo/", css1, true, css2, "");
}

TEST_F(CssOutlineFilterTest, ShardSubresources) {
  SetupOutliner();
  UseMd5Hasher();
  AddShard("outline_style.test", "shard1.com,shard2.com");

  const GoogleString css_in =
      ".p1 { background-image: url('b1.png'); }"
      ".p2 { background-image: url('b2.png'); }";
  const GoogleString css_out =
      ".p1 { background-image: url('http://shard2.com/b1.png'); }"
      ".p2 { background-image: url('http://shard1.com/b2.png'); }";
  TestOutlineCss("http://outline_style.test/index.html", "",
                 css_in, true, css_out, "http://shard1.com/");
}

TEST_F(CssOutlineFilterTest, UrlTooLong) {
  GoogleString html_url = "http://outline_style.test/url_size_test.html";
  GoogleString style_text = "background_blue { background-color: blue; }\n"
                            "foreground_yellow { color: yellow; }\n";

  // By default we succeed at outlining.
  SetupDebug("");  // No debug message.
  TestOutlineCss(html_url, "", style_text, true, style_text, "");

  // But if we set max_url_size too small, it will fail cleanly.
  options()->ClearSignatureForTesting();
  options()->set_max_url_size(0);
  server_context()->ComputeSignature(options());
  // Now we have a debug message.
  debug_message_ = "<!--Rewritten URL too long: "
      "http://outline_style.test/_.pagespeed.co.#.-->";
  TestOutlineCss(html_url, "", style_text, false, style_text, "");
}

// Test our behavior with CDATA blocks.
TEST_F(CssOutlineFilterTest, CdataInContents) {
  SetupOutliner();
  SetXhtmlMimetype();
  // TODO(sligocki): Fix. The outlined file should be "foo  bar ".
  GoogleString css = "foo <![CDATA[ bar ]]>";
  TestOutlineCss("http://outline_css.test/cdata.html", "", css, true, css, "");
}

// Make sure we deal well with no Charactors() node between StartElement()
// and EndElement().
TEST_F(CssOutlineFilterTest, EmptyStyle) {
  SetupOutliner();
  ValidateNoChanges("empty_style", "<style></style>");
}

TEST_F(CssOutlineFilterTest, DoNotOutlineScoped) {
  SetupOutliner();
  // <style scoped> exists (with very limited support) but <link scoped>
  // doesn't, so we shouldn't be outlining scoped styles.
  ValidateNoChanges("scoped", "<style scoped>* {display: none;}</style>");
}

// http://github.com/apache/incubator-pagespeed-mod/issues/416
TEST_F(CssOutlineFilterTest, RewriteDomain) {
  SetupOutliner();
  AddRewriteDomainMapping("cdn.com", kTestDomain);

  // Check that CSS gets outlined to the rewritten domain.
  GoogleString expected_url = Encode("http://cdn.com/", "co", "0", "_", "css");
  ValidateExpected("rewrite_domain",
                   "<style>.a { color: red; }</style>",
                   StrCat("<link rel=\"stylesheet\" href=\"", expected_url,
                          "\">"));

  // And check that it serves correctly from that domain.
  GoogleString content;
  ASSERT_TRUE(FetchResourceUrl(expected_url, &content));
  EXPECT_EQ(".a { color: red; }", content);
}

}  // namespace

}  // namespace net_instaweb
