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

//     and sligocki@google.com (Shawn Ligocki)

#include "net/instaweb/rewriter/public/css_filter.h"

#include "net/instaweb/http/public/async_fetch.h"
#include "net/instaweb/http/public/http_cache.h"
#include "net/instaweb/http/public/logging_proto.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/public/css_rewrite_test_base.h"
#include "net/instaweb/rewriter/public/domain_lawyer.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/test_rewrite_driver_factory.h"
#include "net/instaweb/rewriter/public/test_url_namer.h"
#include "pagespeed/kernel/base/abstract_mutex.h"  // for ScopedMutex
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/dynamic_annotations.h"  // RunningOnValgrind
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.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_parse_test_base.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/opt/logging/log_record.h"
#include "webutil/css/parser.h"

namespace net_instaweb {

namespace {

const char kInputStyle[] =
    ".background_blue { background-color: #f00; }\n"
    ".foreground_yellow { color: yellow; }\n";
const char kOutputStyle[] =
    ".background_blue{background-color:red}"
    ".foreground_yellow{color:#ff0}";
const char kPuzzleJpgFile[] = "Puzzle.jpg";
const char kBikePngFile[] = "BikeCrashIcn.png";

class CssFilterTest : public CssRewriteTestBase {
 protected:
  void TestUrlAbsolutification(const StringPiece id,
                               const StringPiece css_input,
                               const StringPiece expected_output,
                               bool expect_unparseable_section,
                               bool enable_image_rewriting,
                               bool enable_proxy_mode,
                               bool enable_mapping_and_sharding) {
    options()->ClearSignatureForTesting();
    options()->EnableFilter(RewriteOptions::kDebug);
    options()->EnableFilter(RewriteOptions::kRewriteCss);
    if (!enable_image_rewriting) {
      options()->DisableFilter(RewriteOptions::kRecompressJpeg);
      options()->DisableFilter(RewriteOptions::kRecompressPng);
      options()->DisableFilter(RewriteOptions::kRecompressWebp);
      options()->DisableFilter(RewriteOptions::kConvertPngToJpeg);
      options()->DisableFilter(RewriteOptions::kConvertJpegToWebp);
      options()->DisableFilter(RewriteOptions::kConvertGifToPng);
      options()->DisableFilter(RewriteOptions::kConvertToWebpLossless);
      options()->DisableFilter(RewriteOptions::kLeftTrimUrls);
      options()->DisableFilter(RewriteOptions::kExtendCacheImages);
      options()->DisableFilter(RewriteOptions::kSpriteImages);
    }

    // Set things up so that RewriteDriver::ShouldAbsolutifyUrl returns true
    // even though we are not proxying (but skip it if it has already been
    // set up by a previous call to this method).
    if (enable_mapping_and_sharding &&
        !options()->domain_lawyer()->can_rewrite_domains()) {
      DomainLawyer* domain_lawyer = options()->WriteableDomainLawyer();
      MessageHandler* handler = message_handler();
      ASSERT_TRUE(domain_lawyer->AddDomain("http://cdn.com/", handler));
      ASSERT_TRUE(domain_lawyer->AddDomain("http://test.com/", handler));
      ASSERT_TRUE(domain_lawyer->AddShard("cdn.com", "cdn1.com,cdn2.com",
                                          handler));
      EXPECT_FALSE(domain_lawyer->DoDomainsServeSameContent("cdn.com",
                                                            "test.com"));
      ASSERT_TRUE(domain_lawyer->AddRewriteDomainMapping("http://cdn.com",
                                                         "http://test.com",
                                                         handler));
      EXPECT_TRUE(domain_lawyer->DoDomainsServeSameContent("cdn.com",
                                                           "test.com"));
      EXPECT_TRUE(domain_lawyer->can_rewrite_domains());
      GoogleUrl src_base("http://test.com/foo.css");
      bool proxying = true;  // to ensure it's set to false.
      EXPECT_TRUE(rewrite_driver()->ShouldAbsolutifyUrl(src_base, src_base,
                                                        &proxying));
      EXPECT_FALSE(proxying);
      GoogleUrl dst_base("http://cdn.com/foo.css");
      proxying = true;  // again to ensure it's set to false.
      EXPECT_TRUE(rewrite_driver()->ShouldAbsolutifyUrl(src_base, dst_base,
                                                        &proxying));
      EXPECT_FALSE(proxying);
    }
    server_context()->ComputeSignature(options());

    // By default TestUrlNamer doesn't proxy but we might need it for this test.
    TestUrlNamer::SetProxyMode(
        enable_proxy_mode ?
            UrlNamer::ProxyExtent::kFull : UrlNamer::ProxyExtent::kNone);

    SetResponseWithDefaultHeaders("foo.css", kContentTypeCss, css_input, 100);

    // Ensure that the input CSS has/has-not parse errors, as specified by the
    // expect_unparseable_section parameter, to cater for future improvements
    // in the CSS parser.
    Css::Parser parser(css_input);
    parser.set_preservation_mode(true);
    parser.set_quirks_mode(false);
    scoped_ptr<Css::Stylesheet> stylesheet(parser.ParseRawStylesheet());
    EXPECT_TRUE(parser.errors_seen_mask() == Css::Parser::kNoError);
    EXPECT_EQ(expect_unparseable_section,
              parser.unparseable_sections_seen_mask() != Css::Parser::kNoError);

    Parse(id, CssLinkHref("foo.css"));

    // Check for CSS files in the rewritten page.
    StringVector css_urls;
    CollectCssLinks(StrCat(id, "_collect"), output_buffer_, &css_urls);
    ASSERT_LE(1UL, css_urls.size());
    StringPiece prefix;
    if (enable_mapping_and_sharding) {
      prefix = "http://cdn1.com/";
    } else if (factory()->use_test_url_namer()) {
      prefix = kTestDomain;
    } else {
      prefix = "";
    }
    EXPECT_EQ(Encode(prefix, "cf", "0", "foo.css", "css"), css_urls[0]);

    // Get absolute CSS URL.
    GoogleUrl base_url(kTestDomain);
    GoogleUrl css_url(base_url, css_urls[0]);

    // Check the content of the CSS file.
    GoogleString actual_output;
    EXPECT_TRUE(FetchResourceUrl(css_url.Spec(), &actual_output));
    EXPECT_STREQ(expected_output, actual_output);
  }

  // A helper function for webp tests. Note that we require
  // image_content_type independently of any filename extension in
  // input_image_name. The parameters output_image_name_template and
  // output_css_name_template should both include a single "%s" token
  // which is replaced by the input_image_name and the original CSS
  // resource name, respectively.
  void TestWebpRewriting(const char* input_image_name,
                         const net_instaweb::ContentType image_content_type,
                         const char* output_image_name_template,
                         const char* output_css_name_template) {
    static const char kInputCssName[] = "foo.css";
    GoogleString image_out = StringPrintf(output_image_name_template,
                                          input_image_name);
    GoogleString css_input = StringPrintf("body{background:url(%s)}",
                                          input_image_name);
    GoogleString css_output = StringPrintf("body{background:url(%s)}",
                                           image_out.c_str());
    GoogleString expected_url = StringPrintf(output_css_name_template,
                                             kInputCssName);
    AddFileToMockFetcher(StrCat(kTestDomain, input_image_name),
                         input_image_name, image_content_type, 100);

    SetResponseWithDefaultHeaders(kInputCssName, kContentTypeCss,
                                  css_input, 100);
    Parse("webp", CssLinkHref(kInputCssName));
    // Check for CSS files in the rewritten page.
    StringVector css_urls;
    CollectCssLinks("collect", output_buffer_, &css_urls);
    ASSERT_EQ(1, css_urls.size());
    EXPECT_EQ(expected_url, css_urls[0]);

    // Check the content of the CSS file.
    GoogleString actual_output;
    EXPECT_TRUE(FetchResourceUrl(StrCat(kTestDomain, css_urls[0]),
                                 &actual_output));
    EXPECT_STREQ(css_output, actual_output);
  }

  void TestFallbackFetch(const GoogleString& url, StringPiece expected_output) {
    GoogleString content;
    StringAsyncFetch async_fetch(rewrite_driver_->request_context(), &content);
    bool fetched = rewrite_driver_->FetchResource(url, &async_fetch);
    ASSERT_TRUE(fetched);
    rewrite_driver_->WaitForShutDown();
    ClearRewriteDriver();
    EXPECT_TRUE(async_fetch.done());
    EXPECT_TRUE(async_fetch.success());

    EXPECT_STREQ(expected_output, content);
    if (async_fetch.content_length_known()) {
      EXPECT_EQ(content.size(), async_fetch.content_length());
    }
  }
};

TEST_F(CssFilterTest, SimpleRewriteCssTest) {
  ValidateRewrite("rewrite_css", kInputStyle, kOutputStyle, kExpectSuccess);
}

TEST_F(CssFilterTest, SimpleRewriteCssTestExternalUnhealthy) {
  DebugWithMessage("");  // TODO(jmaessen): Needs debug feedback.
  lru_cache()->set_is_healthy(false);
  ValidateRewriteExternalCss("rewrite_css", kInputStyle, kOutputStyle,
                             kExpectNoChange);
}

TEST_F(CssFilterTest, CssRewriteRandomDropAll) {
  // Test that randomized optimization doesn't rewrite when drop % set to 100
  options()->ClearSignatureForTesting();
  options()->set_rewrite_random_drop_percentage(100);
  server_context()->ComputeSignature(options());
  DebugWithMessage("");  // TODO(jmaessen): Needs debug feedback.
  for (int i = 0; i < 100; ++i) {
    ValidateRewriteExternalCss("rewrite_css", kInputStyle, kOutputStyle,
                               kExpectNoChange);
    lru_cache()->Clear();
    ClearStats();
  }
}

TEST_F(CssFilterTest, CssRewriteRandomDropNone) {
  // Test that randomized optimization always rewrites when drop % set to 0.
  options()->ClearSignatureForTesting();
  options()->set_rewrite_random_drop_percentage(0);
  server_context()->ComputeSignature(options());
  DebugWithMessage("");  // Shouldn't see debug feedback.
  for (int i = 0; i < 100; ++i) {
    ValidateRewriteExternalCss("rewrite_css", kInputStyle, kOutputStyle,
                               kExpectSuccess);
    lru_cache()->Clear();
    ClearStats();
  }
}

TEST_F(CssFilterTest, RewriteCss404) {
  // Test to make sure that a missing input is handled well.
  DebugWithMessage("<!--4xx status code, preventing rewriting of"
                   " http://test.com/404.css-->");

  SetFetchResponse404("404.css");

  const char input[] = "<link rel=stylesheet href='404.css'>";
  GoogleString expected_output = StrCat(input, debug_message_);

  ValidateExpected("404", input, expected_output);
  // Second time, to make sure caching doesn't break it.
  ValidateExpected("404", input, expected_output);
}

class CssFilterTestCustomOptions : public CssFilterTest {
 protected:
  // Derived classes should add their options and then call
  // CssFilterTest::SetUp.
  virtual void SetUp() {}
};

TEST_F(CssFilterTestCustomOptions, CssPreserveUrls) {
  options()->SoftEnableFilterForTesting(RewriteOptions::kInlineCss);
  options()->SoftEnableFilterForTesting(RewriteOptions::kRewriteCss);
  options()->set_css_preserve_urls(true);
  CssFilterTest::SetUp();
  // Verify that preserve had a chance to forbid some filters.
  EXPECT_FALSE(options()->Enabled(RewriteOptions::kInlineCss));
  SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kInputStyle, 100);

  // The URL shouldn't change.
  ValidateNoChanges("css_preserve_urls_on", "<link rel=StyleSheet href=a.css>");

  // We should have the optimized CSS even though we didn't render the URL.
  ClearStats();
  GoogleString out_css_url = Encode(kTestDomain, "cf", "0", "a.css", "css");
  GoogleString out_css;
  EXPECT_TRUE(FetchResourceUrl(out_css_url, &out_css));
  EXPECT_EQ(1, http_cache()->cache_hits()->Get());
  EXPECT_EQ(0, http_cache()->cache_misses()->Get());
  EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
  EXPECT_EQ(1, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // Was the CSS minified?
  EXPECT_EQ(kOutputStyle, out_css);
}

TEST_F(CssFilterTestCustomOptions, CssPreserveUrlsOverridingExtend) {
  scoped_ptr<RewriteOptions> global_options(options()->NewOptions());
  global_options->EnableFilter(RewriteOptions::kExtendCacheCss);

  scoped_ptr<RewriteOptions> vhost_options(options()->NewOptions());
  vhost_options->EnableFilter(RewriteOptions::kRewriteCss);
  vhost_options->SoftEnableFilterForTesting(RewriteOptions::kInlineCss);
  vhost_options->SoftEnableFilterForTesting(RewriteOptions::kRewriteCss);
  vhost_options->set_css_preserve_urls(true);  // This will win over ExtendCache
  options()->Merge(*global_options);
  options()->Merge(*vhost_options);

  CssFilterTest::SetUp();
  // Verify that preserve had a chance to forbid some filters.
  EXPECT_FALSE(options()->Enabled(RewriteOptions::kInlineCss));
  SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kInputStyle, 100);

  // The URL shouldn't change.
  ValidateNoChanges("css_preserve_urls_on", "<link rel=StyleSheet href=a.css>");

  // We should have the optimized CSS even though we didn't render the URL.
  ClearStats();
  GoogleString out_css_url = Encode(kTestDomain, "cf", "0", "a.css", "css");
  GoogleString out_css;
  EXPECT_TRUE(FetchResourceUrl(out_css_url, &out_css));
  EXPECT_EQ(1, http_cache()->cache_hits()->Get());
  EXPECT_EQ(0, http_cache()->cache_misses()->Get());
  EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
  EXPECT_EQ(1, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // Was the CSS minified?
  EXPECT_EQ(kOutputStyle, out_css);
}

TEST_F(CssFilterTestCustomOptions, CssPreserveUrlsWithMergedCacheExtend) {
  scoped_ptr<RewriteOptions> global_options(options()->NewOptions());
  global_options->EnableFilter(RewriteOptions::kRewriteCss);
  global_options->set_css_preserve_urls(true);

  // Because we set extend_cache at a "lower level", it takes priority
  // over preserve_css_urls and enables URL-rewriting for CSS.
  scoped_ptr<RewriteOptions> vhost_options(options()->NewOptions());
  vhost_options->EnableFilter(RewriteOptions::kExtendCacheCss);
  options()->Merge(*global_options);
  options()->Merge(*vhost_options);

  CssFilterTest::SetUp();
  SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kInputStyle, 100);

