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

#include "components/webapps/browser/web_contents/web_app_url_loader.h"

#include <memory>
#include <utility>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/timer/timer.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/url_constants.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace webapps {
namespace {
using UrlComparison = WebAppUrlLoader::UrlComparison;

bool EqualsWithComparison(const GURL& a,
                          const GURL& b,
                          UrlComparison url_comparison) {
  DCHECK(a.is_valid());
  DCHECK(b.is_valid());
  if (a == b) {
    return true;
  }
  GURL::Replacements replace;
  switch (url_comparison) {
    case UrlComparison::kExact:
      return false;
    case UrlComparison::kSameOrigin:
      replace.ClearPath();
      [[fallthrough]];
    case UrlComparison::kIgnoreQueryParamsAndRef:
      replace.ClearQuery();
      replace.ClearRef();
      break;
  }
  return a.ReplaceComponents(replace) == b.ReplaceComponents(replace);
}

// TODO(b/302531937): Make this a utility that can be used through out the
// web_applications/ system.
bool WebContentsShuttingDown(content::WebContents* web_contents) {
  return !web_contents || web_contents->IsBeingDestroyed() ||
         web_contents->GetBrowserContext()->ShutdownStarted();
}

class LoaderTask : public content::WebContentsObserver {
 public:
  LoaderTask() = default;
  LoaderTask(const LoaderTask&) = delete;
  LoaderTask& operator=(const LoaderTask&) = delete;
  LoaderTask(LoaderTask&&) = delete;
  LoaderTask& operator=(LoaderTask&&) = delete;
  ~LoaderTask() override = default;

  void LoadUrl(const content::NavigationController::LoadURLParams& load_params,
               content::WebContents* web_contents,
               UrlComparison url_comparison,
               WebAppUrlLoader::ResultCallback callback) {
    url_ = load_params.url;
    url_comparison_ = url_comparison;
    callback_ = std::move(callback);
    Observe(web_contents);

    if (WebContentsShuttingDown(web_contents)) {
      PostResultTask(WebAppUrlLoader::Result::kFailedWebContentsDestroyed);
      return;
    }

    web_contents->GetController().LoadURLWithParams(load_params);

    timer_.Start(FROM_HERE, WebAppUrlLoader::kSecondsToWaitForWebContentsLoad,
                 base::BindOnce(&LoaderTask::OnLoadUrlTimeout,
                                // OneShotTimer is owned by this class and
                                // it guarantees that it will never run after
                                // it's destroyed.
                                base::Unretained(this)));
  }

  // WebContentsObserver
  // DidFinishLoad doesn't always get called after the page has fully loaded.
  // TODO(ortuno): Use DidStopLoading instead.
  void DidFinishLoad(content::RenderFrameHost* render_frame_host,
                     const GURL& validated_url) override {
    if (WebContentsShuttingDown(web_contents())) {
      PostResultTask(WebAppUrlLoader::Result::kFailedWebContentsDestroyed);
      return;
    }

    if (IsSubframeLoad(render_frame_host)) {
      return;
    }

    // Flush all DidFinishLoad events until about:blank loaded.
    if ((url_.IsAboutBlank() && !validated_url.IsAboutBlank()) ||
        (!url_.IsAboutBlank() && validated_url.IsAboutBlank())) {
      return;
    }

    timer_.Stop();

    if (validated_url == content::kUnreachableWebDataURL) {
      // Navigation ends up in an error page. For example, network errors and
      // policy blocked URLs.
      PostResultTask(WebAppUrlLoader::Result::kFailedErrorPageLoaded);
      return;
    }

    const network::mojom::URLResponseHead* response_head =
        render_frame_host->GetLastResponseHead();
    if (response_head && response_head->headers &&
        response_head->headers->response_code() != net::HTTP_OK) {
      // Navigation loads content but is not successful. For example, HTTP-500
      // class of errors.
      PostResultTask(WebAppUrlLoader::Result::kFailedErrorPageLoaded);
      return;
    }

    if (EqualsWithComparison(validated_url, url_, url_comparison_)) {
      PostResultTask(WebAppUrlLoader::Result::kUrlLoaded);
      return;
    }
    LOG(ERROR) << "Error loading " << url_ << "  page redirected to "
               << validated_url;
    PostResultTask(WebAppUrlLoader::Result::kRedirectedUrlLoaded);
  }

  bool IsSubframeLoad(content::RenderFrameHost* render_frame_host) const {
    return !render_frame_host->IsInPrimaryMainFrame();
  }

  void DidFailLoad(content::RenderFrameHost* render_frame_host,
                   const GURL& validated_url,
                   int error_code) override {
    if (WebContentsShuttingDown(web_contents())) {
      PostResultTask(WebAppUrlLoader::Result::kFailedWebContentsDestroyed);
      return;
    }

    if (IsSubframeLoad(render_frame_host)) {
      return;
    }

    // Flush all DidFailLoad events until about:blank loaded.
    if (url_.IsAboutBlank()) {
      return;
    }

    timer_.Stop();

    LOG(ERROR) << "Error loading " << url_ << "  page failed to load.";
    PostResultTask(WebAppUrlLoader::Result::kFailedUnknownReason);
  }

