// Copyright (c) 2012 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 "content/public/renderer/resource_fetcher.h"

#include <stdint.h>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/command_line.h"
#include "base/message_loop/message_loop.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/renderer/render_view.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/WebKit/public/platform/WebURLResponse.h"
#include "third_party/WebKit/public/web/WebFrame.h"
#include "third_party/WebKit/public/web/WebView.h"

using blink::WebFrame;
using blink::WebURLRequest;
using blink::WebURLResponse;

namespace content {

static const int kMaxWaitTimeMs = 5000;

class FetcherDelegate {
 public:
  FetcherDelegate()
      : completed_(false),
        timed_out_(false) {
    // Start a repeating timer waiting for the download to complete.  The
    // callback has to be a static function, so we hold on to our instance.
    FetcherDelegate::instance_ = this;
    StartTimer();
  }

  virtual ~FetcherDelegate() {}

  ResourceFetcher::Callback NewCallback() {
    return base::Bind(&FetcherDelegate::OnURLFetchComplete,
                      base::Unretained(this));
  }

  virtual void OnURLFetchComplete(const WebURLResponse& response,
                                  const std::string& data) {
    response_ = response;
    data_ = data;
    completed_ = true;
    timer_.Stop();
    if (!timed_out_)
      quit_task_.Run();
  }

  bool completed() const { return completed_; }
  bool timed_out() const { return timed_out_; }

  std::string data() const { return data_; }
  const WebURLResponse& response() const { return response_; }

  // Wait for the request to complete or timeout.
  void WaitForResponse() {
    scoped_refptr<MessageLoopRunner> runner = new MessageLoopRunner;
    quit_task_ = runner->QuitClosure();
    runner->Run();
  }

  void StartTimer() {
    timer_.Start(FROM_HERE,
                 base::TimeDelta::FromMilliseconds(kMaxWaitTimeMs),
                 this,
                 &FetcherDelegate::TimerFired);
  }

  void TimerFired() {
    ASSERT_FALSE(completed_);

    timed_out_ = true;
    if (!completed_)
      quit_task_.Run();
    FAIL() << "fetch timed out";
  }

  static FetcherDelegate* instance_;

 private:
  base::OneShotTimer timer_;
  bool completed_;
  bool timed_out_;
  WebURLResponse response_;
  std::string data_;
  base::Closure quit_task_;
};

FetcherDelegate* FetcherDelegate::instance_ = NULL;

class EvilFetcherDelegate : public FetcherDelegate {
 public:
  ~EvilFetcherDelegate() override {}

  void SetFetcher(ResourceFetcher* fetcher) {
    fetcher_.reset(fetcher);
  }

  void OnURLFetchComplete(const WebURLResponse& response,
                          const std::string& data) override {
    FetcherDelegate::OnURLFetchComplete(response, data);

    // Destroy the ResourceFetcher here.  We are testing that upon returning
    // to the ResourceFetcher that it does not crash.  This must be done after
    // calling FetcherDelegate::OnURLFetchComplete, since deleting the fetcher
    // invalidates |response| and |data|.
    fetcher_.reset();
  }

 private:
  std::unique_ptr<ResourceFetcher> fetcher_;
};

class ResourceFetcherTests : public ContentBrowserTest {
 public:
  ResourceFetcherTests() : render_view_routing_id_(MSG_ROUTING_NONE) {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(switches::kSingleProcess);
#if defined(OS_WIN)
    // Don't want to try to create a GPU process.
    command_line->AppendSwitch(switches::kDisableGpu);
#endif
  }

  void SetUpOnMainThread() override {
    render_view_routing_id_ =
        shell()->web_contents()->GetRenderViewHost()->GetRoutingID();
  }

  RenderView* GetRenderView() {
    return RenderView::FromRoutingID(render_view_routing_id_);
  }

  void ResourceFetcherDownloadOnRenderer(const GURL& url) {
    WebFrame* frame = GetRenderView()->GetWebView()->mainFrame();

    std::unique_ptr<FetcherDelegate> delegate(new FetcherDelegate);
    std::unique_ptr<ResourceFetcher> fetcher(ResourceFetcher::Create(url));
    fetcher->Start(frame,
                   WebURLRequest::RequestContextInternal,
                   WebURLRequest::FrameTypeNone,
                   ResourceFetcher::PLATFORM_LOADER,
                   delegate->NewCallback());

    delegate->WaitForResponse();

    ASSERT_TRUE(delegate->completed());
    EXPECT_EQ(delegate->response().httpStatusCode(), 200);
    std::string text = delegate->data();
    EXPECT_TRUE(text.find("Basic html test.") != std::string::npos);
  }