  GoogleString out_css_url = Encode(kTestDomain, "cf", "0", "a.css", "css");

  // The URL should be updated since by specifying "extend_cache_css" the user
  // has signaled its OK to change the URLS.
  ValidateExpected("css_preserve_urls_on",
                   "<link rel=StyleSheet href=a.css>",
                   StrCat("<link rel=StyleSheet href=",
                          ExpectedUrlForCss("a", kOutputStyle), ">"));

  // We should have the optimized CSS even though we didn't render the URL.
  ClearStats();
  GoogleString out_css;
  EXPECT_TRUE(FetchResourceUrl(out_css_url, &out_css));
  EXPECT_EQ(1, http_cache()->cache_hits()->Get());
  EXPECT_EQ(0, http_cache()->cache_misses()->Get());
  EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
  EXPECT_EQ(1, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // Was the CSS minified?
  EXPECT_EQ(kOutputStyle, out_css);
}

TEST_F(CssFilterTestCustomOptions, CssPreserveUrlsNoPreemptiveRewrite) {
  options()->SoftEnableFilterForTesting(RewriteOptions::kInlineCss);
  options()->set_css_preserve_urls(true);
  options()->set_in_place_preemptive_rewrite_css(false);
  CssFilterTest::SetUp();
  // Verify that preserve had a chance to forbid some filters.
  EXPECT_FALSE(options()->Enabled(RewriteOptions::kInlineCss));
  SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kInputStyle, 100);

  // The URL shouldn't change.
  ValidateNoChanges("css_preserve_urls_on_no_preemptive",
                    "<link rel=StyleSheet href=a.css>");

  // We should not have attempted any rewriting.
  EXPECT_EQ(0, http_cache()->cache_hits()->Get());
  EXPECT_EQ(0, http_cache()->cache_misses()->Get());
  EXPECT_EQ(0, http_cache()->cache_inserts()->Get());
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_hits()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_misses()));
  EXPECT_EQ(0, static_cast<int>(lru_cache()->num_inserts()));

  // But, if we fetch the optimized CSS directly, we should receive the
  // optimized version.
  ClearStats();
  GoogleString out_css_url = Encode(kTestDomain, "cf", "0", "a.css", "css");
  GoogleString out_css;
  EXPECT_TRUE(FetchResourceUrl(out_css_url, &out_css));
  EXPECT_EQ(kOutputStyle, out_css);
}


TEST_F(CssFilterTestCustomOptions, FallbackWithGzipData) {
  const char kCss[] = " hello { display: block; border: 1px solid fuchsia; }";
  options()->set_http_cache_compression_level(6);
  // Force the fallback since rewrite is taking too long" path.
  options()->set_test_instant_fetch_rewrite_deadline(true);
  CssFilterTest::SetUp();

  // Store an already-gzipped version of the resource in the cace.
  ResponseHeaders headers;
  DefaultResponseHeaders(kContentTypeCss, 5000, &headers);
  HTTPValue value;
  value.SetHeaders(&headers);
  value.Write(kCss, message_handler());
  http_cache()->Put("http://test.com/a.css", rewrite_driver()->CacheFragment(),
                    RequestHeaders::Properties(),  kDefaultHttpOptionsForTests,
                    &value, message_handler());

  // Fetch with deadline exceeded. Should get uncompressed bits with proper
  // content-length. (Consistency of compression, Content-Encoding and
  // Content-Length is what's important here).
  GoogleString out_css_url = Encode(kTestDomain, "cf", "0", "a.css", "css");
  GoogleString out_css;
  ResponseHeaders out_headers;
  EXPECT_TRUE(FetchResourceUrl(out_css_url, &out_css, &out_headers));
  EXPECT_EQ(kCss, out_css);
  int64 content_length = 0;
  EXPECT_TRUE(out_headers.FindContentLength(&content_length));
  EXPECT_EQ(STATIC_STRLEN(kCss), content_length);
  EXPECT_FALSE(out_headers.IsGzipped());
}

TEST_F(CssFilterTest, LinkHrefCaseInsensitive) {
  // Make sure we check rel value case insensitively.
  // http://github.com/apache/incubator-pagespeed-mod/issues/354
  SetResponseWithDefaultHeaders("a.css", kContentTypeCss, kInputStyle, 100);
  ValidateExpected(
      "case_insensitive", "<link rel=StyleSheet href=a.css>",
      StrCat("<link rel=StyleSheet href=",
             ExpectedUrlForCss("a", kOutputStyle),
             ">"));
}

TEST_F(CssFilterTest, UrlTooLong) {
  DebugWithMessage("<!--Rewritten URL segment too long.-->");

  // Make the filename maximum size, so we cannot rewrite it.
  // -4 because .css will be appended.
  GoogleString filename(options()->max_url_segment_size() - 4, 'z');
  // If filename wasn't too long, this would be rewritten (like in
  // SimpleRewriteCssTest).
  ValidateRewriteExternalCss(filename, kInputStyle, kInputStyle,
                             kExpectNoChange);
}

// Make sure we can deal with 0 character nodes between open and close of style.
TEST_F(CssFilterTest, RewriteEmptyCssTest) {
  // Note: We must check stats ourselves because, for technical reasons,
  // empty inline styles are not treated as being rewritten at all.
  ValidateRewriteInlineCss("rewrite_empty_css-inline", "", "",
                           kExpectSuccess | kNoStatCheck);
  EXPECT_STREQ("", AppliedRewriterStringFromLog());
  EXPECT_EQ(0, num_blocks_rewritten_->Get());
  EXPECT_EQ(0, total_bytes_saved_->Get());
  EXPECT_EQ(0, num_parse_failures_->Get());

  ValidateRewriteExternalCss("rewrite_empty_css-external", "", "",
                             kExpectFailure | kNoStatCheck);
  EXPECT_EQ(0, total_bytes_saved_->Get());
  EXPECT_EQ(0, num_parse_failures_->Get());
}

// Make sure we allow rewriting to empty output (ex: input all commented out).
TEST_F(CssFilterTest, EmptyOutput) {
  ValidateRewrite("empty_output",
                  "/* body { background: blue; } */\n", "", kExpectSuccess);
}

TEST_F(CssFilterTest, TransitionTimeUnits) {
  // TODO(jmarantz): the 'after' CSS is not well minified.  We should strip
  // the spaces around the comma.
  ValidateRewrite("transition_time",
                  "foo {  -moz-transition-delay: 0s, 0s; }",
                  "foo{-moz-transition-delay:0s , 0s}",
                  kExpectSuccess);
}

// Make sure we do not recompute external CSS when re-processing an already
// handled page.
TEST_F(CssFilterTest, RewriteRepeated) {
  // ValidateRewriteExternalCssUrl calls FetchResourceUrl, which resets
  // the request context on rewrite_driver_. So we can test changes to the log
  // record, we keep a (ref counted) pointer to the request context before
  // running the validate.
  // TODO(marq): Make this not necessary by folding log validation into
  // ValidateWithStats().
  RequestContextPtr rctx = rewrite_driver()->request_context();
  rctx->log_record()->SetAllowLoggingUrls(true);
  ValidateRewriteExternalCss("rep", " div { } ", "div{}", kExpectSuccess);
  int inserts_before = lru_cache()->num_inserts();
  EXPECT_EQ(1, num_blocks_rewritten_->Get());  // for factory_
  EXPECT_EQ(1, num_uses_->Get());
  {
    ScopedMutex lock(rctx->log_record()->mutex());
    EXPECT_STREQ("cf", rctx->log_record()->AppliedRewritersString());
  }
  VerifyRewriterInfoEntry(rctx->log_record(), "cf", 0, 0, 1, 1,
                          "http://test.com/rep.css");
  ResetStats();

  rctx.reset(rewrite_driver()->request_context());
  rctx->log_record()->SetAllowLoggingUrls(true);
  ValidateRewriteExternalCss("rep", " div { } ", "div{}",
                             kExpectSuccess | kNoStatCheck);
  int inserts_after = lru_cache()->num_inserts();
  EXPECT_EQ(0, lru_cache()->num_identical_reinserts());
  EXPECT_EQ(inserts_before, inserts_after);
  EXPECT_EQ(0, num_blocks_rewritten_->Get());
  EXPECT_EQ(1, num_uses_->Get());
  {
    ScopedMutex lock(rctx->log_record()->mutex());
    EXPECT_STREQ("cf", rctx->log_record()->AppliedRewritersString());
  }
  VerifyRewriterInfoEntry(rctx->log_record(), "cf", 0, 0, 1, 1,
                          "http://test.com/rep.css");
}

// Make sure we do not reparse external CSS when we know it already has
// a parse error.
TEST_F(CssFilterTest, RewriteRepeatedParseError) {
  DebugWithMessage("<!--CSS rewrite failed: Parse error in %url%-->");
  const char kInvalidCss[] = "@media }}";
  // Note: It is important that these both have the same id so that the
  // generated CSS file names are identical.
  // TODO(sligocki): This is sort of annoying for error reporting which
  // is suposed to use id to uniquely distinguish which test was running.
  ValidateRewriteExternalCss("rep_fail", kInvalidCss, "", kExpectFailure);
  // First time, we fail to parse.
  EXPECT_EQ(1, num_parse_failures_->Get());
  ValidateRewriteExternalCss("rep_fail", kInvalidCss, "",
                             kExpectFailure | kNoStatCheck);
  // Second time, we remember failure and so don't try to reparse.
  EXPECT_EQ(0, num_parse_failures_->Get());
}

// Deal nicely with non-UTF8 encodings.
TEST_F(CssFilterTest, NonUtf8) {
  // Distilled examples.
  // gb2312 (Not valid UTF-8, multi-byte).
  ValidateRewrite("font", "a { font-family: \"\xCB\xCE\xCC\xE5\"; }",
                          "a{font-family: \"\xCB\xCE\xCC\xE5\"}",
                  kExpectSuccess);
  // Windows-1252 (Not valid UTF-8, single-byte).
  ValidateRewrite("string", ".foo { content: \"r\xE9sum\xE9\"; }",
                            ".foo{content: \"r\xE9sum\xE9\"}",
                  kExpectSuccess);
  // Shift_JIS (Not valid UTF-8, multi-byte, second byte may not set high bit).
  ValidateRewrite("ident_value",
                  ".foo { -moz-charset: \x83\x56\x83\x74\x83\x67\x83\x57; }",
                  ".foo{-moz-charset: \x83\x56\x83\x74\x83\x67\x83\x57}",
                  kExpectSuccess);
  // KOI8-R (Not valid UTF-8, single-byte).
  ValidateRewrite("ident_param", ".foo { \xEB\xEF\xE9-8: standard; }",
                                 ".foo{\xEB\xEF\xE9-8: standard}",
                  kExpectSuccess);
  // EUC-KR (Not valid UTF-8, multi-byte).
  ValidateRewrite("ident_selector", ".\xB8\xC0 { color: red; }",
                                    ".\xB8\xC0 {color:red}",
                  kExpectSuccess);

  // Verbatim example from http://www.baidu.com/
  ValidateRewrite("baidu", "#lk span {font:14px \"\xCB\xCE\xCC\xE5\"}",
                           "#lk span{font:14px \"\xCB\xCE\xCC\xE5\"}",
                           kExpectSuccess);
}

// In UTF-8, all multi-byte characters have high bit set. This is not true in
// other common web encodings.
TEST_F(CssFilterTest, Non8BitEncoding) {
  // Shift_JIS can have second bytes in range 0x40-0x7F,
  // which includes ASCII chars: @ A-Z [/]^_` a-z {|}~

  // 0x83 0x7D == KATAKANA LETTER MA
  // 0x7D == RIGHT CURLY BRACKET }
  DebugWithMessage("");
  ValidateRewrite("string-ma", ".foo { font-family: \"\x83\x7D\"; color: red }",
                               ".foo{font-family: \"\x83\x7D\";color:red}",
                  kExpectSuccess);
  // 0x83 0x7B == KATAKANA LETTER BO
  // 0x7B == LEFT CURLY BRACKET {
  ValidateRewrite("string-bo", ".foo { font-family: \"\x83\x7B\"; color: red }",
                               ".foo{font-family: \"\x83\x7B\";color:red}",
                  kExpectSuccess);

  // Note: This text currently fails to be parsed. But if that changes,
  // update this test to the correct golden rewrite.
  DebugWithMessage("<!--CSS rewrite failed: Parse error in %url%-->");
  ValidateFailParse("ident-ma", ".foo { -win-magic: bar\x83\x7D; color: red }");
  // Note: This text currently fails to be parsed. But if that changes,
  // update this test to the correct golden rewrite.
  ValidateFailParse("ident-bo", ".foo { -win-magic: bar\x83\x7B; color: red }");
}

// Make sure bad requests do not corrupt our extension.
TEST_F(CssFilterTest, NoExtensionCorruption) {
  TestCorruptUrl(".css%22");
}

TEST_F(CssFilterTest, NoQueryCorruption) {
  TestCorruptUrl(".css?query");
}

TEST_F(CssFilterTest, NoWrongExtCorruption) {
  TestCorruptUrl(".html");
}

