/*
 * Copyright 2010 Google Inc.
 *
 * Licensed 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.
 */

// Author: jmaessen@google.com (Jan Maessen)

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

#include "base/logging.h"
#include "base/scoped_ptr.h"
#include "net/instaweb/rewriter/public/resource_manager_test_base.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/util/public/google_url.h"
#include "net/instaweb/util/public/gtest.h"
#include "net/instaweb/util/public/string.h"
#include "net/instaweb/util/public/string_util.h"

namespace net_instaweb {

class UrlLeftTrimFilterTest : public ResourceManagerTestBase {
 protected:
  virtual void SetUp() {
    ResourceManagerTestBase::SetUp();
    left_trim_filter_.reset(new UrlLeftTrimFilter(rewrite_driver(),
                                                  statistics()));
    rewrite_driver()->AddFilter(left_trim_filter_.get());
  }

  // Must set base url to "http://www.example.com/dir/*something*"
  // before running these.
  void TestAnchors(const StringPiece& base_url) {
    SetFilterBaseUrl(base_url);
    OneTrim(true, "http://www.example.com/dir/?var=val#anchor",
            "/dir/?var=val#anchor");
    OneTrim(true, "http://www.example.com/dir/#anchor", "/dir/#anchor");
    OneTrim(true, "http://www.example.com/dir/foo.html", "foo.html");
    OneTrim(true, "http://www.example.com/dir/abc/f?g=h", "abc/f?g=h");
    OneTrim(true, "http://www.example.com/dir/f?g=h#anchor",
            "f?g=h#anchor");
    OneTrim(true, "http://www.example.com/dir/index.html#",
            "index.html#");
    OneTrim(true, "http://www.example.com/dir/index.html?f=g#bottom",
            "index.html?f=g#bottom");
    OneTrim(true, "http://www.example.com/dir/index.html?f=g#bottom",
            "index.html?f=g#bottom");
    OneTrim(false, "#top", "");
  }

  void OneTrim(bool changed,
               const StringPiece& init, const StringPiece& expected) {
    StringPiece url(init);
    GoogleString trimmed;
    CHECK(base_url_.get() != NULL);
    EXPECT_EQ(changed, left_trim_filter_->Trim(
        *base_url_.get(), url, &trimmed,
        rewrite_driver()->message_handler()));
    if (changed) {
      EXPECT_STREQ(expected, trimmed);
    }
  }

  void SetFilterBaseUrl(const StringPiece& base_url) {
    base_url_.reset(new GoogleUrl(base_url));
  }

  virtual bool AddBody() const { return false; }

  // TODO(jmarantz): factor out all the image references in this file to use
  // this method.
  GoogleString Image(const StringPiece& image) {
    return StrCat("<img src='", image, "'>");
  }