  void WebContentsDestroyed() override {
    timer_.Stop();
    PostResultTask(WebAppUrlLoader::Result::kFailedWebContentsDestroyed);
  }

 private:
  void OnLoadUrlTimeout() {
    web_contents()->Stop();
    LOG(ERROR) << "Error loading " << url_ << " page took too long to load.";
    PostResultTask(WebAppUrlLoader::Result::kFailedPageTookTooLong);
  }

  void PostResultTask(WebAppUrlLoader::Result result) {
    Observe(nullptr);
    // Post a task to avoid reentrancy issues e.g. adding a WebContentsObserver
    // while a previous observer call is being executed.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback_), result));
  }

  GURL url_;
  UrlComparison url_comparison_;
  WebAppUrlLoader::ResultCallback callback_;

  base::OneShotTimer timer_;

  base::WeakPtrFactory<LoaderTask> weak_ptr_factory_{this};
};

}  // namespace

std::ostream& operator<<(std::ostream& os, WebAppUrlLoaderResult result) {
  switch (result) {
    case WebAppUrlLoaderResult::kUrlLoaded:
      return os << "kUrlLoaded";
    case WebAppUrlLoaderResult::kRedirectedUrlLoaded:
      return os << "kRedirectedUrlLoaded";
    case WebAppUrlLoaderResult::kFailedUnknownReason:
      return os << "kFailedUnknownReason";
    case WebAppUrlLoaderResult::kFailedPageTookTooLong:
      return os << "kFailedPageTookTooLong";
    case WebAppUrlLoaderResult::kFailedWebContentsDestroyed:
      return os << "kFailedWebContentsDestroyed";
    case WebAppUrlLoaderResult::kFailedErrorPageLoaded:
      return os << "kFailedErrorPageLoaded";
  }
}

WebAppUrlLoader::WebAppUrlLoader() = default;

WebAppUrlLoader::~WebAppUrlLoader() = default;

void WebAppUrlLoader::LoadUrl(
    content::NavigationController::LoadURLParams load_url_params,
    content::WebContents* web_contents,
    UrlComparison url_comparison,
    ResultCallback callback) {
  CHECK(web_contents);
  PrepareForLoad(
      web_contents,
      base::BindOnce(
          &WebAppUrlLoader::LoadUrlInternal, weak_factory_.GetWeakPtr(),
          std::move(load_url_params), web_contents->GetWeakPtr(),
          url_comparison,
          base::BindOnce(&WebAppUrlLoader::OnUrlLoaded,
                         weak_factory_.GetWeakPtr(),
                         "Webapp.WebAppUrlLoaderResult", std::move(callback))));
}

void WebAppUrlLoader::LoadUrl(const GURL& url,
                              content::WebContents* web_contents,
                              UrlComparison url_comparison,
                              ResultCallback callback) {
  content::NavigationController::LoadURLParams load_params(url);
  load_params.transition_type = ui::PAGE_TRANSITION_GENERATED;
  LoadUrl(std::move(load_params), web_contents, url_comparison,
          std::move(callback));
}

void WebAppUrlLoader::PrepareForLoad(content::WebContents* web_contents,
                                     base::OnceClosure complete) {
  if (web_contents->GetLastCommittedURL().IsAboutBlank() &&
      web_contents->IsDocumentOnLoadCompletedInPrimaryMainFrame()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(complete));
    return;
  }

  content::NavigationController::LoadURLParams load_params{
      GURL(url::kAboutBlankURL)};
  load_params.transition_type = ui::PAGE_TRANSITION_GENERATED;
  LoadUrlInternal(
      load_params, web_contents->GetWeakPtr(), UrlComparison::kExact,
      base::BindOnce(&WebAppUrlLoader::OnUrlLoaded, weak_factory_.GetWeakPtr(),
                     "Webapp.WebAppUrlLoaderPrepareForLoadResult",
                     base::IgnoreArgs<Result>(std::move(complete))));
}

void WebAppUrlLoader::LoadUrlInternal(
    const content::NavigationController::LoadURLParams& load_url_params,
    base::WeakPtr<content::WebContents> web_contents,
    UrlComparison url_comparison,
    ResultCallback callback) {
  if (WebContentsShuttingDown(web_contents.get())) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback),
                       WebAppUrlLoader::Result::kFailedWebContentsDestroyed));
    return;
  }
  auto loader_task = std::make_unique<LoaderTask>();
  auto* loader_task_ptr = loader_task.get();
  loader_task_ptr->LoadUrl(
      load_url_params, web_contents.get(), url_comparison,
      base::BindOnce(
          [](std::unique_ptr<LoaderTask> task, Result result) {
            task.reset();
            return result;
          },
          std::move(loader_task))
          .Then(std::move(callback)));
}

void WebAppUrlLoader::OnUrlLoaded(std::string_view metrics_name,
                                  ResultCallback callback,
                                  WebAppUrlLoaderResult result) {
  base::UmaHistogramEnumeration(metrics_name, result);
  std::move(callback).Run(result);
}

}  // namespace webapps