TEST_F(CssFilterTest, RewriteVariousCss) {
  // TODO(sligocki): Get these tests to pass with setlocale.
  // EXPECT_TRUE(setlocale(LC_ALL, "tr_TR.utf8"));
  // Distilled examples.
  const char* good_examples[] = {
    "a.b #c.d e#d,f:g>h+i>j{color:red}",  // .#,>+: in selectors
    "a{border:solid 1px #ccc}",  // Multiple values declaration
    "a{border:none!important}",  // !important
    "a{background-image:url(foo.png)}",  // url
    "a{background-position:-19px 60%}",  // negative position
    "a{margin:0}",  // 0 w/ no units
    "a{padding:.01em -.25em}",  // fractions, negative and em
    "a{-moz-border-radius-topleft:0}",  // Browser-specific (-moz)
    ".ds{display:-moz-inline-box}",
    "a{background:none}",  // CSS Parser used to expand this.
    // http://github.com/apache/incubator-pagespeed-mod/issues/121
    "a{color:inherit}",
    // Added for code coverage.
    "@import url(http://www.example.com);",
    "@media a,b{a{color:red}}",
    "@charset \"foobar\";",
    // Invalid charset (string should be quoted).
    "@charset utf-8;",
    "@font-face{font-family:'Ubuntu';font-style:normal}",
    // Unescaped string: Odd chars: \(\)\,\"\'
    "a{content:\"Odd chars: \\(\\)\\,\\\"\\\'\"}",
    // Unescaped string: Unicode: \A0\A0
    "a{content:\"Unicode: \\A0\\A0\"}",
    "img{clip:rect(0,60px,200px,0)}",
    // CSS3-style pseudo-elements.
    "p.normal::selection{background:#c00;color:#fff}",
    "::-moz-focus-inner{border:0}",
    "input::-webkit-input-placeholder{color:#ababab}"
    // http://github.com/apache/incubator-pagespeed-mod/issues/51
    "a{box-shadow:-1px -2px 2px rgba(0,0,0,.15)}",  // CSS3 rgba
    // http://github.com/apache/incubator-pagespeed-mod/issues/66
    "a{-moz-transform:rotate(7deg)}",
    // Microsoft syntax values.
    "a{filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80)}",
    // Make sure we keep "\," distinguished from ",".
    "body{font-family:font\\,1,font\\,2}",
    // Distinguish \. from ., etc.
    // http://github.com/apache/incubator-pagespeed-mod/issues/574
    "#MyForm\\.myfield{property:value}",
    "\\*{color:red}",
    "a{property\\:more:value\\;more}"
    // Found in the wild:
    "a{width:overflow:hidden}",
    // IE hack: \9
    "div{margin:100px\\9 }",
    "div{margin\\9 :100px}",
    "div\\9 {margin:100px}",
    "a{color:red\\9 }",
    "a{background:none\\9 }",

    // Don't replace system color names with defaults.
    "a{color:WindowText}",
    "a{color:FooBar}",

    // Recovered parse errors:
    // Slashes in value list.
    ".border8{border-radius: 36px / 12px }",

    // http://github.com/apache/incubator-pagespeed-mod/issues/220
    // See https://developer.mozilla.org/en/CSS/-moz-transition-property
    // and http://www.webkit.org/blog/138/css-animation/
    // TODO(sligocki): rm spaces around COMMA token.
    "a{-webkit-transition-property:opacity , -webkit-transform}",

    // Parameterized pseudo-selector.
    "div:nth-child(1n) {color:red}",

    // IE8 Hack \0/
    // See http://dimox.net/personal-css-hacks-for-ie6-ie7-ie8/
    "a{color: red\\0/ ;background-color:green}",
    "a{font-family: font\\0  ;color:red}",
    "@media screen and (min-width:0 \\\\0){.foo{color:red}}",

    "a{font:bold verdana 10px }",
    "a{foo: +bar }",
    "a{color: rgb(foo,+,) }",

    // Malformed @import statements.
    "@import styles.css;a{color:red}",
    "@import \"styles.css\", \"other.css\";a{color:red}",
    "@import url(styles.css), url(other.css);a{color:red}",
    "@import \"styles.css\"...;a{color:red}",

    // CSS3 media queries.
    // http://github.com/apache/incubator-pagespeed-mod/issues/50
    "@media screen and (max-width:290px){a{color:red}}",
    "@media only print and (color){a{color:red}}",

    // Malformed @media statements.
    // Important: Don't "fix" by adding space between 'and' and '('.
    "@media only screen and(min-resolution:240dpi){ .bar{ background: red; }}",
    // Unexpected space in media feature name.
    "@media (max-de vice-width: 850px) { .pm-thumb-106 { width: 80px; } }",
    // Unexpected \0 in various places. Common browser hack.
    "@media screen\\0{ .select:before { width: 18px; } }",
    "@media screen and (min-width:0 \\0) { .foo { color: red; } }",
    // Nonsensical, but syntactic, media query.
    "@media not (-moz-dimension-constraints:20 < width < 300 and 45 < height "
    "< 1000){a{color:red}}",
    // Empty media queries.
    "@media , { a { color: red; } }",

    // Unexpected @-statements
    "@keyframes wiggle { 0% { transform: rotate(6deg); } }",

    // Invalid font declaration.
    "a{font: menu foobar }",

    // Do not remove . between 1 and em, this needs to be lexed as:
    // INT(1) DELIM(.) IDENT(em)
    "a{padding-top: 1.em }",

    // Unexpected ! uses in declarations.
    "a{color: red !ie }",
    "a{color: !important red }",
    "a{color: red !important blue }",

    // Things from Alexa-100 that we get parsing errors for. Most are illegal
    // syntax/typos. Some are CSS3 constructs.

    // kDeclarationError from Alexa-100
    // Comma in values
    "a{webkit-transition-property:color , background-color}",
    // Special chars in property
    "a{//display: inline-block }",
    ".ad_300x250{/margin-top:-120px }",
    // Properties with no value
    "a{background-repeat;no-repeat }",
    // Typos
    "a{margin-right:0;width:113px;*/ }",
    "a{z-i ndex:19 }",
    "a{width:352px;height62px ;display:block}",
    "a{color: #5552 }",
    "a{1font-family:Tahoma , Arial , sans-serif}",
    "a{text align:center }",

    // kSelectorError from Alexa-100
    // Selector list ends in comma
    ".hp .col ul, {display:inline}",
    // Parameters for pseudoclass
    "body:not(:target) {color:red}",
    "a:not(.button):hover {color:red}",
    // Typos
    "# new_results_notification{font-size:12px}",
    ".bold: {font-weight:bold}",

    // kFunctionError from Alexa-100
    // Expression
    "a{_top: expression(0+((e=document.documen))) }",
    "a{width: expression(this.width > 120 ? 120:tr) }",
    "a{_height: expression((parentNode.offsetHeight-17))}",
    // Equals in function
    "a{progid:DXImageTransform.Microsoft.AlphaImageLoader"
    "(src=/images/lb/internet_e) }",
    "a{progid:DXImageTransform.Microsoft.AlphaImageLoader"
    "(src=\"/images/lb/internet_e\") }",
    "a{progid:DXImageTransform.Microsoft.AlphaImageLoader"
    "(src='/images/lb/internet_e') }",

    // Mismatched {}s that are acceptable because they do not fail to close {s,
    // but instead have stray }s that would not be parsed as closing previous {s
    "a[}]{color:red}",

    // Don't "fix" font properties.
    "a{font:12px,clean}",

    // Various other Alexa results.
    "@-moz-document url-prefix() { input[type=\"file\"] {border:none}}",
    ".foo{background-image: (/i.png);width:10px}",
    ".bigAd{background:14url(http://himg2.huanqiu.com/bigAd.jpg) center}",
    ".foo{background: url\n\n(../images/btn_login_min.gif) }",
    "@-webkit-keyframes \"foo\" { 0 { transform: scale (.85) } }",
  };

  for (int i = 0; i < arraysize(good_examples); ++i) {
    GoogleString id = StringPrintf("distilled_css_good%d", i);
    ValidateRewrite(id, good_examples[i], good_examples[i], kExpectSuccess);
  }

  const char* fail_examples[] = {
    // Unclosed at-rules.
    // These are technically valid, but we consider them a parsing errors
    // because combining is invalid for them.
    "@import url(styles.css)",
    "@import url(styles.css) screen",
    "@charset 'utf-8'",
    "@media print",
    "@media print ",
    "@media print {",
    "@media print { .a { color: red",
    "@media print { .a { color: red; }",
    "@font-face",
    "@font-face ",
    "@font-face {",
    "@media screen { @font-face",
    "@media screen { @font-face {",
    "@media screen { @font-face { src: url(foo.woff)",
    "@media screen { @font-face {}",
    "@media screen { @font-face { src: url(foo.woff); }",
    "@foobar",
    "@foobar ",
    // Mismatch in unparseable at-rule.
    "@foobar {",
    "@foobar }",

    // Should fail, mismatched {}s.
    "{",
    "}",
    "}{",
    "a { color: red; }}}",
    // Mismatch in unparseable selector.
    "a[{] { color: red; }",
    "a {",
    "a }",
    // Mismatch in unparseable declaration.
    "a { *color{: red; }",
    "a { *color}: red; }",
    "a { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader"
    "(src=/images/{lb/internet_e); }",
    // Missing end }s.
    "a { color: red",
    "a { color: red;\n .foo { margin: 10px; }",
    "a { color: red;\n h1 { margin: 10px; }",
    // Unclosed comment.
    "/*",
    "a { color: red; }/*",
    "a { color/*: red; }",
    "a { color: /*red; }",

    // Parsing failures from Alexa-1000.

    // UTF-16
    "\xFF\xFE"   // UTF-16 byte-order marker.
    "\x2F\x00"   // /
    "\x2A\x00"   // *
    "\x2A\x00"   // *
    "\x2F\x00",  // /

    // Various Typos.
    // TODO(sligocki):  We should pass these through as unparsed regions.
    "</foobar>",
    "-ms-filter:\"progid:DXImageTransform.Microsoft.Alpha\"(Opacity=70)",

    // Trailing gibberish.
    // Note: we are depending upon these failing to indicate to CssCombineFilter
    // that they cannot be combined.
    "a",
    "a { color: red }\n */",
    "a { color: red }\n // Comment",
    "a { color: red } .foo",
    "<!--\n\n // -->",
    "<!-- 让四张图片都可以重叠在一起 -->",
    "---------------------------",
  };

  DebugWithMessage("<!--CSS rewrite failed: Parse error in %url%-->");
  for (int i = 0; i < arraysize(fail_examples); ++i) {
    GoogleString id = StringPrintf("distilled_css_fail%d", i);
    ValidateFailParse(id, fail_examples[i]);
  }
}

TEST_F(CssFilterTest, EmbeddedTag) {
  // We can only test this with ExternalCss, because inline it actually works :)
  const char input[] = "a { color: red } </style><style>.foo { color: green }";
  ValidateRewriteExternalCss("embedded_tag", input, input, kExpectFailure);
}

// Things we could be optimizing.
// This test will fail when we start optimizing these thing.
TEST_F(CssFilterTest, ToOptimize) {
  const char* examples[][2] = {
    // Noticed from YUI minification.
    { ".gb1, .gb3 {}",
      // Could be: "" (Completely removed since it's empty).
      ".gb1,.gb3{}", },
    { "@media screen and (color) { .foo {} .bar {} }",
      // Could be: "" (Completely removed since it's empty).
      "@media screen and (color){.foo{}.bar{}}", },
    { ".lst:focus { outline:none; }",
      // Could be: ".lst:focus{outline:0}"
      ".lst:focus{outline:none}", },
  };

  for (int i = 0; i < arraysize(examples); ++i) {
    GoogleString id = StringPrintf("to_optimize_%d", i);
    ValidateRewrite(id, examples[i][0], examples[i][1], kExpectSuccess);
  }
}