 private:
  scoped_ptr<UrlLeftTrimFilter> left_trim_filter_;
  scoped_ptr<GoogleUrl> base_url_;
};

static const char kBase[] = "http://foo.bar/baz/";
static const char kHttp[] = "http:";
static const char kPath[] = "/baz/";

TEST_F(UrlLeftTrimFilterTest, SimpleTrims) {
  SetFilterBaseUrl("http://foo.bar/baz/");
  // TODO(jmaessen): strip protocol given user-agent-sensitive trimming
  OneTrim(false, "http://www.google.com/", "http://www.google.com/");
  OneTrim(true, kBase, kPath);
  OneTrim(true, "http://foo.bar/baz/quux", "quux");
  OneTrim(true, "/baz/quux", "quux");
  OneTrim(true, "//foo.bar/img/img1.jpg", "/img/img1.jpg");
  OneTrim(false, "/img/img1.jpg", "/img/img1.jpg");
  OneTrim(false, kHttp, kHttp);  // false, because /baz/ is 5 chars long
  OneTrim(true, "//foo.bar/baz/quux", "quux");
  OneTrim(false, "baz/img.jpg", "baz/img.jpg");
}

static const char kRootedBase[] = "http://foo.bar/";

// Catch screw cases when a base url lies at the root of a domain.
TEST_F(UrlLeftTrimFilterTest, RootedTrims) {
  SetFilterBaseUrl(kRootedBase);
  // TODO(jmaessen): strip protocol given user-agent-sensitive trimming
  OneTrim(false, "http://www.google.com/", "http://www.google.com/");
  OneTrim(true, kBase, "baz/");
  OneTrim(false, "//www.google.com/", "//www.google.com/");
  OneTrim(true, kPath, "baz/");
  OneTrim(false, "quux", "quux");
}

static const char kNone[] =
    "<head><base href='ftp://what.the/heck/'/>"
    "<link rel='stylesheet' href='http://what.the.cow/heck/'/></head>"
    "<body><a href='spdy://www.google.com/'>google</a>"
    "<img src='file:///where/the/heck.jpg'/></body>";

TEST_F(UrlLeftTrimFilterTest, NoChanges) {
  ValidateNoChanges("none_forward", kNone);
}

static const char kSome[] =
    "<head><base href='http://foo.bar/baz/'/>"
    "<link rel='stylesheet' href='http://foo.bar/baz/'/></head>"
    "<body><a href='http://www.google.com/'>google</a>"
    "<img src='http://foo.bar/baz/nav.jpg'/>"
    "<img src='http://foo.bar/img/img1.jpg'/>"
    "<img src='/baz/img2.jpg'/>"
    "<img src='//foo.bar/baz/widget.png'/>"
    "<a href='./xyz/something.html'>text!</a></body>";

static const char kSomeRewritten[] =
    "<head><base href='http://foo.bar/baz/'/>"
    "<link rel='stylesheet' href='/baz/'/></head>"
    "<body><a href='http://www.google.com/'>google</a>"
    "<img src='nav.jpg'/>"
    "<img src='/img/img1.jpg'/>"
    "<img src='img2.jpg'/>"
    "<img src='widget.png'/>"
    "<a href='xyz/something.html'>text!</a></body>";

TEST_F(UrlLeftTrimFilterTest, SomeChanges) {
  ValidateExpected("some_forward", kSome, kSomeRewritten);
}

static const char kFirstDoc[] =
    "<head><base href='http://foo/'/></head>"
    "<body><a href='http://foo/abc'>link</a>"
    "<img src='www.google.com/pretty_picture.jpg'>"
    "<img src='http://foo/bar/123.png'></body>";

static const char kFirstDocRewritten[] =
    "<head><base href='http://foo/'/></head>"
    "<body><a href='abc'>link</a>"
    "<img src='www.google.com/pretty_picture.jpg'>"
    "<img src='bar/123.png'></body>";

static const char kSecondDoc[] =
    "<head><base href='http://newurl/baz/'/></head>"
    "<body><a href='http://foo/baz/abc'>text</a>"
    "<a href='http://newurl/baz/target'>more text</a>"
    "<img src='www.google.com/pretty_picture.jpg'>"
    "<img src='/baz/image.jpg'></body>";

static const char kSecondDocRewritten[] =
    "<head><base href='http://newurl/baz/'/></head>"
    "<body><a href='http://foo/baz/abc'>text</a>"
    "<a href='target'>more text</a>"
    "<img src='www.google.com/pretty_picture.jpg'>"
    "<img src='image.jpg'></body>";

TEST_F(UrlLeftTrimFilterTest, TwoBases) {
  ValidateExpected("first_doc", kFirstDoc, kFirstDocRewritten);
  ValidateExpected("second_doc", kSecondDoc, kSecondDocRewritten);
}

static const char kPartialUrl[] =
    "<head><base href='http://abcdef/123'/></head>"
    "<body><a href='abcdef/something'>link</a>"
    "<img src='http://abcdefg'></body>";

static const char kPartialUrlRewritten[] =
    "<head><base href='http://abcdef/123'/></head>"
    "<body><a href='abcdef/something'>link</a>"
    "<img src='http://abcdefg'></body>";

TEST_F(UrlLeftTrimFilterTest, PartialUrl) {
  ValidateExpected("partial_url", kPartialUrl, kPartialUrlRewritten);
}

// TODO(nforman): in correct html, the base tag (with href) must come before
// any other urls, thereby making them all relative to the same thing (i.e.
// the doc's url if there is no base tag, and the base tag url if there is one).
// However, different browsers deal with malformed html in different ways.
// Some browsers change the base at the point of the base tag (Firefox),
// and therefore will resolve the following (located at http://abc.com/foo.html)
// <html>
// <head>
// <title>Foo - Too Many Bases Test</title>
// <a href="imghp">Google Images, before base tag</a>
// <base href="http://www.google.com">
// </head>
// <body>
// <a href="/">Empty Link after base tag.</a>
// </body>
// to an invalid link, http://abc.com/imghp, and to http://www.google.com.
// However, chrome will resolve all the urls against "http://www.google.com",
// giving http://www.google.com/imghp and http://www.google.com.
// Furthermore, chrome and firefox handle the multiple base tags issue
// differently.
// Our current behavior is to ignore any src or href attributes that come
// before the base tag.
static const char kMidBase[] =
    "<head><link rel='stylesheet' href='http://foo.bar/baz'/>"
    "<a href='baz.html'>strange link in header</a>"
    "<base href='http://foo.bar'></head>"
    "<body><img src='//foo.bar/img.jpg'></body>";

static const char kMidBaseRewritten[] =
    "<head><link rel='stylesheet' href='http://foo.bar/baz'/>"
    "<a href='baz.html'>strange link in header</a>"
    "<base href='http://foo.bar'></head>"
    "<body><img src='img.jpg'></body>";

TEST_F(UrlLeftTrimFilterTest, MidwayBaseUrl) {
  ValidateExpected("midway_base", kMidBase, kMidBaseRewritten);
}

static const char kAnnoyingWiki[] =
    "<head><base href='http://en.wikipedia.org/wiki/Labrador_Retriever'/>"
    "</head><body><img src='/wiki/img.jpg'>"
    "<a href='/wiki/File:puppy.jpg'>dog</a></body>";

static const char kAnnoyingWikiRewritten[] =
    "<head><base href='http://en.wikipedia.org/wiki/Labrador_Retriever'/>"
    "</head><body><img src='img.jpg'>"
    "<a href='/wiki/File:puppy.jpg'>dog</a></body>";

TEST_F(UrlLeftTrimFilterTest, AnnoyingWiki) {
  ValidateExpected("wiki", kAnnoyingWiki, kAnnoyingWikiRewritten);
}

TEST_F(UrlLeftTrimFilterTest, Directories) {
  SetFilterBaseUrl("http://www.example.com/foo/bar/index.html");
  OneTrim(false, "..", "..");
}

TEST_F(UrlLeftTrimFilterTest, Dots) {
  SetFilterBaseUrl("http://foo/bar/");
  OneTrim(true, "foo/bar/../baz/x.html", "foo/baz/x.html");
}

TEST_F(UrlLeftTrimFilterTest, XKCD) {
  SetFilterBaseUrl("http://forums.xkcd.com/");
  // TODO(jmaessen): strip protocol given user-agent-sensitive trimming
  OneTrim(false, "http://xkcd.com/", "http://xkcd.com/");
}

TEST_F(UrlLeftTrimFilterTest, OneDot) {
  SetFilterBaseUrl("http://foo.bar/baz/index.html");
  OneTrim(true, "./cows/index.html", "cows/index.html");
}

TEST_F(UrlLeftTrimFilterTest, Query) {
  SetFilterBaseUrl("http://foo.bar/index.html");
  OneTrim(true, "http://foo.bar/?a=b", "/?a=b");
}

TEST_F(UrlLeftTrimFilterTest, TrimQuery) {
  SetFilterBaseUrl("http://foo.bar/baz/index.html");
  OneTrim(true, "http://foo.bar/baz/other.html?a=b", "other.html?a=b");
}

TEST_F(UrlLeftTrimFilterTest, DoubleSlashPath) {
  SetFilterBaseUrl("http://foo.bar/baz/index.html");
  OneTrim(true, "http://foo.bar/baz//other.html", "/baz//other.html");
}

TEST_F(UrlLeftTrimFilterTest, DoubleSlashBeginningPath) {
  SetFilterBaseUrl("http://foo.bar/index.html");
  OneTrim(true, "http://foo.bar//other.html", "other.html");
}

TEST_F(UrlLeftTrimFilterTest, TripleSlashPath) {
  SetFilterBaseUrl("http://foo.bar/example/index.html");
  OneTrim(true, "http://foo.bar/example///other.html", "/example///other.html");
}

static const char kBlankBase[] =
    "<head><base href=''>"
    "</head><body>"
    "<a href='http://www.google.com/'>foo</a></body>";

static const char kBlankBaseRewritten[] =
    "<head><base href=''>"
    "</head><body>"
    "<a href='http://www.google.com/'>foo</a></body>";

TEST_F(UrlLeftTrimFilterTest, BlankBase) {
  ValidateExpected("wiki", kBlankBase, kBlankBaseRewritten);
}

static const char kRelativeBase[] =
    "<head><base href='/directory/'>"
    "</head><body>"
    "<img src='/directory/img.jpg'></body>";

static const char kRelativeBaseRewritten[] =
    "<head><base href='/directory/'>"
    "</head><body>"
    "<img src='img.jpg'></body>";

TEST_F(UrlLeftTrimFilterTest, RelativeBase) {
  ValidateExpected("wiki", kRelativeBase, kRelativeBaseRewritten);
}

TEST_F(UrlLeftTrimFilterTest, QueryWithEncodedAmpersand) {
  ValidateExpected(
      "ampersand",
      Image("http://test.com/discuss/a.php?&action=vtopic&amp;forum=2"),
      Image("discuss/a.php?&amp;action=vtopic&amp;forum=2"));

  // TODO(jmarantz): consider this case.  We think this should be trimmed:
  //     ValidateExpected("improperly_terminated_multi_byte_escape",
  //       Image("http://test.com/?q=klaatu+nikto+barada&lang=fr"),
  //       Image("?q=klaatu+nikto+barada&amp;lang=fr"));
  // but it is not, because with "&lang=", we interpretet the
  // "&lang" as an improperly terminated "&lang;".  Now "&lang;" is
  // a multi-byte sequence, which we cannot represent, so we get a
  // decoding error, and thus cannot intpreret this URL at all.
  //
  // TODO(jmarantz): consider treating improperly terminated multi-byte
  // escape sequences as literals.
  //
  // For now:
  ValidateNoChanges("improperly_terminated_multi_byte_escape",
                    Image("http://test.com/?q=klaatu+nikto+barada&lang=fr"));
}

TEST_F(UrlLeftTrimFilterTest, Anchors) {
  TestAnchors("http://www.example.com/dir/?var=val");
  TestAnchors("http://www.example.com/dir/index.html");
  TestAnchors("http://www.example.com/dir/index.html#top");
  TestAnchors("http://www.example.com/dir/index.html?f=g");
  TestAnchors("http://www.example.com/dir/index.html?f=g&y=z#bottom");
}

}  // namespace net_instaweb
