// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <string>
#include <utility>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string16.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace content {

namespace {

using net::test_server::HttpResponse;
using net::test_server::HttpRequest;
using net::test_server::BasicHttpResponse;

const char kCountedHtmlPath[] = "/counted.html";
const char kCookieHtmlPath[] = "/cookie.html";

class AsyncRevalidationManagerBrowserTest : public ContentBrowserTest {
 protected:
  AsyncRevalidationManagerBrowserTest() {}
  ~AsyncRevalidationManagerBrowserTest() override {}

  void SetUp() override {
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
    ContentBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    embedded_test_server()->StartAcceptingConnections();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch("enable-stale-while-revalidate");
  }

  base::RunLoop* run_loop() { return &run_loop_; }
  int requests_counted() const { return requests_counted_; }

  // This method lacks diagnostics for the failure case because TitleWatcher
  // will just wait until the test times out if |expected_title| does not
  // appear.
  bool TitleBecomes(const GURL& url, const std::string& expected_title) {
    base::string16 expected_title16(base::ASCIIToUTF16(expected_title));
    TitleWatcher title_watcher(shell()->web_contents(), expected_title16);
    NavigateToURL(shell(), url);
    return title_watcher.WaitAndGetTitle() == expected_title16;
  }

  void RegisterCountingRequestHandler() {
    embedded_test_server()->RegisterRequestHandler(base::Bind(
        &AsyncRevalidationManagerBrowserTest::CountingRequestHandler, this));
  }

  void RegisterCookieRequestHandler() {
    embedded_test_server()->RegisterRequestHandler(base::Bind(
        &AsyncRevalidationManagerBrowserTest::CookieRequestHandler, this));
  }

 private:
  // A request handler which increases the number in the title tag on every
  // request.
  scoped_ptr<HttpResponse> CountingRequestHandler(const HttpRequest& request) {
    if (request.relative_url != kCountedHtmlPath)
      return nullptr;

    int version = ++requests_counted_;

    scoped_ptr<BasicHttpResponse> http_response(StaleWhileRevalidateHeaders());
    http_response->set_content(
        base::StringPrintf("<title>Version %d</title>", version));

    // The second time this handler is run is the async revalidation. Tests can
    // use this for synchronisation.
    if (version == 2)
      run_loop_.Quit();
    return std::move(http_response);
  }

  // A request handler which increases a cookie value on every request.
  scoped_ptr<HttpResponse> CookieRequestHandler(const HttpRequest& request) {
    static const char kHtml[] =
        "<script>\n"
        "var intervalId;\n"
        "function checkCookie() {\n"
        "  if (document.cookie.search(/version=2/) != -1) {\n"
        "    clearInterval(intervalId);\n"
        "    document.title = \"PASS\";\n"
        "  }\n"
        "}\n"
        "intervalId = setInterval(checkCookie, 10);\n"
        "</script>\n"
        "<title>Loaded</title>\n";

    if (request.relative_url != kCookieHtmlPath)
      return nullptr;

    int version = ++requests_counted_;

    scoped_ptr<BasicHttpResponse> http_response(StaleWhileRevalidateHeaders());
    http_response->AddCustomHeader("Set-Cookie",
                                   base::StringPrintf("version=%d", version));
    http_response->set_content(kHtml);

    return std::move(http_response);
  }

  // Generate the standard response headers common to all request handlers.
  scoped_ptr<BasicHttpResponse> StaleWhileRevalidateHeaders() {
    scoped_ptr<BasicHttpResponse> http_response(new BasicHttpResponse);
    http_response->set_code(net::HTTP_OK);
    http_response->set_content_type("text/html; charset=utf-8");
    http_response->AddCustomHeader("Cache-Control",
                                   "max-age=0, stale-while-revalidate=86400");
    // A validator is needed for revalidations, and hence
    // stale-while-revalidate, to work.
    std::string etag = base::StringPrintf(
        "\"AsyncRevalidationManagerBrowserTest%d\"", requests_counted_);
    http_response->AddCustomHeader("ETag", etag);
    return http_response;
  }

  base::RunLoop run_loop_;
  int requests_counted_ = 0;

  DISALLOW_COPY_AND_ASSIGN(AsyncRevalidationManagerBrowserTest);
};

// Verify that the "Cache-Control: stale-while-revalidate" directive correctly
// triggers an async revalidation.
IN_PROC_BROWSER_TEST_F(AsyncRevalidationManagerBrowserTest,
                       StaleWhileRevalidateIsApplied) {
  RegisterCountingRequestHandler();
  GURL url(embedded_test_server()->GetURL(kCountedHtmlPath));

  EXPECT_TRUE(TitleBecomes(url, "Version 1"));

  // The first request happens synchronously.
  EXPECT_EQ(1, requests_counted());

  // Force the renderer to be destroyed so that the Blink cache doesn't
  // interfere with the result.
  NavigateToURL(shell(), GURL("about:blank"));

  // Load the page again. We should get the stale version from the cache.
  EXPECT_TRUE(TitleBecomes(url, "Version 1"));

  // Wait for the async revalidation to complete.
  run_loop()->Run();
  EXPECT_EQ(2, requests_counted());
}

// The fresh cache entry must become visible once the async revalidation request
// has been sent.
IN_PROC_BROWSER_TEST_F(AsyncRevalidationManagerBrowserTest, CacheIsUpdated) {
  using base::ASCIIToUTF16;
  RegisterCountingRequestHandler();
  GURL url(embedded_test_server()->GetURL(kCountedHtmlPath));

  EXPECT_TRUE(TitleBecomes(url, "Version 1"));

  // Reset the renderer cache.
  NavigateToURL(shell(), GURL("about:blank"));

  // Load the page again. We should get the stale version from the cache.
  EXPECT_TRUE(TitleBecomes(url, "Version 1"));

  // Wait for the async revalidation request to be processed by the
  // EmbeddedTestServer.
  run_loop()->Run();

  // Reset the renderer cache.
  NavigateToURL(shell(), GURL("about:blank"));

  // Since the async revalidation request has been sent, the cache can no
  // longer return the stale contents.
  EXPECT_TRUE(TitleBecomes(url, "Version 2"));
}

// When the asynchronous revalidation arrives, any cookies it contains must be
// applied immediately.
IN_PROC_BROWSER_TEST_F(AsyncRevalidationManagerBrowserTest,
                       CookieSetAsynchronously) {
  RegisterCookieRequestHandler();
  GURL url(embedded_test_server()->GetURL(kCookieHtmlPath));

  // Set cookie to version=1
  NavigateToURL(shell(), url);

  // Reset render cache.
  NavigateToURL(shell(), GURL("about:blank"));

  // The page will load from the cache, then when the async revalidation
  // completes the cookie will update.
  EXPECT_TRUE(TitleBecomes(url, "PASS"));
}

}  // namespace

}  // namespace content