// Test more complicated CSS.
TEST_F(CssFilterTest, ComplexCssTest) {
  // Real-world examples. Picked out of Wikipedia's CSS.
  const char* examples[][2] = {
    { "#userlogin, #userloginForm {\n"
      "  border: solid 1px #cccccc;\n"
      "  padding: 1.2em;\n"
      "  float: left;\n"
      "}\n",

      "#userlogin,#userloginForm{border:solid 1px #ccc;padding:1.2em;"
      "float:left}"},

    { "h3 .editsection { font-size: 76%; font-weight: normal; }\n",
      "h3 .editsection{font-size:76%;font-weight:normal}"},

    { "div.magnify a, div.magnify img {\n"
      "  display: block;\n"
      "  border: none !important;\n"
      "  background: none !important;\n"
      "}\n",

      "div.magnify a,div.magnify img{display:block;border:none!important;"
      "background:none!important}"},

    { "#ca-watch.icon a:hover {\n"
      "  background-image: url('images/watch-icons.png?1');\n"
      "  background-position: -19px 60%;\n"
      "}\n",

      "#ca-watch.icon a:hover{background-image:url(images/watch-icons.png?1);"
      "background-position:-19px 60%}"},

    { "body {\n"
      "  background: White;\n"
      "  /*font-size: 11pt !important;*/\n"
      "  color: Black;\n"
      "  margin: 0;\n"
      "  padding: 0;\n"
      "}\n",

      "body{background:#fff;color:#000;margin:0;padding:0}"},

    { ".suggestions-result{\n"
      "  color:black;\n"
      "  color:WindowText;\n"
      "  padding:0.01em 0.25em;\n"
      "}\n",

      ".suggestions-result{color:#000;color:WindowText;padding:.01em .25em}" },

    { ".ui-corner-tl { -moz-border-radius-topleft: 0; -webkit-border-top-left"
      "-radius: 0; }\n",

      ".ui-corner-tl{-moz-border-radius-topleft:0;-webkit-border-top-left"
      "-radius:0}"},

    { ".ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li."
      "ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { "
      "cursor: pointer; }\n",

      ".ui-tabs .ui-tabs-nav li.ui-tabs-selected a,.ui-tabs .ui-tabs-nav "
      "li.ui-state-disabled a,.ui-tabs .ui-tabs-nav li.ui-state-processing a{"
      "cursor:pointer}"},

    { ".ui-datepicker-cover {\n"
      "  display: none; /*sorry for IE5*/\n"
      "  display/**/: block; /*sorry for IE5*/\n"
      "  position: absolute; /*must have*/\n"
      "  z-index: -1; /*must have*/\n"
      "  filter: mask(); /*must have*/\n"
      "  top: -4px; /*must have*/\n"
      "  left: -4px; /*must have*/\n"
      "  width: 200px; /*must have*/\n"
      "  height: 200px; /*must have*/\n"
      "}\n",

      // TODO(sligocki): Should we preserve the display/**/:?
      //".ui-datepicker-cover{display:none;display/**/:block;position:absolute;"
      //"z-index:-1;filter:mask();top:-4px;left:-4px;width:200px;height:200px}"

      ".ui-datepicker-cover{display:none;display:block;position:absolute;"
      "z-index:-1;filter:mask();top:-4px;left:-4px;width:200px;height:200px}" },

    { ".shift {\n"
      "  -moz-transform: rotate(7deg);\n"
      "  -webkit-transform: rotate(7deg);\n"
      "  -moz-transform: skew(-25deg);\n"
      "  -webkit-transform: skew(-25deg);\n"
      "  -moz-transform: scale(0.5);\n"
      "  -webkit-transform: scale(0.5);\n"
      "  -moz-transform: translate(3em, 0);\n"
      "  -webkit-transform: translate(3em, 0);\n"
      "}\n",

      ".shift{-moz-transform:rotate(7deg);-webkit-transform:rotate(7deg);"
      "-moz-transform:skew(-25deg);-webkit-transform:skew(-25deg);"
      "-moz-transform:scale(.5);-webkit-transform:scale(.5);"
      "-moz-transform:translate(3em,0);-webkit-transform:translate(3em,0)}" },

    // http://github.com/apache/incubator-pagespeed-mod/issues/5
    // Keep space between trebuchet and ms.
    // TODO(sligocki): Print as font-family:"trebuchet ms" instead.
    // According to the CSS spec:
    //   To avoid mistakes in escaping, it is recommended to quote font
    //   family names that contain white space, digits, or punctuation
    //   characters other than hyphens.
    // It also seems more likely that a bad browser would be more likely
    // to fail to read the escaped space than a proper string.
    { "a { font-family: trebuchet ms; }", "a{font-family:trebuchet\\ ms}" },

    // http://github.com/apache/incubator-pagespeed-mod/issues/121
    { "body { font: 2em sans-serif; }", "body{font:2em sans-serif}" },
    { "body { font: 0.75em sans-serif; }", "body{font:.75em sans-serif}" },

    // http://github.com/apache/incubator-pagespeed-mod/issues/128
    { "#breadcrumbs ul { list-style-type: none; }",
      "#breadcrumbs ul{list-style-type:none}" },

    // http://github.com/apache/incubator-pagespeed-mod/issues/126
    // Extra spaces assure that we actually rewrite the first arg even if
    // font: is expanded by parser.
    { ".menu { font: menu; }               ", ".menu{font:menu}" },

    // http://github.com/apache/incubator-pagespeed-mod/issues/211
    { "#some_id {\n"
      "background: #cccccc url(images/picture.png) 50% 50% repeat-x;\n"
      "}\n",

      "#some_id{background:#ccc url(images/picture.png) 50% 50% repeat-x}" },

    { ".gac_od { border-color: -moz-use-text-color #E7E7E7 #E7E7E7 "
      "-moz-use-text-color; }",

      ".gac_od{border-color:-moz-use-text-color #e7e7e7 #e7e7e7 "
      "-moz-use-text-color}" },

    // Star/Underscore hack
    // See: http://developer.yahoo.com/yui/compressor/css.html
    { "a { *padding-bottom: 0px; }",
      "a{*padding-bottom: 0px}" },

    { "#element { width: 1px; _width: 3px; }",
      "#element{width:1px;_width:3px}" },

    // Complex nested functions
    { "body {\n"
      "  background-image:-webkit-gradient(linear, 50% 0%, 50% 100%,"
      " from(rgb(232, 237, 240)), to(rgb(252, 252, 253)));\n"
      "  color: red;\n"
      "}\n"
      ".foo { color: rgba(1, 2, 3, 0.4); }\n",

      "body{background-image:-webkit-gradient(linear,50% 0%,50% 100%,"
      "from(#e8edf0),to(#fcfcfd));color:red}.foo{color:rgba(1,2,3,.4)}" },

    // Counters
    // http://www.w3schools.com/CSS/tryit.asp?filename=trycss_gen_counter-reset
    { "body {counter-reset:section;}\n"
      "h1 {counter-reset:subsection;}\n"
      "h1:before\n"
      "{\n"
      "counter-increment:section;\n"
      "content:\"Section \" counter(section) \". \";\n"
      "}\n"
      "h2:before \n"
      "{\n"
      "counter-increment:subsection;\n"
      "content:counter(section) \".\" counter(subsection) \" \";\n"
      "}\n",

      "body{counter-reset:section}"
      "h1{counter-reset:subsection}"
      "h1:before{counter-increment:section;"
      "content:\"Section \" counter(section) \". \"}"
      "h2:before{counter-increment:subsection;"
      "content:counter(section) \".\" counter(subsection) \" \"}" },

    // Don't lowercase font names.
    { "a { font-family: Arial; }",
      "a{font-family:Arial}" },

    // Don't drop precision on large integers (this is 2^31 + 1 which is
    // just larger than larges z-index accepted by chrome, 2^31 - 1).
    { "#foo { z-index: 2147483649; }",
      // Not "#foo{z-index:2.14748e+09}"
      "#foo{z-index:2147483649}" },

    { "#foo { z-index: 123456789012345678901234567890; }",
      "#foo{z-index:123456789012345678901234567890}" },

    // Don't drop precision on long floating point numbers.
    { ".ad-contain .ad-jump {\n"
      "  color: #000;\n"
      "  font: bold 1.54545455em/0.823529412 \"Benton Sans Bold\", Arial, "
      "Helvetica, sans-serif;   /* 17px / 11px; 14px / 17px */\n"
      "  margin-bottom: 1em;\n"
      "}",

      // Note: we drop the leading 0 from 0.823... but not any of the
      // digits of precision.
      ".ad-contain .ad-jump{color:#000;font:bold 1.54545455em/.823529412 "
      "\"Benton Sans Bold\",Arial,Helvetica,sans-serif;margin-bottom:1em}" },

    // Parse and serialize "\n" correctly as "n" and "\A " correctly as newline.
    // But leave the original string without messing with escaping.
    { "a { content: \"Special chars: \\n\\r\\t\\A \\D \\9\" }",
      "a{content:\"Special chars: \\n\\r\\t\\A \\D \\9\"}" },

    // Test some interesting combinations of @media.
    { "@media screen {"
      "  body { counter-reset:section }"
      "  h1 { counter-reset:subsection }"
      "}"
      "@media screen,printer { a { color:red } }"
      "@media screen,printer { b { color:green } }"
      "@media screen,printer { c { color:blue } }"
      "@media screen         { d { color:black } }"
      "@media screen,printer { e { color:white } }",

      "@media screen{"
      "body{counter-reset:section}"
      "h1{counter-reset:subsection}"
      "}"
      "@media screen,printer{"
      "a{color:red}"
      "b{color:green}"
      "c{color:#00f}"
      "}"
      "@media screen{d{color:#000}}"
      "@media screen,printer{e{color:#fff}}",
    },

    // Charsets
    { "@charset \"UTF-8\";\n"
      "a { color: red }\n",

      "@charset \"UTF-8\";a{color:red}" },

    // Recovered parse errors:
    // http://github.com/apache/incubator-pagespeed-mod/issues/220
    { ".mui-navbar-wrap, .mui-navbar-clone {"
      "opacity:1;-webkit-transform:translateX(0);"
      "-webkit-transition-property:opacity,-webkit-transform;"
      "-webkit-transition-duration:400ms;}",

      ".mui-navbar-wrap,.mui-navbar-clone{"
      "opacity:1;-webkit-transform:translateX(0);"
      "-webkit-transition-property:opacity , -webkit-transform;"
      "-webkit-transition-duration:400ms}" },

    // IE 8 hack \0/.
    { ".gbxms{background-color:#ccc;display:block;position:absolute;"
      "z-index:1;top:-1px;left:-2px;right:-2px;bottom:-2px;opacity:.4;"
      "-moz-border-radius:3px;"
      "filter:progid:DXImageTransform.Microsoft.Blur(pixelradius=5);"
      "*opacity:1;*top:-2px;*left:-5px;*right:5px;*bottom:4px;"
      "-ms-filter:\"progid:DXImageTransform.Microsoft.Blur(pixelradius=5)\";"
      "opacity:1\\0/;top:-4px\\0/;left:-6px\\0/;right:5px\\0/;bottom:4px\\0/}",

      ".gbxms{background-color:#ccc;display:block;position:absolute;"
      "z-index:1;top:-1px;left:-2px;right:-2px;bottom:-2px;opacity:.4;"
      "-moz-border-radius:3px;"
      "filter:progid:DXImageTransform.Microsoft.Blur(pixelradius=5);"
      "*opacity:1;*top:-2px;*left:-5px;*right:5px;*bottom:4px;"
      "-ms-filter:\"progid:DXImageTransform.Microsoft.Blur(pixelradius=5)\";"
      "opacity:1\\0/;top:-4px\\0/;left:-6px\\0/;right:5px\\0/;bottom:4px\\0/}"},

    // Alexa-100 with parse errors (illegal syntax or CSS3).
    // Comma in values
    { ".cnn_html_slideshow_controls > .cnn_html_slideshow_pager_container >"
      " .cnn_html_slideshow_pager > li\n"
      "{\n"
      "  font-size: 16px;\n"
      "  -webkit-transition-property: color, background-color;\n"
      "  -webkit-transition-duration: 0.5s;\n"
      "}\n",

      ".cnn_html_slideshow_controls>.cnn_html_slideshow_pager_container>"
      ".cnn_html_slideshow_pager>li{"
      "font-size:16px;-webkit-transition-property:color , background-color;"
      "-webkit-transition-duration:.5s}" },

    { "a.login,a.home{position:absolute;right:15px;top:15px;display:block;"
      "float:right;height:29px;line-height:27px;font-size:15px;"
      "font-weight:bold;color:rgba(255,255,255,0.7)!important;color:#fff;"
      "text-shadow:0 -1px 0 rgba(0,0,0,0.2);background:#607890;padding:0 12px;"
      "opacity:.9;text-decoration:none;border:1px solid #2e4459;"
      "-moz-border-radius:6px;-webkit-border-radius:6px;border-radius:6px;"
      "-moz-box-shadow:0 1px 0 rgba(255,255,255,0.15),0 1px 0"
      " rgba(255,255,255,0.15) inset;-webkit-box-shadow:0 1px 0 "
      "rgba(255,255,255,0.15),0 1px 0 rgba(255,255,255,0.15) inset;"
      "box-shadow:0 1px 0 rgba(255,255,255,0.15),0 1px 0 "
      "rgba(255,255,255,0.15) inset}",

      // Note: We do not strip leading 0s from 0.15s below because those
      // sections are passed through verbatim rather than being parsed as
      // decimal numbers.
      "a.login,a.home{position:absolute;right:15px;top:15px;display:block;"
      "float:right;height:29px;line-height:27px;font-size:15px;"
      "font-weight:bold;color:rgba(255,255,255,.7)!important;color:#fff;"
      "text-shadow:0 -1px 0 rgba(0,0,0,.2);background:#607890;padding:0 12px;"
      "opacity:.9;text-decoration:none;border:1px solid #2e4459;"
      "-moz-border-radius:6px;-webkit-border-radius:6px;border-radius:6px;"
      "-moz-box-shadow:0 1px 0 rgba(255,255,255,.15) , 0 1px 0"
      " rgba(255,255,255,.15) inset;-webkit-box-shadow:0 1px 0 "
      "rgba(255,255,255,.15) , 0 1px 0 rgba(255,255,255,.15) inset;"
      "box-shadow:0 1px 0 rgba(255,255,255,.15) , 0 1px 0 "
      "rgba(255,255,255,.15) inset}" },

    // Special chars in property
    { ".authorization .mail .login input, .authorization .pswd input {"
      "float: left; width: 100%; font-size: 75%; -moz-box-sizing: border-box; "
      "-webkit-box-sizing: border-box; box-sizing: border-box; height: 21px; "
      "padding: 2px; #height: 13px}\n"
      ".authorization .mail .domain select {float: right; width: 97%; "
      "#width: 88%; font-size: 75%; height: 21px; -moz-box-sizing: border-box; "
      "-webkit-box-sizing: border-box; box-sizing: border-box}\n"
      ".weather_review .main img.attention {position: absolute; z-index: 5; "
      "left: -10px; top: 6px; width: 29px; height: 26px; \n"
      "background: url('http://limg3.imgsmail.ru/r/weather_new/ico_attention."
      "png'); \n"
      "//background-image: none; \n"
      "filter: progid:DXImageTransform.Microsoft.AlphaImageLoader("
      "src=\"http://limg3.imgsmail.ru/r/weather_new/ico_attention.png\", "
      "sizingMethod=\"crop\"); \n"
      "} \n"
      ".rb_body {font-size: 12px; padding: 0 0 0 10px; overflow: hidden; "
      "text-align: left; //display: inline-block;}\n"
      ".rb_h4 {border-bottom: 1px solid #0857A6; color: #0857A6; "
      "font-size: 17px; font-weight: bold; text-decoration: none;}\n",

      ".authorization .mail .login input,.authorization .pswd input{"
      "float:left;width:100%;font-size:75%;-moz-box-sizing:border-box;"
      "-webkit-box-sizing:border-box;box-sizing:border-box;height:21px;"
      "padding:2px;#height: 13px}"
      ".authorization .mail .domain select{float:right;width:97%;"
      "#width: 88%;font-size:75%;height:21px;-moz-box-sizing:border-box;"
      "-webkit-box-sizing:border-box;box-sizing:border-box}"
      ".weather_review .main img.attention{position:absolute;z-index:5;"
      "left:-10px;top:6px;width:29px;height:26px;"
      "background:url(http://limg3.imgsmail.ru/r/weather_new/ico_attention."
      "png);"
      "//background-image: none;"
      "filter: progid:DXImageTransform.Microsoft.AlphaImageLoader("
      "src=\"http://limg3.imgsmail.ru/r/weather_new/ico_attention.png\", "
      "sizingMethod=\"crop\")}"
      ".rb_body{font-size:12px;padding:0 0 0 10px;overflow:hidden;"
      "text-align:left;//display: inline-block}"
      ".rb_h4{border-bottom:1px solid #0857a6;color:#0857a6;"
      "font-size:17px;font-weight:bold;text-decoration:none}" },

    // Expression
    { ".file_manager .loading { _position: absolute;_top: expression(0+((e=doc"
      "ument.documentElement.scrollTop)?e:document.body.scrollTop)+'px'); "
      "color: red; }\n"
      ".connect_widget .page_stream img{max-width:120px;"
      "width:expression(this.width > 120 ? 120:true); color: red; }\n",

      ".file_manager .loading{_position:absolute;_top: expression(0+((e=doc"
      "ument.documentElement.scrollTop)?e:document.body.scrollTop)+'px');"
      "color:red}"
      ".connect_widget .page_stream img{max-width:120px;"
      "width:expression(this.width > 120 ? 120:true);color:red}" },

    { "#fancybox-loading.fancybox-ie6 {\n"
      "        position: absolute; margin-top: 0;\n"
      "        top: expression( (-20 + (document.documentElement.clientHeight "
      "? document.documentElement.clientHeight/2 : document.body.clientHeight/"
      "2 ) + ( ignoreMe = document.documentElement.scrollTop ? document.docume"
      "ntElement.scrollTop : document.body.scrollTop )) + 'px');\n"
      "}\n",

      "#fancybox-loading.fancybox-ie6{position:absolute;margin-top:0;"
      "top: expression( (-20 + (document.documentElement.clientHeight "
      "? document.documentElement.clientHeight/2 : document.body.clientHeight/"
      "2 ) + ( ignoreMe = document.documentElement.scrollTop ? document.docume"
      "ntElement.scrollTop : document.body.scrollTop )) + 'px')}" },

    // Equals in function
    { ".imdb_lb .header{width:726px;width=728px;height:12px;padding:1px;"
      "border-bottom:1px #000000 solid;background:#eeeeee;font-size:10px;"
      "text-align:left;}"
      ".cboxIE #cboxTopLeft{background:transparent;filter:progid:"
      "DXImageTransform.Microsoft.AlphaImageLoader(src=/images/lb/"
      "internet_explorer/borderTopLeft.png, sizingMethod='scale');}",

      ".imdb_lb .header{width:726px;width=728px;height:12px;padding:1px;"
      "border-bottom:1px #000 solid;background:#eee;font-size:10px;"
      "text-align:left}"
      ".cboxIE #cboxTopLeft{background:transparent;filter:progid:"
      "DXImageTransform.Microsoft.AlphaImageLoader(src=/images/lb/"
      "internet_explorer/borderTopLeft.png, sizingMethod='scale')}" },

    // Special chars in values
    { ".login-form .input-text{ width:144px;padding:6px 3px; "
      "background-color:#fff;background-position:0 -170px;"
      "background-repeat;no-repeat}"
      "td.pop_content .dialog_body{padding:10px;border-bottom:1px# solid #ccc}",

      ".login-form .input-text{width:144px;padding:6px 3px;"
      "background-color:#fff;background-position:0 -170px;"
      "background-repeat;no-repeat}"
      "td.pop_content .dialog_body{padding:10px;border-bottom:1px# solid #ccc}"
    },

    // kSelectorError from Alexa-100
    // Selector list ends in comma
    { ".hp .col ul, {\n"
      "  display: inline !important;\n"
      "  zoom: 1;\n"
      "  vertical-align: top;\n"
      "  margin-left: -10px;\n"
      "  position: relative;\n"
      "}\n",

      ".hp .col ul, {display:inline!important;zoom:1;vertical-align:top;"
      "margin-left:-10px;position:relative}" },

    // Invalid comment type ("//").
    { ".ciuNoteEditBox .topLeft\n"
      "{\n"
      "        background-position:left top;\n"
      "\tbackground-repeat:no-repeat;\n"
      "\tfont-size:4px;\n"
      "\t\n"
      "\t\n"
      "\tpadding: 0px 0px 0px 1px; \n"
      "\t\n"
      "\twidth:7px;\n"
      "}\n"
      "\n"
      "// css hack to make font-size 0px in only ff2.0 and older "
      "(http://pornel.net/firefoxhack)\n"
      ".ciuNoteBox .topLeft,\n"
      ".ciuNoteEditBox .topLeft, x:-moz-any-link {\n"
      "\tfont-size: 0px;\n"
      "}\n",

      ".ciuNoteEditBox .topLeft{background-position:left top;"
      "background-repeat:no-repeat;font-size:4px;padding:0 0 0 1px;"
      "width:7px}// css hack to make font-size 0px in only ff2.0 and older "
      "(http://pornel.net/firefoxhack)\n"
      ".ciuNoteBox .topLeft,\n"
      ".ciuNoteEditBox .topLeft, x:-moz-any-link {font-size:0}" },

    // Parameters for pseudoclass
    { "/* Opera（＋Firefox、Safari） */\n"
      "body:not(:target) .sh_heading_main_b, body:not(:target) "
      ".sh_heading_main_b_wide{\n"
      "  background:url(\"data:image/png;base64,"
      "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAoCAYAAAA/tpB3AAAAQ0lEQVR42k3EMQLAIAg"
      "EMP//WkRQVMB2YLgMae/XMhOLCMzdq3svds7B9t6VmWFrLWzOWakqJiLYGKNiZqz3jh"
      "HR+wBZbpvd95zR6QAAAABJRU5ErkJggg==\") repeat-x left top;\n"
      "}\n"
      "/* Firefox（＋Google Chrome2） */\n"
      "html:not([lang*=""]) .sh_heading_main_b,\n"
      "html:not([lang*=""]) .sh_heading_main_b_wide{\n"
      "\t/* For Mozilla/Gecko (Firefox etc) */\n"
      "\tbackground:-moz-linear-gradient(top, #FFFFFF, #F0F0F0);\n"
      "\t/* For WebKit (Safari, Google Chrome etc) */\n"
      "\tbackground:-webkit-gradient(linear, left top, left bottom, "
      "from(#FFFFFF), to(#F0F0F0));\n"
      "}\n"
      "/* Safari */\n"
      "html:not(:only-child:only-child) .sh_heading_main_b,\n"
      "html:not(:only-child:only-child) .sh_heading_main_b_wide{\n"
      "\t/* For WebKit (Safari, Google Chrome etc) */\n"
      "\tbackground: -webkit-gradient(linear, left top, left bottom, "
      "from(#FFFFFF), to(#F0F0F0));\n"
      "}\n",

      "body:not(:target) .sh_heading_main_b, body:not(:target) "
      ".sh_heading_main_b_wide{background:url(data:image/png;base64,"
      "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAoCAYAAAA/tpB3AAAAQ0lEQVR42k3EMQLAIAg"
      "EMP//WkRQVMB2YLgMae/XMhOLCMzdq3svds7B9t6VmWFrLWzOWakqJiLYGKNiZqz3jh"
      "HR+wBZbpvd95zR6QAAAABJRU5ErkJggg==) repeat-x left top}"
      "html:not([lang*=""]) .sh_heading_main_b,\n"
      "html:not([lang*=""]) .sh_heading_main_b_wide{"
      "background:-moz-linear-gradient(top,#fff,#f0f0f0);"
      "background:-webkit-gradient(linear,left top,left bottom,"
      "from(#fff),to(#f0f0f0))}"
      "html:not(:only-child:only-child) .sh_heading_main_b,\n"
      "html:not(:only-child:only-child) .sh_heading_main_b_wide{"
      "background:-webkit-gradient(linear,left top,left bottom,"
      "from(#fff),to(#f0f0f0))}" },

    // @import stuff
    { "@import \"styles.css\"foo; a { color: red; }",
      "@import url(styles.css) foo;a{color:red}" },

    // @media with no contents
    { "@media; a { color: red; }", "a{color:red}" },
    { "@media screen, print; a { color: red; }", "a{color:red}" },

    // Unexpected @-statements
    { "@-webkit-keyframes wiggle {\n"
      "  0% {-webkit-transform:rotate(6deg);}\n"
      "  50% {-webkit-transform:rotate(-6deg);}\n"
      "  100% {-webkit-transform:rotate(6deg);}\n"
      "}\n"
      "@-moz-keyframes wiggle {\n"
      "  0% {-moz-transform:rotate(6deg);}\n"
      "  50% {-moz-transform:rotate(-6deg);}\n"
      "  100% {-moz-transform:rotate(6deg);}\n"
      "}\n"
      "@keyframes wiggle {\n"
      "  0% {transform:rotate(6deg);}\n"
      "  50% {transform:rotate(-6deg);}\n"
      "  100% {transform:rotate(6deg);}\n"
      "}\n",

      // Rewritten version only has newlines stripped between @-rules.
      "@-webkit-keyframes wiggle {\n"
      "  0% {-webkit-transform:rotate(6deg);}\n"
      "  50% {-webkit-transform:rotate(-6deg);}\n"
      "  100% {-webkit-transform:rotate(6deg);}\n"
      "}"
      "@-moz-keyframes wiggle {\n"
      "  0% {-moz-transform:rotate(6deg);}\n"
      "  50% {-moz-transform:rotate(-6deg);}\n"
      "  100% {-moz-transform:rotate(6deg);}\n"
      "}"
      "@keyframes wiggle {\n"
      "  0% {transform:rotate(6deg);}\n"
      "  50% {transform:rotate(-6deg);}\n"
      "  100% {transform:rotate(6deg);}\n"
      "}" },

    // @font-face
    // TODO(sligocki): src: attributes with multiple attributes are not parsed.
    { "@font-face {font-family:'Ubuntu';font-style:normal;font-weight:normal;"
      "src:local('Ubuntu'), url('http://themes.googleusercontent.com/static/"
      "fonts/ubuntu/v2/2Q-AW1e_taO6pHwMXcXW5w.ttf') format('truetype')}"
      "@font-face{font-family:'Ubuntu';font-style:normal;font-weight:bold;"
      "src:local('Ubuntu Bold'), local('Ubuntu-Bold'), url('http://themes."
      "googleusercontent.com/static/fonts/ubuntu/v2/0ihfXUL2emPh0ROJezvraKCWc"
      "ynf_cDxXwCLxiixG1c.ttf') format('truetype')}",

      "@font-face{font-family:'Ubuntu';font-style:normal;font-weight:normal;"
      "src:local('Ubuntu') , url(http://themes.googleusercontent.com/static/"
      "fonts/ubuntu/v2/2Q-AW1e_taO6pHwMXcXW5w.ttf) format('truetype')}"
      "@font-face{font-family:'Ubuntu';font-style:normal;font-weight:bold;"
      "src:local('Ubuntu Bold') , local('Ubuntu-Bold') , url(http://themes."
      "googleusercontent.com/static/fonts/ubuntu/v2/0ihfXUL2emPh0ROJezvraKCWc"
      "ynf_cDxXwCLxiixG1c.ttf) format('truetype')}" },

    { "@font-face { font-family: 'Roboto Condensed'; font-style: normal; "
      "font-weight: 400; src: local('Roboto Condensed Regular'), "
      "local('RobotoCondensed-Regular'), url(http://stc.v3.news.zing.vn/css/"
      "fonts/Roboto-Condensed.woff) format('woff'); }\n",

      "@font-face{font-family:'Roboto Condensed';font-style:normal;"
      "font-weight:400;src:local('Roboto Condensed Regular') , "
      "local('RobotoCondensed-Regular') , url(http://stc.v3.news.zing.vn/css/"
      "fonts/Roboto-Condensed.woff) format('woff')}" },

    // Multiple src properties.
    { "@font-face {\n"
      "  font-family: 'BebasNeueRegular';\n"
      "  src: url('fonts/BebasNeue-webfont.eot');\n"
      "  src: url('fonts/BebasNeue-webfont.eot?#iefix') "
      "format('embedded-opentype'),\n"
      "    url('fonts/BebasNeue-webfont.woff') format('woff'),\n"
      "    url('fonts/BebasNeue-webfont.ttf') format('truetype'),\n"
      "    url('fonts/BebasNeue-webfont.svg#BebasNeueRegular') format('svg');\n"
      "  font-weight: normal;\n"
      "  font-style: normal;\n"
      "}\n",

      "@font-face{font-family:'BebasNeueRegular';"
      "src:url(fonts/BebasNeue-webfont.eot);"
      "src:url(fonts/BebasNeue-webfont.eot?#iefix) "
      "format('embedded-opentype') , "
      "url(fonts/BebasNeue-webfont.woff) format('woff') , "
      "url(fonts/BebasNeue-webfont.ttf) format('truetype') , "
      "url(fonts/BebasNeue-webfont.svg#BebasNeueRegular) format('svg');"
      "font-weight:normal;font-style:normal}" },

    // @font-face inside @media
    { "@media screen and (-webkit-min-device-pixel-ratio:0) {\n"
      "  @font-face {\n"
      "    font-family: 'tiefontello';\n"
      "    src: url('fonts/tiefontello.svg?88026028#fontello') format('svg');\n"
      "  }\n"
      "}\n",

      "@media screen and (-webkit-min-device-pixel-ratio:0){"
      "@font-face{font-family:'tiefontello';"
      "src:url(fonts/tiefontello.svg?88026028#fontello) format('svg')}}" },

    // CSS3 media queries.
    // http://github.com/apache/incubator-pagespeed-mod/issues/50
    { "@media only screen and (min-device-width: 320px) and"
      " (max-device-width: 480px) {\n"
      "        body {"
      "                padding: 0;\n"
      "        }\n"
      "        #page {\n"
      "                margin-top: 0;\n"
      "        }\n"
      "        #branding {\n"
      "                border-top: none;\n"
      "        }\n"
      "\n"
      "}\n",

      "@media only screen and (min-device-width:320px) and (max-device-width:"
      "480px){body{padding:0}#page{margin-top:0}#branding{border-top:none}}" },

    // Make sure we distinguish similar media queries.
    { "@media screen { .a { color: red; } }\n"
      "@media screen and (color) { .b { color: green; } }\n"
      "@media not screen { .c { color: blue; } }\n"
      "@media only screen { .d { color: cyan; } }\n",

      "@media screen{.a{color:red}}"
      "@media screen and (color){.b{color:green}}"
      "@media not screen{.c{color:#00f}}"
      "@media only screen{.d{color:#0ff}}" },

    // http://github.com/apache/incubator-pagespeed-mod/issues/575
    { "[class^=\"icon-\"],[class*=\" icon-\"] { color: red }",
      "[class^=\"icon-\"],[class*=\" icon-\"]{color:red}" },

    // Don't "fix" quirks-mode colors.
    // Note: DECAFB is not converted to a more correct #decafb.
    { "body { color: DECAFB }", "body{color: DECAFB }" },

    { "#post_content, #post_content p{\n"
      " font-family:Helvetica;\n"
      " font-size:16px;\n"
      " color:333;\n"
      " line-height:24px;\n"
      " margin-bottom:15px;\n"
      "}",

      // Note: 333 is not converted to a more correct #333.
      "#post_content,#post_content p{font-family:Helvetica;font-size:16px;"
      "color:333;line-height:24px;margin-bottom:15px}" },

    // Properly deal with space in URL.
    { "#ac { background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA"
      "AAG4AAAAfCAA AAAAjTqdDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZS\") no-repeat; }",

      // Note we unquote the url() and escape the space with a backslash.
      // It's fine if we choose a different strategy in the future. We just
      // need to make sure that the space is not left verbatim in the
      // unquoted url().
      "#ac{background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA"
      "AAG4AAAAfCAA\\ AAAAjTqdDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZS) no-repeat}" },

    // Noticed from YUI minification.
    // https://github.com/apache/incubator-pagespeed-mod/issues/614
    { "td { line-height: 0.8em; margin: -0.9in; }",
      "td{line-height:.8em;margin:-.9in}" },

    // ::- in selectors
    { "::-moz-selection {background: #f36921 ; color: #fff ; text-shadow:none}",
      "::-moz-selection{background:#f36921;color:#fff;text-shadow:none}" },

    // Sloppy color syntax
    // Note that according to the CSS spec: 0000ff should be parsed as
    // DIM(0, ff) and then serialized as 0ff, but browsers will parse both
    // of these values as quirks-mode colors and thus we would be changing
    // the links from blue to cyan.
    // https://github.com/apache/incubator-pagespeed-mod/issues/639
    { "A:link, A:visited { color: 0000ff }",
      "A:link,A:visited{color: 0000ff }" },

    // Unexpected ! uses in declarations.
    { ".filehistory a img,#file img:hover{background:white url(data:image/png;"
      "base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGElEQVQYV2N4DwX/"
      "oYBhgARgDJjEAAkAAEC99wFuu0VFAAAAAElFTkSuQmCC) repeat;"
      "background:white url(http://static.uncyc.org/skins/common/images/Checke"
      "r-16x16.png?2012-02-15T12:25:00Z) repeat!ie}",

      ".filehistory a img,#file img:hover{background:#fff url(data:image/png;"
      "base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGElEQVQYV2N4DwX/"
      "oYBhgARgDJjEAAkAAEC99wFuu0VFAAAAAElFTkSuQmCC) repeat;"
      "background:white url(http://static.uncyc.org/skins/common/images/Checke"
      "r-16x16.png?2012-02-15T12:25:00Z) repeat!ie}" },

    { "body{font:13px/1.231,clean;*font-size:small;*font:x-small;"
      "font-family:Arial !important}",

      "body{font:13px/1.231,clean;*font-size:small;*font:x-small;"
      "font-family:Arial!important}" },

    // https://github.com/apache/incubator-pagespeed-mod/issues/722
    { ".a { color: red; }\n"
      "@import url('foo.css');\n"
      ".b { color: blue; }\n",

      ".a{color:red}@import url('foo.css');.b{color:#00f}" },

    // Ignoring chars at end of function.
    { "* html #profilePhoto {\n"
      "  height: expression((this.flag == undefined) ? (this.scrollHeight > "
      "500) ? this.flag = '500px' : 'auto' : '');\n"
      "}",

      "* html #profilePhoto{height: expression((this.flag == undefined) ? "
      "(this.scrollHeight > 500) ? this.flag = '500px' : 'auto' : '')}" },

    // Nonsense: (rgba(...)) (last line).
    // From http://yandex.st/www/1.473/touch-bem/pages-touch/index/_index.css
    { ".b-search__button{position:relative;min-width:50px;margin:0 0 0 -1px;"
      "padding:1px 0 1px 1px;-webkit-border-top-left-radius:2px;"
      "border-top-left-radius:2px;-webkit-border-bottom-left-radius:2px;"
      "border-bottom-left-radius:2px;background:-webkit-gradient(linear,0 0,"
      "0 100%,from(rgba(192,192,192,.6)),to(rgba(49,49,49,.3)));"
      "background:-o-linear-gradient(top,(rgba(192,192,192,.6)),"
      "(rgba(49,49,49,.3)))}",

      ".b-search__button{position:relative;min-width:50px;margin:0 0 0 -1px;"
      "padding:1px 0 1px 1px;-webkit-border-top-left-radius:2px;"
      "border-top-left-radius:2px;-webkit-border-bottom-left-radius:2px;"
      "border-bottom-left-radius:2px;background:-webkit-gradient(linear,0 0,"
      "0 100%,from(rgba(192,192,192,.6)),to(rgba(49,49,49,.3)));"
      "background:-o-linear-gradient(top,(rgba(192,192,192,.6)),"
      "(rgba(49,49,49,.3)))}" },

    // Malformed: media type, "screen", must be first.
    { "@media (color) and screen { .a { color: red; } }",

      "@media (color) and screen { .a { color: red; } }" },

    // Do not "fix" by putting a space between 'and' and '('.
    { "@media only screen and(-webkit-min-device-pixel-ratio:1.5),"
      "only screen and(min--moz-device-pixel-ratio:1.5),"
      "only screen and(min-resolution:240dpi){"
      ".ui-icon-plus,.ui-icon-minus,.ui-icon-delete,.ui-icon-arrow-r,"
      ".ui-icon-arrow-l,.ui-icon-arrow-u,.ui-icon-arrow-d,.ui-icon-check,"
      ".ui-icon-gear,.ui-icon-refresh,.ui-icon-forward,.ui-icon-back,"
      ".ui-icon-grid,.ui-icon-star,.ui-icon-alert,.ui-icon-info,.ui-icon-home,"
      ".ui-icon-search,.ui-icon-searchfield:after,.ui-icon-checkbox-off,"
      ".ui-icon-checkbox-on,.ui-icon-radio-off,.ui-icon-radio-on{"
      "background-image:url(images/icons-36-white.png);"
      "-moz-background-size:776px 18px;-o-background-size:776px 18px;"
      "-webkit-background-size:776px 18px;background-size:776px 18px;}"
      ".ui-icon-alt{background-image:url(images/icons-36-black.png);}}",

      "@media only screen and(-webkit-min-device-pixel-ratio:1.5),"
      "only screen and(min--moz-device-pixel-ratio:1.5),"
      "only screen and(min-resolution:240dpi){"
      ".ui-icon-plus,.ui-icon-minus,.ui-icon-delete,.ui-icon-arrow-r,"
      ".ui-icon-arrow-l,.ui-icon-arrow-u,.ui-icon-arrow-d,.ui-icon-check,"
      ".ui-icon-gear,.ui-icon-refresh,.ui-icon-forward,.ui-icon-back,"
      ".ui-icon-grid,.ui-icon-star,.ui-icon-alert,.ui-icon-info,.ui-icon-home,"
      ".ui-icon-search,.ui-icon-searchfield:after,.ui-icon-checkbox-off,"
      ".ui-icon-checkbox-on,.ui-icon-radio-off,.ui-icon-radio-on{"
      "background-image:url(images/icons-36-white.png);"
      "-moz-background-size:776px 18px;-o-background-size:776px 18px;"
      "-webkit-background-size:776px 18px;background-size:776px 18px;}"
      ".ui-icon-alt{background-image:url(images/icons-36-black.png);}}" },

    // No space between "and" and "(".
    { "@media all and(-webkit-max-device-pixel-ratio:10000),\n"
      "   not all and(-webkit-min-device-pixel-ratio:0) {\n"
      "\n"
      "\t:root .RadTreeView_rtl .rtPlus,\n"
      "\t:root .RadTreeView_rtl .rtMinus\n"
      "\t{\n"
      "\t\tposition: relative;\n"
      "\t\tmargin-left: 2px;\n"
      "\t\tmargin-right: -13px;\n"
      "\t\tright: -15px;\n"
      "\t}\n"
      "}\n",

      "@media all and(-webkit-max-device-pixel-ratio:10000),\n"
      "   not all and(-webkit-min-device-pixel-ratio:0) {\n"
      "\n"
      "\t:root .RadTreeView_rtl .rtPlus,\n"
      "\t:root .RadTreeView_rtl .rtMinus\n"
      "\t{\n"
      "\t\tposition: relative;\n"
      "\t\tmargin-left: 2px;\n"
      "\t\tmargin-right: -13px;\n"
      "\t\tright: -15px;\n"
      "\t}\n"
      "}" },

    { "@media screen and (min-width: 0 \\0){.ending-actions li a .icon-top,.en"
      "ding-actions li a .icon-feed{vertical-align:text-bottom}.nav-previous{m"
      "argin:20px auto 0}}",

      "@media screen and (min-width: 0 \\0){.ending-actions li a .icon-top,.en"
      "ding-actions li a .icon-feed{vertical-align:text-bottom}.nav-previous{m"
      "argin:20px auto 0}}" },

    { "@media screen {\n"
      "  .foo { color: red; }\n"
      "  @font-face { font-family: 'Cibric'; }\n"
      "}\n",

      // Note: It is safe to pull @font-face above Rulesets:
      //   http://lists.w3.org/Archives/Public/www-style/2014Sep/0272.html
      // Note: We could combine these both into the same @media block, but
      // I (sligocki) am not convinced it's worth the effort.
      "@media screen{@font-face{font-family:'Cibric'}}"
      "@media screen{.foo{color:red}}" },
  };

  for (int i = 0; i < arraysize(examples); ++i) {
    GoogleString id = StringPrintf("complex_css%d", i);
    ValidateRewrite(id, examples[i][0], examples[i][1], kExpectSuccess);
  }

  const char* parse_fail_examples[] = {
    // Bad syntax
    "}}",
    "@foobar this is totally wrong CSS syntax }",

    // Unclosed @media block.
    "@media screen and (max-width: 478px) {\n"
    "  .main-header h2 {\n"
    "    height: auto;\n"
    "    margin-top: 2px;\n"
    "    width: 70px;\n",

    // Things discovered in the wild by shanemc:

    // Zero width space <U+200B> = \xE2\x80\x8B
    // http://zenpencils.com/wp-content/plugins/zpcustomselectmenu/ddSlick.css
    "   .dd-container { \n"
    "           position:relative; \n"
    "           margin:0px;\n"
    "   }\xE2\x80\x8B \n"
    "   .dd-selected-text { \n"
    "           font-weight:bold;\n"
    "           cursor:pointer !important;\n"
    "   }\xE2\x80\x8B",

    // !important in selectors.
    // From http://mstatic.allegrostatic.pl/allegro/touch/css/style.min.css
    "-moz-appearance:none!important;html{height:100%}",
    "-moz-foo { -webkit-bar: -ie-quuz }",
  };

  DebugWithMessage("<!--CSS rewrite failed: Parse error in %url%-->");
  for (int i = 0; i < arraysize(parse_fail_examples); ++i) {
    GoogleString id = StringPrintf("complex_css_parse_fail%d", i);
    ValidateFailParse(id, parse_fail_examples[i]);
  }
}