  void ResourceFetcher404OnRenderer(const GURL& url) {
    WebFrame* frame = GetRenderView()->GetWebView()->mainFrame();

    std::unique_ptr<FetcherDelegate> delegate(new FetcherDelegate);
    std::unique_ptr<ResourceFetcher> fetcher(ResourceFetcher::Create(url));
    fetcher->Start(frame,
                   WebURLRequest::RequestContextInternal,
                   WebURLRequest::FrameTypeNone,
                   ResourceFetcher::PLATFORM_LOADER,
                   delegate->NewCallback());

    delegate->WaitForResponse();

    ASSERT_TRUE(delegate->completed());
    EXPECT_EQ(delegate->response().httpStatusCode(), 404);
  }

  void ResourceFetcherDidFailOnRenderer() {
    WebFrame* frame = GetRenderView()->GetWebView()->mainFrame();

    // Try to fetch a page on a site that doesn't exist.
    GURL url("http://localhost:1339/doesnotexist");
    std::unique_ptr<FetcherDelegate> delegate(new FetcherDelegate);
    std::unique_ptr<ResourceFetcher> fetcher(ResourceFetcher::Create(url));
    fetcher->Start(frame,
                   WebURLRequest::RequestContextInternal,
                   WebURLRequest::FrameTypeNone,
                   ResourceFetcher::PLATFORM_LOADER,
                   delegate->NewCallback());

    delegate->WaitForResponse();

    // When we fail, we still call the Delegate callback but we pass in empty
    // values.
    EXPECT_TRUE(delegate->completed());
    EXPECT_TRUE(delegate->response().isNull());
    EXPECT_EQ(delegate->data(), std::string());
    EXPECT_FALSE(delegate->timed_out());
  }

  void ResourceFetcherTimeoutOnRenderer(const GURL& url) {
    WebFrame* frame = GetRenderView()->GetWebView()->mainFrame();

    std::unique_ptr<FetcherDelegate> delegate(new FetcherDelegate);
    std::unique_ptr<ResourceFetcher> fetcher(ResourceFetcher::Create(url));
    fetcher->Start(frame,
                   WebURLRequest::RequestContextInternal,
                   WebURLRequest::FrameTypeNone,
                   ResourceFetcher::PLATFORM_LOADER,
                   delegate->NewCallback());
    fetcher->SetTimeout(base::TimeDelta());

    delegate->WaitForResponse();

    // When we timeout, we still call the Delegate callback but we pass in empty
    // values.
    EXPECT_TRUE(delegate->completed());
    EXPECT_TRUE(delegate->response().isNull());
    EXPECT_EQ(delegate->data(), std::string());
    EXPECT_FALSE(delegate->timed_out());
  }

  void ResourceFetcherDeletedInCallbackOnRenderer(const GURL& url) {
    WebFrame* frame = GetRenderView()->GetWebView()->mainFrame();

    std::unique_ptr<EvilFetcherDelegate> delegate(new EvilFetcherDelegate);
    std::unique_ptr<ResourceFetcher> fetcher(ResourceFetcher::Create(url));
    fetcher->Start(frame,
                   WebURLRequest::RequestContextInternal,
                   WebURLRequest::FrameTypeNone,
                   ResourceFetcher::PLATFORM_LOADER,
                   delegate->NewCallback());
    fetcher->SetTimeout(base::TimeDelta());
    delegate->SetFetcher(fetcher.release());

    delegate->WaitForResponse();
    EXPECT_FALSE(delegate->timed_out());
  }

  void ResourceFetcherPost(const GURL& url) {
    const char* kBody = "Really nifty POST body!";

    WebFrame* frame = GetRenderView()->GetWebView()->mainFrame();

    std::unique_ptr<FetcherDelegate> delegate(new FetcherDelegate);
    std::unique_ptr<ResourceFetcher> fetcher(ResourceFetcher::Create(url));
    fetcher->SetMethod("POST");
    fetcher->SetBody(kBody);
    fetcher->Start(frame,
                   WebURLRequest::RequestContextInternal,
                   WebURLRequest::FrameTypeNone,
                   ResourceFetcher::PLATFORM_LOADER,
                   delegate->NewCallback());

    delegate->WaitForResponse();
    ASSERT_TRUE(delegate->completed());
    EXPECT_EQ(delegate->response().httpStatusCode(), 200);
    EXPECT_EQ(kBody, delegate->data());
  }

