/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


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

#include "net/instaweb/rewriter/public/domain_lawyer.h"
#include "net/instaweb/rewriter/public/resource.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/rewrite_test_base.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/http/google_url.h"

namespace net_instaweb {

namespace {

class CountingFilter : public CommonFilter {
 public:
  explicit CountingFilter(RewriteDriver* driver) : CommonFilter(driver),
                                                   start_doc_calls_(0),
                                                   start_element_calls_(0),
                                                   end_element_calls_(0) {}

  virtual void StartDocumentImpl() { ++start_doc_calls_; }
  virtual void StartElementImpl(HtmlElement* element) {
    ++start_element_calls_;
  }
  virtual void EndElementImpl(HtmlElement* element) { ++end_element_calls_; }

  virtual const char* Name() const { return "CommonFilterTest.CountingFilter"; }

  int start_doc_calls_;
  int start_element_calls_;
  int end_element_calls_;
};

class CommonFilterTest : public RewriteTestBase {
 protected:
  virtual void SetUp() {
    RewriteTestBase::SetUp();
    filter_.reset(new CountingFilter(rewrite_driver()));
    rewrite_driver()->AddFilter(filter_.get());
  }

  void ExpectUrl(const GoogleString& expected_url,
                 const GoogleUrl& actual_gurl) {
    EXPECT_EQ(expected_url, actual_gurl.Spec());
  }

  bool CanRewriteResource(CommonFilter* filter, const StringPiece& url) {
    bool unused;
    ResourcePtr resource(filter->CreateInputResource(
        url, RewriteDriver::InputRole::kUnknown, &unused));
    return (resource.get() != NULL);
  }

  CommonFilter* MakeFilter(const StringPiece& base_url,
                           const StringPiece& domain,
                           RewriteOptions* options,
                           RewriteDriver* driver) {
    options->WriteableDomainLawyer()->AddDomain(domain, message_handler());
    CountingFilter* filter = new CountingFilter(driver);
    driver->AddOwnedPostRenderFilter(filter);
    driver->AddFilters();
    driver->StartParse(base_url);
    driver->Flush();
    return filter;
  }