// Most tests are run with set_always_rewrite_css(true),
// but all production use has set_always_rewrite_css(false).
// This test makes sure that setting to false still does what we intend.
TEST_F(CssFilterTest, NoAlwaysRewriteCss) {
  // When we force always_rewrite_css, we can expand some statements.
  // Note: when this example is fixed in the minifier, this test will break :/
  options()->ClearSignatureForTesting();
  options()->set_always_rewrite_css(true);
  DebugWithMessage("");
  server_context()->ComputeSignature(options());
  ValidateRewrite("expanding_example",
                  "@import 'http://www.example.com';",
                  "@import url(http://www.example.com);",
                  kExpectSuccess);

  // With it set false, we do not expand CSS (as long as we didn't do anything
  // else, like rewrite sub-resources.
  options()->ClearSignatureForTesting();
  options()->set_always_rewrite_css(false);
  DebugWithMessage("<!--CSS rewrite failed: Cannot improve %url%-->");
  server_context()->ComputeSignature(options());
  ValidateRewrite("non_expanding_example",
                  "@import 'http://www.example.com';",
                  "@import 'http://www.example.com';",
                  kExpectNoChange);

  // When we force always_rewrite_css, we allow rewriting something to nothing.
  options()->ClearSignatureForTesting();
  options()->set_always_rewrite_css(true);
  DebugWithMessage("");
  server_context()->ComputeSignature(options());
  ValidateRewrite("contracting_example", "  ", "", kExpectSuccess);

  // We still contract it with set_always_rewrite_css(false).
  // Note: In the past we did not allow rewrites that resulted in empty output.
  options()->ClearSignatureForTesting();
  options()->set_always_rewrite_css(false);
  server_context()->ComputeSignature(options());
  ValidateRewrite("contracting_example2", "  ", "", kExpectSuccess);
}