  void ResourceFetcherSetHeader(const GURL& url) {
    const char* kHeader = "Rather boring header.";

    WebFrame* frame = GetRenderView()->GetWebView()->mainFrame();

    std::unique_ptr<FetcherDelegate> delegate(new FetcherDelegate);
    std::unique_ptr<ResourceFetcher> fetcher(ResourceFetcher::Create(url));
    fetcher->SetHeader("header", kHeader);
    fetcher->Start(frame,
                   WebURLRequest::RequestContextInternal,
                   WebURLRequest::FrameTypeNone,
                   ResourceFetcher::PLATFORM_LOADER,
                   delegate->NewCallback());

    delegate->WaitForResponse();
    ASSERT_TRUE(delegate->completed());
    EXPECT_EQ(delegate->response().httpStatusCode(), 200);
    EXPECT_EQ(kHeader, delegate->data());
  }

  int32_t render_view_routing_id_;
};

// Test a fetch from the test server.
// If this flakes, use http://crbug.com/51622.
IN_PROC_BROWSER_TEST_F(ResourceFetcherTests, ResourceFetcherDownload) {
  // Need to spin up the renderer.
  NavigateToURL(shell(), GURL(url::kAboutBlankURL));

  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url(embedded_test_server()->GetURL("/simple_page.html"));

  PostTaskToInProcessRendererAndWait(
        base::Bind(&ResourceFetcherTests::ResourceFetcherDownloadOnRenderer,
                   base::Unretained(this), url));
}

IN_PROC_BROWSER_TEST_F(ResourceFetcherTests, ResourceFetcher404) {
  // Need to spin up the renderer.
  NavigateToURL(shell(), GURL(url::kAboutBlankURL));

  // Test 404 response.
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url = embedded_test_server()->GetURL("/thisfiledoesntexist.html");

  PostTaskToInProcessRendererAndWait(
        base::Bind(&ResourceFetcherTests::ResourceFetcher404OnRenderer,
                   base::Unretained(this), url));
}

// If this flakes, use http://crbug.com/51622.
IN_PROC_BROWSER_TEST_F(ResourceFetcherTests, ResourceFetcherDidFail) {
  // Need to spin up the renderer.
  NavigateToURL(shell(), GURL(url::kAboutBlankURL));

  PostTaskToInProcessRendererAndWait(
        base::Bind(&ResourceFetcherTests::ResourceFetcherDidFailOnRenderer,
                   base::Unretained(this)));
}

IN_PROC_BROWSER_TEST_F(ResourceFetcherTests, ResourceFetcherTimeout) {
  // Need to spin up the renderer.
  NavigateToURL(shell(), GURL(url::kAboutBlankURL));

  // Grab a page that takes at least 1 sec to respond, but set the fetcher to
  // timeout in 0 sec.
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url(embedded_test_server()->GetURL("/slow?1"));

  PostTaskToInProcessRendererAndWait(
        base::Bind(&ResourceFetcherTests::ResourceFetcherTimeoutOnRenderer,
                   base::Unretained(this), url));
}

IN_PROC_BROWSER_TEST_F(ResourceFetcherTests, ResourceFetcherDeletedInCallback) {
  // Need to spin up the renderer.
  NavigateToURL(shell(), GURL(url::kAboutBlankURL));

  // Grab a page that takes at least 1 sec to respond, but set the fetcher to
  // timeout in 0 sec.
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url(embedded_test_server()->GetURL("/slow?1"));

  PostTaskToInProcessRendererAndWait(
        base::Bind(
            &ResourceFetcherTests::ResourceFetcherDeletedInCallbackOnRenderer,
            base::Unretained(this), url));
}



// Test that ResourceFetchers can handle POSTs.
IN_PROC_BROWSER_TEST_F(ResourceFetcherTests, ResourceFetcherPost) {
  // Need to spin up the renderer.
  NavigateToURL(shell(), GURL(url::kAboutBlankURL));

  // Grab a page that echos the POST body.
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url(embedded_test_server()->GetURL("/echo"));

  PostTaskToInProcessRendererAndWait(
        base::Bind(
            &ResourceFetcherTests::ResourceFetcherPost,
            base::Unretained(this), url));
}

// Test that ResourceFetchers can set headers.
IN_PROC_BROWSER_TEST_F(ResourceFetcherTests, ResourceFetcherSetHeader) {
  // Need to spin up the renderer.
  NavigateToURL(shell(), GURL(url::kAboutBlankURL));

  // Grab a page that echos the POST body.
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL url(embedded_test_server()->GetURL("/echoheader?header"));

  PostTaskToInProcessRendererAndWait(
        base::Bind(
            &ResourceFetcherTests::ResourceFetcherSetHeader,
            base::Unretained(this), url));
}

}  // namespace content