  scoped_ptr<CountingFilter> filter_;
};

TEST_F(CommonFilterTest, DoesCallImpls) {
  EXPECT_EQ(0, filter_->start_doc_calls_);
  filter_->StartDocument();
  EXPECT_EQ(1, filter_->start_doc_calls_);

  RewriteDriver* driver = rewrite_driver();
  HtmlElement* element = driver->NewElement(NULL, "foo");
  EXPECT_EQ(0, filter_->start_element_calls_);
  filter_->StartElement(element);
  EXPECT_EQ(1, filter_->start_element_calls_);

  EXPECT_EQ(0, filter_->end_element_calls_);
  filter_->EndElement(element);
  EXPECT_EQ(1, filter_->end_element_calls_);
}

TEST_F(CommonFilterTest, StoresCorrectBaseUrl) {
  GoogleString doc_url = "http://www.example.com/";
  RewriteDriver* driver = rewrite_driver();
  driver->AddFilters();
  driver->StartParse(doc_url);
  driver->Flush();
  // Base URL starts out as document URL.
  ExpectUrl(doc_url, driver->google_url());
  ExpectUrl(doc_url, filter_->base_url());

  driver->ParseText(
      "<html><head><link rel='stylesheet' href='foo.css'>");
  driver->Flush();
  ExpectUrl(doc_url, filter_->base_url());

  GoogleString base_url = "http://www.baseurl.com/foo/";
  driver->ParseText("<base href='");
  driver->ParseText(base_url);
  driver->ParseText("' />");
  driver->Flush();
  // Update to base URL.
  ExpectUrl(base_url, filter_->base_url());
  // Make sure we didn't change the document URL.
  ExpectUrl(doc_url, driver->google_url());

  driver->ParseText("<link rel='stylesheet' href='foo.css'>");
  driver->Flush();
  ExpectUrl(base_url, filter_->base_url());

  GoogleString new_base_url = "http://www.somewhere-else.com/";
  driver->ParseText("<base href='");
  driver->ParseText(new_base_url);
  driver->ParseText("' />");
  driver->Flush();
  EXPECT_EQ(1, message_handler()->TotalMessages());

  // Uses old base URL.
  ExpectUrl(base_url, filter_->base_url());

  driver->ParseText("</head></html>");
  driver->Flush();
  ExpectUrl(base_url, filter_->base_url());
  driver->FinishParse();
  ExpectUrl(doc_url, driver->google_url());
}

TEST_F(CommonFilterTest, ResolveUrl) {
  GoogleUrl out;

  // Normal parse, no <base>
  GoogleString doc_url = "http://www.example.com/";
  RewriteDriver* driver = rewrite_driver();
  driver->AddFilters();
  driver->StartParse(doc_url);
  filter_->ResolveUrl("a.css", &out);
  ExpectUrl("http://www.example.com/a.css", out);
  driver->FinishParse();

  // Refs from base
  driver->StartParse(doc_url);
  driver->ParseText("<base href='https://www.example.org/' >");
  driver->Flush();
  filter_->ResolveUrl("a.css", &out);
  ExpectUrl("https://www.example.org/a.css", out);
  driver->FinishParse();

  // Nasty case: refs before base.
  driver->StartParse(doc_url);
  driver->set_refs_before_base();
  driver->ParseText("<html>");  // Establish non-AMP, allowing filters to run.
  driver->Flush();
  filter_->ResolveUrl("a.css", &out);
  EXPECT_FALSE(out.IsAnyValid());
  driver->ParseText("<base href='https://www.example.org/' ></html>");
  driver->Flush();
  filter_->ResolveUrl("a.css", &out);
  ExpectUrl("https://www.example.org/a.css", out);
  driver->FinishParse();
}

TEST_F(CommonFilterTest, DetectsNoScriptCorrectly) {
  GoogleString doc_url = "http://www.example.com/";
  RewriteDriver* driver = rewrite_driver();
  driver->AddFilters();
  driver->StartParse(doc_url);
  driver->Flush();
  EXPECT_TRUE(filter_->noscript_element() == NULL);

  driver->ParseText("<html><head><title>Example Site");
  driver->Flush();
  EXPECT_TRUE(filter_->noscript_element() == NULL);

  driver->ParseText("</title><noscript>");
  driver->Flush();
  EXPECT_TRUE(filter_->noscript_element() != NULL);

  // Nested <noscript> elements
  driver->ParseText("Blah blah blah <noscript><noscript> do-de-do-do ");
  driver->Flush();
  EXPECT_TRUE(filter_->noscript_element() != NULL);

  driver->ParseText("<link href='style.css'>");
  driver->Flush();
  EXPECT_TRUE(filter_->noscript_element() != NULL);

  // Close inner <noscript>s
  driver->ParseText("</noscript></noscript>");
  driver->Flush();
  EXPECT_TRUE(filter_->noscript_element() != NULL);

  // Close outter <noscript>
  driver->ParseText("</noscript>");
  driver->Flush();
  EXPECT_TRUE(filter_->noscript_element() == NULL);

  driver->ParseText("</head></html>");
  driver->FinishParse();
  EXPECT_TRUE(filter_->noscript_element() == NULL);
}

TEST_F(CommonFilterTest, TestTwoDomainLawyers) {
  static const char kBaseUrl[] = "http://www.base.com/";
  CommonFilter* a = MakeFilter(kBaseUrl, "a.com", options(), rewrite_driver());
  CommonFilter* b = MakeFilter(kBaseUrl, "b.com", other_options(),
                               other_rewrite_driver());

  // Either filter can rewrite resources from the base URL
  EXPECT_TRUE(CanRewriteResource(a, StrCat(kBaseUrl, "base.css")));
  EXPECT_TRUE(CanRewriteResource(b, StrCat(kBaseUrl, "base.css")));

  // But the other domains are specific to the two different drivers/filters
  EXPECT_TRUE(CanRewriteResource(a, "http://a.com/a.css"));
  EXPECT_FALSE(CanRewriteResource(a, "http://b.com/b.css"));
  EXPECT_FALSE(CanRewriteResource(b, "http://a.com/a.css"));
  EXPECT_TRUE(CanRewriteResource(b, "http://b.com/b.css"));
}

const char kEndDocumentComment[] = "<!--test comment-->";

class EndDocumentInserterFilter : public CommonFilter {
 public:
  explicit EndDocumentInserterFilter(RewriteDriver* driver)
      : CommonFilter(driver)
  {}