TEST_F(CssFilterTest, RemoveComments) {
  ValidateRewrite("remove_comments",
                  " /* This comment will be removed. */ ", "", kExpectSuccess);
}

TEST_F(CssFilterTest, InlineCssOptimizedFromCache) {
  const char in_css[]  = "body {color:DECAFB}";
  const char out_css[] = "body{color:DECAFB}";
  ValidateRewrite("css_html", in_css, out_css, kExpectSuccess);
  ValidateRewrite("css_html", in_css, out_css, kExpectSuccess | kNoStatCheck);
}

TEST_F(CssFilterTest, NoQuirksModeFixes) {
  const char in_css[]  = "body {color:DECAFB}";
  const char out_css[] = "body{color:DECAFB}";

  // Test that we don't parse the CSS in quirks-mode in HTML.
  ValidateRewrite("quirks_html", in_css, out_css, kExpectSuccess);

  // Test that the same thing happens in XHTML.
  SetDoctype(kXhtmlDtd);
  ValidateRewrite("quirks_xhtml", in_css, out_css,
                  kExpectSuccess | kNoStatCheck);
  // Note: We must use kNoStatCheck, because this will use the cached result
  // from the HTML case and thus not record accurate savings.
}

// http://github.com/apache/incubator-pagespeed-mod/issues/324
TEST_F(CssFilterTest, RetainExtraHeaders) {
  GoogleString url = StrCat(kTestDomain, "retain.css");
  SetResponseWithDefaultHeaders(url, kContentTypeCss, kInputStyle, 300);
  TestRetainExtraHeaders("retain.css", "cf", "css");
}

TEST_F(CssFilterTest, RewriteStyleAttribute) {
  // Test that nothing happens if rewriting is disabled (default).
  ValidateNoChanges("RewriteStyleAttribute",
                    "<div style='background-color: #f00; color: yellow;'/>");

  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kDebug);
  options()->EnableFilter(RewriteOptions::kRewriteStyleAttributes);
  server_context()->ComputeSignature(options());

  // Test no rewriting.
  ValidateNoChanges("no-rewriting",
                    "<div style='background-color:red;color:#ff0'/>");

  // Test successful rewriting.
  ValidateExpected("rewrite-simple",
                   "<div style='background-color: #f00; color: yellow;'/>",
                   "<div style='background-color:red;color:#ff0'/>");
  // Rewritten elements with style attributes don't have any log entries.
  EXPECT_EQ(0, rewrite_driver()->request_context()->log_record()->
            logging_info()->rewriter_info().size());


  SetFetchResponse404("404.css");
  static const char kMixedInput[] =
      "<div style=\""
      "  background-image: url('images/watch-icons.png?1');\n"
      "  background-position: -19px 60%;\""
      ">\n"
      "<link rel=stylesheet href='404.css'>\n"
      "<span style=\"font-family: Verdana\">Verdana</span>\n"
      "</div>";
  static const char kMixedOutput[] =
      "<div style=\""
      "background-image:url(images/watch-icons.png?1);"
      "background-position:-19px 60%\""
      ">\n"
      "<link rel=stylesheet href='404.css'>"
      "<!--4xx status code, preventing rewriting of"
      " http://test.com/404.css-->\n"
      "<span style=\"font-family:Verdana\">Verdana</span>\n"
      "</div>";
  ValidateExpected("rewrite-mixed", kMixedInput, kMixedOutput);

  // Test that nothing happens if we have a style attribute on a style element,
  // which is actually invalid.
  ValidateNoChanges("rewrite-style-with-style",
                   "<style style='background-color: #f00; color: yellow;'/>");
}

TEST_F(CssFilterTest, RewriteStyleAttributeDifferentDirsNoUrl) {
  // Make sure we don't pointlessly produce different cache keys for
  // attribute CSS w/o URLs in different directories.
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kDebug);
  options()->EnableFilter(RewriteOptions::kRewriteStyleAttributes);
  server_context()->ComputeSignature(options());

  const char kCss[] = "color : yellow;  ";
  const char kMinCss[] = "color:#ff0";

  ValidateExpected("main_page",
                   StrCat("<div style='", kCss, "'/>"),
                   StrCat("<div style='", kMinCss, "'/>"));

  ValidateExpected("subdir/file",
                   StrCat("<div style='", kCss, "'/>"),
                   StrCat("<div style='", kMinCss, "'/>"));

  EXPECT_EQ(1, statistics()->GetVariable(CssFilter::kBlocksRewritten)->Get());
}

TEST_F(CssFilterTest, RewriteStyleAttributeDifferentDirsAbsUrl) {
  // Make sure we don't pointlessly produce different cache keys for
  // attribute CSS with same absolute URLs in different directories.
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kDebug);
  options()->EnableFilter(RewriteOptions::kRewriteStyleAttributes);
  server_context()->ComputeSignature(options());

  const char kCss[] = "background : url(http://example.com/artful.png); ";
  const char kMinCss[] = "background:url(http://example.com/artful.png)";

  ValidateExpected("main_page",
                   StrCat("<div style='", kCss, "'/>"),
                   StrCat("<div style='", kMinCss, "'/>"));

  ValidateExpected("subdir/file",
                   StrCat("<div style='", kCss, "'/>"),
                   StrCat("<div style='", kMinCss, "'/>"));

  EXPECT_EQ(1, statistics()->GetVariable(CssFilter::kBlocksRewritten)->Get());
}

TEST_F(CssFilterTest, RewriteStyleAttributeDifferentDirsRelUrl) {
  // When URLs inside the inline CSS are relative (and resolve to different
  // bases) we should get 2 separate rewrites.
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kDebug);
  options()->EnableFilter(RewriteOptions::kRewriteStyleAttributes);
  server_context()->ComputeSignature(options());

  const char kCss[] = "background : url(artful.png); ";
  const char kMinCss[] = "background:url(artful.png)";

  ValidateExpected("main_page",
                   StrCat("<div style='", kCss, "'/>"),
                   StrCat("<div style='", kMinCss, "'/>"));

  ValidateExpected("subdir/file",
                   StrCat("<div style='", kCss, "'/>"),
                   StrCat("<div style='", kMinCss, "'/>"));

  EXPECT_EQ(2, statistics()->GetVariable(CssFilter::kBlocksRewritten)->Get());
}

TEST_F(CssFilterTest, DontAbsolutifyCssImportUrls) {
  // Since we are not using a proxy URL namer (TestUrlNamer) nor any
  // domain rewriting/sharding, we expect the relative URLs in
  // the @import's to be passed though untouched.
  const char styles_filename[] = "styles.css";
  const char styles_css[] =
      ".background_red{background-color:red}"
      ".foreground_yellow{color:#ff0}";
  const GoogleString css_in = StrCat(
      "@import url(media/print.css) print;",
      "@import url(media/screen.css) screen;",
      styles_css);
  SetResponseWithDefaultHeaders(styles_filename, kContentTypeCss, css_in, 100);

  static const char html_prefix[] =
      "<head>\n"
      "  <title>Example style outline</title>\n"
      "  <!-- Style starts here -->\n"
      "  <style type='text/css'>";
  static const char html_suffix[] = "</style>\n"
      "  <!-- Style ends here -->\n"
      "</head>";

  GoogleString html = StrCat(html_prefix, css_in,  html_suffix);

  ValidateNoChanges("dont_absolutify_css_import_urls", html);
}

TEST_F(CssFilterTest, DontAbsolutifyEmptyUrl) {
  // Ensure that an empty URL is left as-is and is not absolutified.
  const char kEmptyUrlRule[] = "#gallery { list-style: none outside url(''); }";
  const char kNoUrlRule[] = "#gallery{list-style:none outside url()}";
  ValidateRewrite("empty_url_in_rule", kEmptyUrlRule, kNoUrlRule,
                  kExpectSuccess);

  const char kEmptyUrlImport[] = "@import url('');";
  const char kNoUrlImport[] = "@import url();";
  ValidateRewrite("empty_url_in_import", kEmptyUrlImport, kNoUrlImport,
                  kExpectSuccess);
}

TEST_F(CssFilterTest, WebpRewriting) {
  if (RunningOnValgrind()) {  // Too slow under vg.
    return;
  }

  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebp();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kPuzzleJpgFile, kContentTypeJpeg,
                    "x%s.pagespeed.ic.0.webp",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, WebpLaRewriting) {
  if (RunningOnValgrind()) {  // Too slow under vg.
    return;
  }

  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebpLossless();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kPuzzleJpgFile, kContentTypeJpeg,
                    "x%s.pagespeed.ic.0.webp",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, WebpLaWithFlagRewriting) {
  if (RunningOnValgrind()) {  // Too slow under vg.
    return;
  }

  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kConvertToWebpLossless);
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebpLossless();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kPuzzleJpgFile, kContentTypeJpeg,
                    "x%s.pagespeed.ic.0.webp",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, NoWebpRewritingFromJpgIfDisabled) {
  options()->ClearSignatureForTesting();
  options()->DisableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressJpeg);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebp();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kPuzzleJpgFile, kContentTypeJpeg,
                    "x%s.pagespeed.ic.0.jpg",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, WebpRewritingFromJpgWithWebpFlagWebpLaUa) {
  if (RunningOnValgrind()) {  // Too slow under vg.
    return;
  }

  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressJpeg);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kConvertToWebpLossless);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebpLossless();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kPuzzleJpgFile, kContentTypeJpeg,
                    "x%s.pagespeed.ic.0.webp",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, WebpRewritingFromJpgWithWebpFlagWebpUa) {
  if (RunningOnValgrind()) {  // Too slow under vg.
    return;
  }

  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressJpeg);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kConvertToWebpLossless);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebp();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kPuzzleJpgFile, kContentTypeJpeg,
                    "x%s.pagespeed.ic.0.webp",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, NoWebpLaRewritingFromJpgIfDisabled) {
  options()->ClearSignatureForTesting();
  options()->DisableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressJpeg);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebpLossless();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kPuzzleJpgFile, kContentTypeJpeg,
                    "x%s.pagespeed.ic.0.jpg",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, NoWebpRewritingFromPngIfDisabled) {
  options()->ClearSignatureForTesting();
  options()->DisableFilter(RewriteOptions::kConvertPngToJpeg);
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressPng);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebp();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kBikePngFile, kContentTypePng,
                    "x%s.pagespeed.ic.0.png",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, WebpRewritingFromPngWithWebpFlagWebpLaUa) {
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kConvertPngToJpeg);
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressPng);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kConvertToWebpLossless);
  options()->set_image_recompress_quality(85);
  SetupForWebpLossless();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kBikePngFile, kContentTypePng,
                    "x%s.pagespeed.ic.0.webp",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, WebpRewritingFromPngWithWebpFlagWebpUa) {
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kConvertPngToJpeg);
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressPng);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kConvertToWebpLossless);
  options()->set_image_recompress_quality(85);
  SetupForWebp();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kBikePngFile, kContentTypePng,
                    "x%s.pagespeed.ic.0.webp",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, NoWebpLaRewritingFromPngIfDisabled) {
  options()->ClearSignatureForTesting();
  options()->DisableFilter(RewriteOptions::kConvertPngToJpeg);
  options()->EnableFilter(RewriteOptions::kConvertJpegToWebp);
  options()->EnableFilter(RewriteOptions::kRecompressPng);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->set_image_jpeg_recompress_quality(85);
  SetupForWebpLossless();
  server_context()->ComputeSignature(options());

  TestWebpRewriting(kBikePngFile, kContentTypePng,
                    "x%s.pagespeed.ic.0.png",
                    "A.%s.pagespeed.cf.0.css");
}