  virtual void EndDocument() {
    InsertNodeAtBodyEnd(driver()->NewCommentNode(NULL, "test comment"));
  }

  virtual void StartDocumentImpl() {}
  virtual void StartElementImpl(HtmlElement* element) {}
  virtual void EndElementImpl(HtmlElement* element) {}

  virtual const char* Name() const {
    return "CommonFilterTest.EndDocumentInserterFilter";
  }
};

class CommonFilterInsertNodeAtBodyEndTest : public RewriteTestBase {
 protected:
  virtual void SetUp() {
    RewriteTestBase::SetUp();
    filter_.reset(new EndDocumentInserterFilter(rewrite_driver()));
    rewrite_driver()->AddFilter(filter_.get());
    SetupWriter();
  }

  void StartTest(StringPiece pre_comment) {
    GoogleString url = "http://www.example.com/";
    rewrite_driver()->StartParse(url);
    rewrite_driver()->ParseText(pre_comment);
  }

  const GoogleString FinishTest(StringPiece pre_comment,
                                StringPiece post_comment) {
    const GoogleString expected_html =
        StrCat(pre_comment, kEndDocumentComment, post_comment);
    rewrite_driver()->ParseText(post_comment);
    rewrite_driver()->FinishParse();
    return expected_html;
  }

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

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

  scoped_ptr<EndDocumentInserterFilter> filter_;
};

TEST_F(CommonFilterInsertNodeAtBodyEndTest, OneBody) {
  GoogleString expected =
      FullTest("<html><head></head><body>", "</body></html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, WhiteSpace) {
  GoogleString expected =
      FullTest("<html><head></head><body>", "</body>\n</html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, NoBody) {
  GoogleString expected =
      FullTest("some content without body tag\n</html>", "");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, NoCloseBody) {
  GoogleString expected =
      FullTest("<html><head></head><body><img src=\"a.jpg\">", "</html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, FlushInBody) {
  GoogleString expected =
      FlushTest("<html><head></head><body>", "", "</body></html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, FlushBeforeBody) {
  GoogleString expected =
      FlushTest("<html><head></head>", "<body>", "</body></html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, FlushAfterCloseBody) {
  // kEndDocumentComment gets inserted after </body> since both the open and
  // close tags have been flushed already.
  GoogleString expected =
      FlushTest("<html><head></head><body></body>", "", "</html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, FlushAtEnd) {
  // This causes us to append to the end of document after the flush.
  GoogleString expected =
      FlushTest("<html><head></head><body></body></html>", "", "");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, TwoBodies) {
  GoogleString expected =
      FullTest("<html><head></head><body></body><body>", "</body></html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, TextAfterCloseBody) {
  GoogleString expected =
      FullTest("<html><head></head><body></body>extra text", "</html>");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, TextAfterCloseHtml) {
  GoogleString expected =
      FullTest("<html><head></head><body></body></html>extra text", "");
  EXPECT_STREQ(expected, output_buffer_);
}

TEST_F(CommonFilterInsertNodeAtBodyEndTest, BodyInNoscript) {
  GoogleString expected = FullTest(
      "<html><head></head><noscript><body></body></noscript>", "</html>");
  EXPECT_STREQ(expected, output_buffer_);
}

}  // namespace

}  // namespace net_instaweb