TEST_F(CssFilterTest, DontAbsolutifyUrlsIfNoDomainMapping) {
  // We are not using a proxy URL namer (TestUrlNamer) nor any domain
  // rewriting/sharding, so relative URLs can stay relative.
  const char css_input[] =
      "body{background:url(a.png)}"
      "@foobar {background: url(a.png), url( http://test.com/b.png ), "
      "url('sub/c.png'), url( \"/sub/d.png\"  )}";
  // with image rewriting
  TestUrlAbsolutification("dont_absolutify_unparseable_urls_etc_with",
                          css_input, css_input,
                          true  /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          false /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
  // without image rewriting
  TestUrlAbsolutification("dont_absolutify_unparseable_urls_etc_without",
                          css_input, css_input,
                          true  /* expect_unparseable_section */,
                          false /* enable_image_rewriting */,
                          false /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
}

TEST_F(CssFilterTest, AbsolutifyUnparseableUrlsWithDomainMapping) {
  // We are not using a proxy URL namer (TestUrlNamer) but we ARE mapping and
  // sharding domains, so we expect the relative URLs to be absolutified.
  const char css_input[] =
      "body{background:url(a.png)}"
      "@foobar {background: url(a.png), url( http://test.com/b.png ), "
      "url('sub/c.png'), url( \"/sub/d.png\"  )}";
  const char css_output[] =
      "body{background:url(http://cdn2.com/a.png)}"
      "@foobar {background: url(http://cdn2.com/a.png), "
      "url(http://cdn1.com/b.png), "
      "url('http://cdn1.com/sub/c.png'), "
      "url(\"http://cdn2.com/sub/d.png\")}";
  // with image rewriting
  TestUrlAbsolutification("absolutify_unparseable_urls_etc_with",
                          css_input, css_output,
                          true  /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          false /* enable_proxy_mode */,
                          true  /* enable_mapping_and_sharding */);
  // without image rewriting
  TestUrlAbsolutification("absolutify_unparseable_urls_etc_without",
                          css_input, css_output,
                          true  /* expect_unparseable_section */,
                          false /* enable_image_rewriting */,
                          false /* enable_proxy_mode */,
                          true  /* enable_mapping_and_sharding */);
}

TEST_F(CssFilterTest, DontAbsolutifyCursorUrlsWithoutDomainMapping) {
  // Ensure that cursor URLs are left alone when there's nothing to do.
  const char css_input[] =
      ":link,:visited { cursor: url(example.svg) pointer }";
  const char expected_output[] =
      ":link,:visited{cursor:url(example.svg) pointer}";
  // with image rewriting
  TestUrlAbsolutification("dont_absolutify_cursor_urls_etc_with",
                          css_input, expected_output,
                          false /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          false /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
  // without image rewriting
  TestUrlAbsolutification("dont_absolutify_cursor_urls_etc_without",
                          css_input, expected_output,
                          false /* expect_unparseable_section */,
                          false /* enable_image_rewriting */,
                          false /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
}

TEST_F(CssFilterTest, AbsolutifyCursorUrlsWithDomainMapping) {
  // Ensure that cursor URLs are correctly absolutified.
  const char css_input[] =
      ":link,:visited { cursor: url(example.svg) pointer }";
  const char expected_output[] =
      ":link,:visited{cursor:url(http://cdn2.com/example.svg) pointer}";
  TestUrlAbsolutification("absolutify_cursor_urls_with_domain_mapping",
                          css_input, expected_output,
                          false /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          false /* enable_proxy_mode */,
                          true  /* enable_mapping_and_sharding */);
}

TEST_F(CssFilterTest, SimpleFetch) {
  SetResponseWithDefaultHeaders(StrCat(kTestDomain, "style.css"),
                                kContentTypeCss, kInputStyle, 100);
  GoogleString output;
  ASSERT_TRUE(FetchResourceUrl(
      Encode(kTestDomain, "cf", "0", "style.css", "css"), &output));
  EXPECT_EQ(kOutputStyle, output);
}

TEST_F(CssFilterTest, SimpleFetchUnhealthy) {
  lru_cache()->set_is_healthy(false);
  SetResponseWithDefaultHeaders(StrCat(kTestDomain, "style.css"),
                                kContentTypeCss, kInputStyle, 100);
  GoogleString output;
  ASSERT_TRUE(FetchResourceUrl(
      Encode(kTestDomain, "cf", "0", "style.css", "css"), &output));
  EXPECT_EQ(kOutputStyle, output);
}

// Make sure we correctly decode the previously unexpected I.. format.
// http://github.com/apache/incubator-pagespeed-mod/issues/427
TEST_F(CssFilterTest, EmptyLeafFetch) {
  // CSS URL ends in /
  SetResponseWithDefaultHeaders(StrCat(kTestDomain, "style/"),
                                kContentTypeCss, kInputStyle, 100);

  GoogleString output;
  // Note: We intentionally do not use Encode() to make this test as explicit
  // as possible. We just want to test that we correctly deal with the
  // unexpected I.. format. EmptyLeafFull tests the full flow and thus
  // will continue to test the right thing if the encoding changes.
  ASSERT_TRUE(FetchResourceUrl(
      StrCat(kTestDomain, "style/I..pagespeed.cf.Hash.css"), &output));
  EXPECT_EQ(kOutputStyle, output);
}

// Make sure we correctly rewrite, encode and decode a CSS URL with empty leaf.
// http://github.com/apache/incubator-pagespeed-mod/issues/427
TEST_F(CssFilterTest, EmptyLeafFull) {
  // CSS URL ends in /
  ValidateRewriteExternalCssUrl("empty_leaf", StrCat(kTestDomain, "style/"),
                                kInputStyle, kOutputStyle, kExpectSuccess);
}

TEST_F(CssFilterTest, UnauthorizedCssResource) {
  GoogleUrl gurl("http://unauth.example.com/style.css");
  DebugWithMessage(StrCat(
      "<!--",
      rewrite_driver()->GenerateUnauthorizedDomainDebugComment(
          gurl, RewriteDriver::InputRole::kStyle),
      "-->"));
  ValidateRewriteExternalCssUrl("unauth", gurl.Spec(),
                                kInputStyle, kInputStyle, kExpectNoChange);
}

TEST_F(CssFilterTest, FlushInInlineCss) {
  SetupWriter();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText("<html><body><style>.a { co");
  // Flush in middle of inline CSS.
  rewrite_driver()->Flush();
  rewrite_driver()->ParseText("lor: red; }</style></body></html>");
  rewrite_driver()->FinishParse();

  // Expect text to be rewritten because it is coalesced.
  // HtmlParse will send events like this to filter:
  //   StartElement style
  //   Flush
  //   Characters ...
  //   EndElement style
  EXPECT_EQ("<html><body><style>.a{color:red}</style></body></html>",
            output_buffer_);
  // Inlined rewritten css don't have any log entries.
  EXPECT_EQ(0, rewrite_driver()->request_context()->log_record()->
            logging_info()->rewriter_info().size());
}

TEST_F(CssFilterTest, InlineCssWithExternalUrlAndDelayCache) {
  GoogleString img_url = StrCat(kTestDomain, "a.jpg");
  AddFileToMockFetcher(img_url, kPuzzleJpgFile, kContentTypeJpeg, 100);
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kDebug);
  options()->EnableFilter(RewriteOptions::kRecompressJpeg);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  server_context()->ComputeSignature(options());

  // Delay the http cache lookup for the image so that it is not rewritten.
  delay_cache()->DelayKey(HttpCacheKey(img_url));

  SetupWriter();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText(
      "<html><body>"
      "<style>body{background:url(a.jpg)}</style>");
  rewrite_driver()->Flush();
  rewrite_driver()->ParseText("</body></html>");
  delay_cache()->ReleaseKey(HttpCacheKey(img_url));
  rewrite_driver()->FinishParse();

  EXPECT_EQ("<html><body><style>body{background:url(a.jpg)}</style>"
            "<!--deadline_exceeded for filter CssFilter--></body></html>",
            output_buffer_);

  // There was previously a bug where we were logging this as a successful
  // application of the css filter. Make sure that this case isn't logged.
  EXPECT_STREQ("", AppliedRewriterStringFromLog());
}

TEST_F(CssFilterTest, FlushInEndTag) {
  SetupWriter();
  rewrite_driver()->StartParse(kTestDomain);
  rewrite_driver()->ParseText("<html><body><style>.a { color: red; }</st");
  // Flush in middle of closing </style> tag.
  rewrite_driver()->Flush();
  rewrite_driver()->ParseText("yle></body></html>");
  rewrite_driver()->FinishParse();

  // Expect text to be rewritten because it is coalesced.
  // HtmlParse will send events like this to filter:
  //   StartElement style
  //   Characters ...
  //   Flush
  //   EndElement style
  EXPECT_EQ("<html><body><style>.a{color:red}</style></body></html>",
            output_buffer_);
}

// See: http://www.alistapart.com/articles/alternate/
//  and http://www.w3.org/TR/html4/present/styles.html#h-14.3.1
TEST_F(CssFilterTest, AlternateStylesheet) {
  SetResponseWithDefaultHeaders("foo.css", kContentTypeCss, kInputStyle, 100);

  const char html_format[] = "<link rel='%s' href='%s' title='foo'>";
  const GoogleString new_url = Encode("", "cf", "0", "foo.css", "css");

  ValidateExpected("preferred_stylesheet",
                   StringPrintf(html_format, "stylesheet", "foo.css"),
                   StringPrintf(html_format, "stylesheet", new_url.c_str()));

  ValidateExpected("alternate_stylesheet",
                   StringPrintf(html_format, "alternate stylesheet", "foo.css"),
                   StringPrintf(html_format, "alternate stylesheet",
                                new_url.c_str()));

  ValidateExpected("alternate_stylesheet2",
                   StringPrintf(html_format, " StyleSheet alterNATE  ",
                                "foo.css"),
                   StringPrintf(html_format, " StyleSheet alterNATE  ",
                                new_url.c_str()));

  ValidateExpected("alternate_stylesheet_and_more",
                   StringPrintf(html_format, "  foo stylesheet alternate bar ",
                                "foo.css"),
                   StringPrintf(html_format, "  foo stylesheet alternate bar ",
                                new_url.c_str()));

  ValidateNoChanges("alternate_not_stylesheet",
                    StringPrintf(html_format, "alternate snowflake",
                                 "foo.css"));
}

TEST_F(CssFilterTest, AbsolutifyServingFallback) {
  const char css_input[] =
      "@import url(x.ss);\n"
      "body { background: url(a.png); }\n";
  const char expected_output[] =
      "@import url(http://cdn.example.com/x.ss);"
      "body{background:url(http://cdn.example.com/a.png)}";

  UseMd5Hasher();
  options()->ClearSignatureForTesting();
  options()->SetRewriteLevel(RewriteOptions::kPassThrough);
  options()->EnableFilter(RewriteOptions::kDebug);
  options()->EnableFilter(RewriteOptions::kRewriteCss);
  options()->EnableFilter(RewriteOptions::kRewriteDomains);
  DomainLawyer* domain_lawyer = options()->WriteableDomainLawyer();
  domain_lawyer->AddRewriteDomainMapping("http://cdn.example.com", kTestDomain,
                                         message_handler());
  server_context()->ComputeSignature(options());
  SetResponseWithDefaultHeaders("foo.css", kContentTypeCss, css_input, 100);

  GoogleString url = Encode(kTestDomain, "cf", "0" /* wrong hash */,
                            "foo.css", "css");

  TestFallbackFetch(url, expected_output);
  TestFallbackFetch(url, expected_output);
}

TEST_F(CssFilterTest, BasicCsp) {
  EnableDebug();
  SetResponseWithDefaultHeaders("styles/a.css",
                                kContentTypeCss, kInputStyle, 100);
  SetResponseWithDefaultHeaders("uploads/sneaky.png",
                                kContentTypeCss, kInputStyle, 100);

  static const char kCsp[] = "<meta http-equiv=\"Content-Security-Policy\" "
                             "content=\"style-src */styles/ \">";
  ValidateExpected(
      "basic_csp",
      StrCat(kCsp,
             CssLinkHref("styles/a.css"),
             CssLinkHref("uploads/sneaky.png")),
      StrCat(kCsp,
             CssLinkHref(Encode("styles/", "cf", "0", "a.css", "css")),
             CssLinkHref("uploads/sneaky.png"),
             "<!--The preceding resource was not rewritten "
             "because CSP disallows its fetch-->"));
}

TEST_F(CssFilterTest, InlineCsp) {
  EnableDebug();
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kRewriteStyleAttributes);
  server_context()->ComputeSignature(options());

  static const char kCsp[] = "<meta http-equiv=\"Content-Security-Policy\" "
                             "content=\"style-src */styles/ \">";
  static const char kCss[] = "<style>* { display: stylish; }</style>";
  static const char kStyledDiv[] = "<div style='background-color: #f00; '/>";

  ValidateExpected(
      "inline_css",
      StrCat(kCsp, kCss, kStyledDiv),
      StrCat(kCsp, kCss,
             "<!--Avoiding modifying inline style with CSP present-->",
             kStyledDiv,
             "<!--Avoiding modifying inline style with CSP present-->"));
}

TEST_F(CssFilterTest, RenderCsp) {
  EnableDebug();
  SetResponseWithDefaultHeaders("styles/a.css",
                                kContentTypeCss, kInputStyle, 100);
  SetResponseWithDefaultHeaders("uploads/sneaky.png",
                                kContentTypeCss, kInputStyle, 100);

  static const char kCsp[] = "<meta http-equiv=\"Content-Security-Policy\" "
                             "content=\"style-src */styles/a.css \">";

  // No CSP -> fine.
  ValidateExpected(
      "no_csp",
      CssLinkHref("styles/a.css"),
      CssLinkHref(Encode("styles/", "cf", "0", "a.css", "css")));

  // CSP applied at render time, from cached result.
  ValidateExpected(
      "render_csp",
      StrCat(kCsp, CssLinkHref("styles/a.css")),
      StrCat(kCsp, CssLinkHref("styles/a.css"),
             "<!--PageSpeed output (by CssFilter) not permitted by "
             "Content Security Policy-->"));
}

TEST_F(CssFilterTest, InlineCspIrrelevant) {
  EnableDebug();
  options()->ClearSignatureForTesting();
  options()->EnableFilter(RewriteOptions::kRewriteStyleAttributes);
  server_context()->ComputeSignature(options());

  // We don't worry about not-style, not-default CSP.
  static const char kCsp[] = "<meta http-equiv=\"Content-Security-Policy\" "
                             "content=\"img-src */images/ \">";

  static const char kCss[] = "<style>* { display: stylish; }</style>";
  static const char kMinCss[] = "<style>*{display:stylish}</style>";
  static const char kStyledDiv[] = "<div style='background-color: #f00; '/>";
  static const char kMinStyledDiv[] = "<div style='background-color:red'/>";

  ValidateExpected(
      "inline_css",
      StrCat(kCsp, kCss, kStyledDiv),
      StrCat(kCsp, kMinCss, kMinStyledDiv));
}

class CssFilterTestUrlNamer : public CssFilterTest {
 public:
  CssFilterTestUrlNamer() {
    // We need a subclass to do this because of the timing of construction
    // and SetUp calls, and doing it after all that doesn't inject it in all
    // right places.
    SetUseTestUrlNamer(true);
  }
};

TEST_F(CssFilterTestUrlNamer, AbsolutifyUnparseableUrls) {
  // Here we ARE using a proxy URL namer (TestUrlNamer) so the URLs in
  // unparseable CSS must be absolutified.
  const char css_input[] =
      "@foobar { background: url(a.png), url( http://test.com/b.png ), "
      "url('sub/c.png'), url( \"/sub/d.png\"  ); }\n";
  const char expected_output[] =
      "@foobar { background: "
      "url(http://test.com/a.png), "
      "url( http://test.com/b.png ), "  // already absolute means no change
      "url('http://test.com/sub/c.png'), "
      "url(\"http://test.com/sub/d.png\"); }";
  // with image rewriting
  TestUrlAbsolutification("absolutify_unparseable_urls_with",
                          css_input, expected_output,
                          true  /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          true  /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
  // without image rewriting
  TestUrlAbsolutification("do_absolutify_unparseable_urls_without",
                          css_input, expected_output,
                          true  /* expect_unparseable_section */,
                          false /* enable_image_rewriting */,
                          true  /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
}

TEST_F(CssFilterTestUrlNamer, AbsolutifyParseableUrls) {
  // Here we are using a proxy URL namer (TestUrlNamer) but the URLs in the
  // CSS isn't rewritten by the image rewriter, but we still must absolutify.
  const char css_input[] =
      "body { background: url(a.png); }\n";
  const char expected_output[] =
      "body{background:url(http://test.com/a.png)}";
  // with image rewriting
  TestUrlAbsolutification("absolutify_parseable_urls_with",
                          css_input, expected_output,
                          false /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          true  /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
  // without image rewriting
  TestUrlAbsolutification("absolutify_parseable_urls_without",
                          css_input, expected_output,
                          false /* expect_unparseable_section */,
                          false /* enable_image_rewriting */,
                          true  /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
}

TEST_F(CssFilterTestUrlNamer, AbsolutifyOtherUrlsWithProxy) {
  // Ensure that non-rewritten URLs are correctly absolutified.
  const char css_input[] =
      ":link,:visited { cursor: url(example.svg) pointer }\n"
      ".png .itab_prev { behavior: url(/js/iepngfix.htc) }\n"
      ".foo { bar: url('baz.ext'); }";
  const char expected_output[] =
      ":link,:visited{cursor:url(http://test.com/example.svg) pointer}"
      ".png .itab_prev{behavior:url(http://test.com/js/iepngfix.htc)}"
      ".foo{bar:url(http://test.com/baz.ext)}";
  TestUrlAbsolutification("absolutify_other_urls_with_proxy",
                          css_input, expected_output,
                          false /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          true  /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
}

TEST_F(CssFilterTestUrlNamer, AbsolutifyWithBom) {
  // We ARE using a proxy URL namer (TestUrlNamer) so the URLs in unparseable
  // CSS must be absolutified. The CSS is unparseable because of the BOM.
  const char css_input[] =
      "\xEF\xBB\xBF"
      "@import url(x.ss);\n"
      "body { background: url(a.png); }\n";
  const char expected_output[] =
      "\xEF\xBB\xBF"
      "@import url(http://test.com/x.ss);"
      "body{background:url(http://test.com/a.png)}";
  // with image rewriting
  TestUrlAbsolutification("absolutify_with_bom_with",
                          css_input, expected_output,
                          true  /* expect_unparseable_section */,
                          true  /* enable_image_rewriting */,
                          true  /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
  // without image rewriting
  TestUrlAbsolutification("do_absolutify_with_bom_without",
                          css_input, expected_output,
                          true  /* expect_unparseable_section */,
                          false /* enable_image_rewriting */,
                          true  /* enable_proxy_mode */,
                          false /* enable_mapping_and_sharding */);
}

}  // namespace

}  // namespace net_instaweb
