// Copyright 2021 The Chromium Embedded Framework 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 <limits>
#include <map>
#include <memory>
#include <queue>
#include <sstream>
#include <string>

#include "include/base/cef_callback.h"
#include "include/wrapper/cef_closure_task.h"
#include "tests/ceftests/routing_test_handler.h"
#include "tests/ceftests/test_handler.h"
#include "tests/ceftests/test_util.h"
#include "tests/gtest/include/gtest/gtest.h"

// Set to 1 to enable verbose debugging info logging.
#define VERBOSE_DEBUGGING 0

namespace {

// Must match CefFrameHostImpl::kInvalidFrameId.
const int kInvalidFrameId = -4;

// Tracks callback status for a single frame object.
struct FrameStatus {
  // Callbacks in expected order. Not all callbacks are executed in all cases
  // (see IsExpectedCallback).
  enum CallbackType {
    FRAME_CREATED,
    MAIN_FRAME_INITIAL_ASSIGNED,
    AFTER_CREATED,
    FRAME_ATTACHED,
    MAIN_FRAME_CHANGED_ASSIGNED,
    LOAD_START,
    LOAD_END,
    BEFORE_CLOSE,
    FRAME_DETACHED,
    MAIN_FRAME_CHANGED_REMOVED,
    MAIN_FRAME_FINAL_REMOVED,

    CALLBACK_LAST = MAIN_FRAME_FINAL_REMOVED,
  };

  static const char* GetCallbackName(int type) {
    switch (type) {
      case FRAME_CREATED:
        return "OnFrameCreated";
      case MAIN_FRAME_INITIAL_ASSIGNED:
        return "OnMainFrameChanged(initial_assigned)";
      case AFTER_CREATED:
        return "OnAfterCreated";
      case FRAME_ATTACHED:
        return "OnFrameAttached";
      case MAIN_FRAME_CHANGED_ASSIGNED:
        return "OnMainFrameChanged(changed_assigned)";
      case LOAD_START:
        return "OnLoadStart";
      case LOAD_END:
        return "OnLoadEnd";
      case BEFORE_CLOSE:
        return "OnBeforeClose";
      case FRAME_DETACHED:
        return "OnFrameDetached";
      case MAIN_FRAME_CHANGED_REMOVED:
        return "OnMainFrameChanged(changed_removed)";
      case MAIN_FRAME_FINAL_REMOVED:
        return "OnMainFrameChanged(final_removed)";
    }
    NOTREACHED();
    return "Unknown";
  }

  // Returns true for callbacks that should only execute for main frames.
  static bool IsMainFrameOnlyCallback(int type) {
    return (type == MAIN_FRAME_INITIAL_ASSIGNED || type == AFTER_CREATED ||
            type == MAIN_FRAME_CHANGED_ASSIGNED || type == BEFORE_CLOSE ||
            type == MAIN_FRAME_CHANGED_REMOVED ||
            type == MAIN_FRAME_FINAL_REMOVED);
  }

  static std::string GetFrameDebugString(CefRefPtr<CefFrame> frame) {
    // Match the logic in frame_util::GetFrameDebugString.
    // Specific formulation of the frame ID is an implementation detail that
    // should generally not be relied upon, but this decomposed format makes the
    // debug logging easier to follow.
    uint64_t frame_id = frame->GetIdentifier();
    uint32_t process_id = frame_id >> 32;
    uint32_t routing_id = std::numeric_limits<uint32_t>::max() & frame_id;
    std::stringstream ss;
    ss << (frame->IsMain() ? "main" : " sub") << "[" << process_id << ","
       << routing_id << "]";
    return ss.str();
  }

  FrameStatus(CefRefPtr<CefFrame> frame)
      : frame_id_(frame->GetIdentifier()),
        is_main_(frame->IsMain()),
        ident_str_(GetFrameDebugString(frame)) {}

  int64_t frame_id() const { return frame_id_; }
  bool is_main() const { return is_main_; }

  bool AllQueriesDelivered(std::string* msg = nullptr) const {
    EXPECT_UI_THREAD();
    const int expected_ct = is_temporary_ ? 0 : expected_query_ct_;
#if VERBOSE_DEBUGGING
    if (msg) {
      std::stringstream ss;
      ss << ident_str_ << "(expected=" << expected_ct
         << " delivered=" << delivered_query_ct_ << ")";
      *msg += ss.str();
    }
#endif
    return delivered_query_ct_ == expected_ct;
  }
  int QueriesDeliveredCount() const {
    EXPECT_UI_THREAD();
    return delivered_query_ct_;
  }

  bool IsSame(CefRefPtr<CefFrame> frame) const {
    return frame->GetIdentifier() == frame_id();
  }

  bool IsLoaded(std::string* msg = nullptr) const {
#if VERBOSE_DEBUGGING
    if (msg) {
      std::stringstream ss;
      ss << ident_str_ << "(";
      for (int i = 0; i <= LOAD_END; ++i) {
        ss << GetCallbackName(i) << "=" << got_callback_[i];
        if (i < LOAD_END) {
          ss << " ";
        }
      }
      ss << ")";
      *msg += ss.str();
    }
#endif
    return got_callback_[LOAD_END];
  }
  bool IsDetached() const { return got_callback_[FRAME_DETACHED]; }

  void SetIsFirstMain(bool val) {
    EXPECT_TRUE(is_main_);
    is_first_main_ = val;
    if (is_first_main_) {
      // Also expect OnAfterCreated
      expected_query_ct_++;
    }
  }
  void SetIsLastMain(bool val) {
    EXPECT_TRUE(is_main_);
    is_last_main_ = val;
  }

  void SetIsTemporary(bool val) {
    EXPECT_FALSE(is_main_);
    is_temporary_ = val;
  }
  bool IsTemporary() const { return is_temporary_; }

  void SetAdditionalDebugInfo(const std::string& debug_info) {
    debug_info_ = debug_info;
  }

  std::string GetDebugString() const { return debug_info_ + ident_str_; }

  // The main frame will be reused for same-origin navigations.
  void ResetMainLoadStatus() {
    EXPECT_TRUE(is_main_);

    ResetCallbackStatus(LOAD_START, /*expect_query=*/true);
    ResetCallbackStatus(LOAD_END, /*expect_query=*/true);
  }

  void OnFrameCreated(CefRefPtr<CefBrowser> browser,
                      CefRefPtr<CefFrame> frame) {
    EXPECT_UI_THREAD();
    VerifyBrowser(__FUNCTION__, browser);
    VerifyFrame(__FUNCTION__, frame);

    GotCallback(__FUNCTION__, FRAME_CREATED);

    // Test delivery of messages using a frame that isn't connected yet.
    // This tests queuing of messages in the browser process and possibly the
    // renderer process.
    ExecuteQuery(frame, FRAME_CREATED);
  }

  void OnFrameAttached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame) {
    EXPECT_UI_THREAD();
    VerifyBrowser(__FUNCTION__, browser);
    VerifyFrame(__FUNCTION__, frame);

    GotCallback(__FUNCTION__, FRAME_ATTACHED);

    // Test delivery of messages using a frame that just connected.
    // This tests queuing of messages in the browser process and possibly the
    // renderer process.
    ExecuteQuery(frame, FRAME_ATTACHED);
  }

  void OnFrameDetached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame) {
    EXPECT_UI_THREAD();
    VerifyBrowser(__FUNCTION__, browser);
    // A frame is never valid after it's detached.
    VerifyFrame(__FUNCTION__, frame, /*expect_valid=*/false);

    GotCallback(__FUNCTION__, FRAME_DETACHED);
  }

  void OnMainFrameChanged(CefRefPtr<CefBrowser> browser,
                          CefRefPtr<CefFrame> old_frame,
                          CefRefPtr<CefFrame> new_frame) {
    EXPECT_UI_THREAD();
    EXPECT_TRUE(is_main_);
    VerifyBrowser(__FUNCTION__, browser);

    bool got_match = false;

    if (old_frame && new_frame) {
      EXPECT_NE(old_frame->GetIdentifier(), new_frame->GetIdentifier());
    }

    if (old_frame && IsSame(old_frame)) {
      got_match = true;
      // A frame is never valid after it's detached.
      VerifyFrame(__FUNCTION__, old_frame, /*expect_valid=*/false);
      GotCallback(__FUNCTION__, new_frame ? MAIN_FRAME_CHANGED_REMOVED
                                          : MAIN_FRAME_FINAL_REMOVED);
      if (is_last_main_) {
        EXPECT_FALSE(new_frame);
      }
    }

    if (new_frame && IsSame(new_frame)) {
      got_match = true;
      VerifyFrame(__FUNCTION__, new_frame);
      GotCallback(__FUNCTION__, old_frame ? MAIN_FRAME_CHANGED_ASSIGNED
                                          : MAIN_FRAME_INITIAL_ASSIGNED);
      if (is_first_main_) {
        EXPECT_FALSE(old_frame);
      }
    }

    EXPECT_TRUE(got_match);
  }

  void OnAfterCreated(CefRefPtr<CefBrowser> browser) {
    EXPECT_UI_THREAD();
    VerifyBrowser(__FUNCTION__, browser);

    auto frame = browser->GetMainFrame();
    VerifyFrame(__FUNCTION__, frame);

    GotCallback(__FUNCTION__, AFTER_CREATED);
    ExecuteQuery(frame, AFTER_CREATED);
  }

  // Called for all existing frames, not just the target frame.
  // We need to track this status to know if the browser should be valid in
  // following calls to OnFrameDetached.
  void OnBeforeClose(CefRefPtr<CefBrowser> browser) {
    EXPECT_UI_THREAD();
    VerifyBrowser(__FUNCTION__, browser);

    auto frame = browser->GetMainFrame();
    EXPECT_TRUE(frame->IsValid());

    got_before_close_ = true;
    if (IsSame(frame)) {
      VerifyFrame(__FUNCTION__, frame);
      GotCallback(__FUNCTION__, BEFORE_CLOSE);
    }
  }

  void OnLoadStart(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame) {
    EXPECT_UI_THREAD();
    VerifyBrowser(__FUNCTION__, browser);
    VerifyFrame(__FUNCTION__, frame);

    GotCallback(__FUNCTION__, LOAD_START);
    ExecuteQuery(frame, LOAD_START);
  }

  void OnLoadEnd(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame) {
    EXPECT_UI_THREAD();
    VerifyBrowser(__FUNCTION__, browser);
    VerifyFrame(__FUNCTION__, frame);

    GotCallback(__FUNCTION__, LOAD_END);
    ExecuteQuery(frame, LOAD_END);
  }

  void OnQuery(CefRefPtr<CefBrowser> browser,
               CefRefPtr<CefFrame> frame,
               const CefString& request) {
    EXPECT_UI_THREAD();

    const std::string& received_query = request;

#if VERBOSE_DEBUGGING
    LOG(INFO) << GetDebugString() << " recv query " << received_query << " ("
              << (delivered_query_ct_ + 1) << " of " << expected_query_ct_
              << ")";
#endif

    VerifyBrowser(__FUNCTION__, browser);
    VerifyFrame(__FUNCTION__, frame);

    EXPECT_GE(pending_queries_.size(), 1U);
    const std::string& expected_query = pending_queries_.front();
    EXPECT_STREQ(expected_query.c_str(), received_query.c_str());
    if (expected_query == received_query) {
      pending_queries_.pop();
    }

    EXPECT_LT(delivered_query_ct_, expected_query_ct_);
    delivered_query_ct_++;
  }

  void VerifyTestResults() {
    EXPECT_UI_THREAD();

    // Verify that all expected callbacks have executed.
    VerifyCallbackStatus(__FUNCTION__, CALLBACK_LAST + 1);

    if (is_temporary_) {
      // Should not receive any queries.
      EXPECT_FALSE(is_main_);
      EXPECT_EQ(0, delivered_query_ct_);
    } else {
      // Verify that all expected messages have been sent and received.
      EXPECT_EQ(expected_query_ct_, delivered_query_ct_);
      EXPECT_EQ(0U, pending_queries_.size());
      while (!pending_queries_.empty()) {
        ADD_FAILURE() << "Query sent but not received: "
                      << pending_queries_.front();
        pending_queries_.pop();
      }
    }
  }

  bool DidGetCallback(int callback) const { return got_callback_[callback]; }

 private:
  void GotCallback(const std::string& func, int callback) {
#if VERBOSE_DEBUGGING
    LOG(INFO) << GetDebugString() << " callback " << GetCallbackName(callback);
#endif

    EXPECT_TRUE(IsExpectedCallback(callback)) << func;
    VerifyCallbackStatus(func, callback);
    got_callback_[callback].yes();
  }

  bool IsExpectedCallback(int callback) const {
    if (!is_main_ && IsMainFrameOnlyCallback(callback)) {
      return false;
    }

    if (is_main_) {
      if ((callback == MAIN_FRAME_INITIAL_ASSIGNED ||
           callback == AFTER_CREATED) &&
          !is_first_main_) {
        return false;
      }
      if ((callback == BEFORE_CLOSE || callback == MAIN_FRAME_FINAL_REMOVED) &&
          !is_last_main_) {
        return false;
      }
      if (callback == MAIN_FRAME_CHANGED_ASSIGNED && is_first_main_) {
        return false;
      }
      if (callback == MAIN_FRAME_CHANGED_REMOVED && is_last_main_) {
        return false;
      }
    } else if (is_temporary_) {
      // For cross-process sub-frame navigation a sub-frame is first created in
      // the parent's renderer process. That sub-frame is then discarded after
      // the real cross-origin sub-frame is created in a different renderer
      // process. These discarded sub-frames will get OnFrameCreated/
      // OnFrameAttached immediately followed by OnFrameDetached.
      return callback == FRAME_CREATED || callback == FRAME_ATTACHED ||
             callback == FRAME_DETACHED;
    }

    return true;
  }

  void VerifyCallbackStatus(const std::string& func,
                            int current_callback) const {
    EXPECT_UI_THREAD();

    for (int i = 0; i <= CALLBACK_LAST; ++i) {
      if (i < current_callback && IsExpectedCallback(i)) {
        EXPECT_TRUE(got_callback_[i])
            << "inside " << func << " should already have gotten "
            << GetCallbackName(i);
      } else {
        EXPECT_FALSE(got_callback_[i])
            << "inside " << func << " should not already have gotten "
            << GetCallbackName(i);
      }
    }
  }

  void VerifyBrowser(const std::string& func,
                     CefRefPtr<CefBrowser> browser) const {
    const bool expect_valid = !got_before_close_;
    if (expect_valid) {
      EXPECT_TRUE(browser->IsValid()) << func;
    } else {
      EXPECT_FALSE(browser->IsValid()) << func;
    }

    // Note that this might not be the same main frame as us when navigating
    // cross-origin, because the new main frame object is assigned to the
    // browser before the CefFrameHandler callbacks related to main frame change
    // have executed. This started out as an implementation detail but it fits
    // nicely with the concept that "GetMainFrame() always returns a frame that
    // can be used", which wouldn't be the case if we returned the old frame
    // when calling GetMainFrame() from inside OnFrameCreated (for the new
    // frame), OnFrameDetached (for the old frame) or OnMainFrameChanged.
    auto main_frame = browser->GetMainFrame();
    if (expect_valid) {
      EXPECT_TRUE(main_frame) << func;
      EXPECT_TRUE(main_frame->IsValid()) << func;
      EXPECT_TRUE(main_frame->IsMain()) << func;
    } else {
      // GetMainFrame() returns nullptr after OnBeforeClose.
      EXPECT_FALSE(main_frame) << func;
    }
  }

  void VerifyFrame(const std::string& func,
                   CefRefPtr<CefFrame> frame,
                   bool expect_valid = true) const {
    if (expect_valid) {
      EXPECT_TRUE(frame->IsValid()) << func;
    } else {
      EXPECT_FALSE(frame->IsValid()) << func;
    }

    // |frame| should be us. This checks the frame type and ID.
    EXPECT_STREQ(ident_str_.c_str(), GetFrameDebugString(frame).c_str())
        << func;
  }

  void ExecuteQuery(CefRefPtr<CefFrame> frame, int callback) {
    EXPECT_UI_THREAD();
    const std::string& value = GetCallbackName(callback);

    std::string js_string;

#if VERBOSE_DEBUGGING
    LOG(INFO) << GetDebugString() << " sent query " << value;
    js_string +=
        "console.log('" + GetDebugString() + " exec query " + value + "');";
#endif

    js_string += "window.testQuery({request:'" + value + "'});";

    pending_queries_.push(value);

    // GetURL() will return an empty string for early callbacks, but that
    // doesn't appear to cause any issues.
    frame->ExecuteJavaScript(js_string, frame->GetURL(), 0);
  }

  // Reset state so we can get the same callback again.
  void ResetCallbackStatus(int callback, bool expect_query) {
    EXPECT_UI_THREAD();

    EXPECT_TRUE(got_callback_[callback]) << GetCallbackName(callback);
    got_callback_[callback].reset();

    if (expect_query) {
      delivered_query_ct_--;
    }
  }

  const int64_t frame_id_;
  const bool is_main_;
  const std::string ident_str_;

  bool is_first_main_ = false;
  bool is_last_main_ = false;
  bool is_temporary_ = false;
  std::string debug_info_;

  bool got_before_close_ = false;

  TrackCallback got_callback_[CALLBACK_LAST + 1];

  std::queue<std::string> pending_queries_;

  // Expect OnCreated, OnAttached, OnLoadStart, OnLoadEnd.
  int expected_query_ct_ = 4;
  int delivered_query_ct_ = 0;
};

const char kOrderMainUrl[] = "https://tests-frame-handler/main-order.html";

class OrderMainTestHandler : public RoutingTestHandler, public CefFrameHandler {
 public:
  OrderMainTestHandler(CompletionState* completion_state = nullptr)
      : RoutingTestHandler(completion_state) {}

  CefRefPtr<CefFrameHandler> GetFrameHandler() override {
    get_frame_handler_ct_++;
    return this;
  }

  void RunTest() override {
    // Add the main resource that we will navigate to/from.
    AddResource(GetMainURL(), GetMainHtml(), "text/html");

    // Create the browser.
    CreateBrowser(GetMainURL());

    // Time out the test after a reasonable period of time.
    SetTestTimeout();
  }

  void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
    EXPECT_UI_THREAD();
    RoutingTestHandler::OnAfterCreated(browser);

    EXPECT_FALSE(got_after_created_);
    got_after_created_ = true;

    EXPECT_TRUE(current_main_frame_);
    current_main_frame_->OnAfterCreated(browser);
  }

  void OnLoadStart(CefRefPtr<CefBrowser> browser,
                   CefRefPtr<CefFrame> frame,
                   TransitionType transition_type) override {
    EXPECT_UI_THREAD();
    RoutingTestHandler::OnLoadStart(browser, frame, transition_type);

    EXPECT_TRUE(current_main_frame_);
    current_main_frame_->OnLoadStart(browser, frame);
  }

  void OnLoadEnd(CefRefPtr<CefBrowser> browser,
                 CefRefPtr<CefFrame> frame,
                 int httpStatusCode) override {
    EXPECT_UI_THREAD();
    RoutingTestHandler::OnLoadEnd(browser, frame, httpStatusCode);

    EXPECT_TRUE(current_main_frame_);
    current_main_frame_->OnLoadEnd(browser, frame);

    MaybeDestroyTest();
  }

  void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
    EXPECT_UI_THREAD();

    EXPECT_FALSE(got_before_close_);
    got_before_close_ = true;

    EXPECT_TRUE(current_main_frame_);
    current_main_frame_->OnBeforeClose(browser);

    RoutingTestHandler::OnBeforeClose(browser);
  }

  bool OnQuery(CefRefPtr<CefBrowser> browser,
               CefRefPtr<CefFrame> frame,
               int64_t query_id,
               const CefString& request,
               bool persistent,
               CefRefPtr<Callback> callback) override {
    EXPECT_UI_THREAD();

    // Messages for the old and new frames are interleaved during cross-origin
    // navigation.
    if (pending_main_frame_) {
      EXPECT_TRUE(IsCrossOriginOrSameSiteBFCacheEnabled());
      pending_main_frame_->OnQuery(browser, frame, request);
    } else {
      EXPECT_TRUE(current_main_frame_);
      current_main_frame_->OnQuery(browser, frame, request);
    }

    MaybeDestroyTest();
    return true;
  }

  void OnFrameCreated(CefRefPtr<CefBrowser> browser,
                      CefRefPtr<CefFrame> frame) override {
    EXPECT_UI_THREAD();

    EXPECT_TRUE(frame->IsMain());
    EXPECT_FALSE(pending_main_frame_);

    // First callback referencing the new frame.
    pending_main_frame_ = new FrameStatus(frame);
    pending_main_frame_->SetAdditionalDebugInfo(GetAdditionalDebugInfo());
    pending_main_frame_->SetIsFirstMain(!got_after_created_);
    pending_main_frame_->SetIsLastMain(
        !IsCrossOriginOrSameSiteBFCacheEnabled() || IsLastNavigation());
    pending_main_frame_->OnFrameCreated(browser, frame);
  }

  void OnFrameAttached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame,
                       bool reattached) override {
    EXPECT_UI_THREAD();

    // May arrive before or after OnMainFrameChanged switches the frame (after
    // on initial browser creation, before on cross-origin navigation).
    if (pending_main_frame_) {
      EXPECT_TRUE(IsCrossOriginOrSameSiteBFCacheEnabled());
      pending_main_frame_->OnFrameAttached(browser, frame);
    } else {
      EXPECT_TRUE(current_main_frame_);
      current_main_frame_->OnFrameAttached(browser, frame);
    }
  }

  void OnFrameDetached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame) override {
    EXPECT_UI_THREAD();
    EXPECT_TRUE(current_main_frame_);
    current_main_frame_->OnFrameDetached(browser, frame);
  }

  void OnMainFrameChanged(CefRefPtr<CefBrowser> browser,
                          CefRefPtr<CefFrame> old_frame,
                          CefRefPtr<CefFrame> new_frame) override {
    EXPECT_UI_THREAD();
    EXPECT_TRUE(old_frame || new_frame);
    if (old_frame) {
      EXPECT_FALSE(old_frame->IsValid());
      EXPECT_TRUE(old_frame->IsMain());

      // May be nullptr with PopupOrderMainTestHandler.
      if (current_main_frame_) {
        // Last callback referencing the old frame.
        EXPECT_TRUE(current_main_frame_);
        current_main_frame_->OnMainFrameChanged(browser, old_frame, new_frame);
        current_main_frame_->VerifyTestResults();
        delete current_main_frame_;
        current_main_frame_ = nullptr;
      }
    }
    if (new_frame) {
      EXPECT_TRUE(new_frame->IsValid());
      EXPECT_TRUE(new_frame->IsMain());

      // Always called after OnFrameCreated. See also comments in
      // OnFrameAttached.
      EXPECT_TRUE(pending_main_frame_);
      pending_main_frame_->OnMainFrameChanged(browser, old_frame, new_frame);

      // The pending frame becomes the current frame.
      EXPECT_FALSE(current_main_frame_);
      current_main_frame_ = pending_main_frame_;
      pending_main_frame_ = nullptr;
    }

    if (old_frame && new_frame) {
      // Main frame changed due to cross-origin navigation.
      EXPECT_TRUE(IsCrossOriginOrSameSiteBFCacheEnabled());
      main_frame_changed_ct_++;
    }

    if (old_frame && !new_frame) {
      // Very last callback.
      VerifyTestResults();
    }
  }

 protected:
  virtual std::string GetMainURL() const { return kOrderMainUrl; }

  virtual std::string GetMainHtml() const {
    return "<html><body>TEST</body></html>";
  }

  virtual std::string GetNextMainURL() { return std::string(); }
  virtual bool IsFirstNavigation() const { return true; }
  virtual bool IsLastNavigation() const { return true; }
  virtual bool IsCrossOrigin() const { return false; }

  bool IsCrossOriginOrSameSiteBFCacheEnabled() const {
    return IsCrossOrigin() || IsSameSiteBFCacheEnabled();
  }

  virtual std::string GetAdditionalDebugInfo() const { return std::string(); }

  virtual bool AllQueriesDelivered(std::string* msg = nullptr) const {
    EXPECT_UI_THREAD();
    if (pending_main_frame_) {
      return false;
    }
    return current_main_frame_->AllQueriesDelivered(msg);
  }

  virtual bool AllFramesLoaded(std::string* msg = nullptr) const {
    EXPECT_UI_THREAD();
    if (pending_main_frame_) {
      return false;
    }
    return current_main_frame_->IsLoaded(msg);
  }

  void MaybeDestroyTest() {
#if VERBOSE_DEBUGGING
    std::string delivered_msg, loaded_msg;
    const bool all_queries_delivered = AllQueriesDelivered(&delivered_msg);
    const bool all_frames_loaded = AllFramesLoaded(&loaded_msg);
    LOG(INFO) << (current_main_frame_ ? current_main_frame_->GetDebugString()
                                      : "")
              << " AllQueriesDelivered=" << all_queries_delivered << " {"
              << delivered_msg << "}"
              << " AllFramesLoaded=" << all_frames_loaded << " {" << loaded_msg
              << "}";
    if (all_queries_delivered && all_frames_loaded) {
#else
    if (AllQueriesDelivered() && AllFramesLoaded()) {
#endif
      const std::string& next_url = GetNextMainURL();
      if (!next_url.empty()) {
        if (!IsCrossOriginOrSameSiteBFCacheEnabled()) {
          // Reusing the same main frame for same origin nav.
          current_main_frame_->ResetMainLoadStatus();
        }

#if VERBOSE_DEBUGGING
        LOG(INFO) << current_main_frame_->GetDebugString()
                  << "--> Navigating to " << next_url;
#endif
        GetBrowser()->GetMainFrame()->LoadURL(next_url);
      } else {
#if VERBOSE_DEBUGGING
        LOG(INFO) << "--> Destroy test";
#endif
        DestroyTest();
      }
    }
  }

  virtual void VerifyTestResults() {
    EXPECT_UI_THREAD();

    // OnMainFrameChanged should have cleaned up.
    EXPECT_FALSE(pending_main_frame_);
    EXPECT_FALSE(current_main_frame_);

    EXPECT_TRUE(got_after_created_);
    EXPECT_TRUE(got_before_close_);

    // We document GetFrameHandler() as only being called a single time.
    EXPECT_EQ(1, get_frame_handler_ct_);

    // Make sure we get the expected number OnMainFrameChanged callbacks for the
    // main frame.
    EXPECT_EQ(expected_main_frame_changed_ct_, main_frame_changed_ct_);
  }

  // Number of times we expect the main frame to change (e.g. once per
  // cross-origin navigation).
  int expected_main_frame_changed_ct_ = 0;

  bool got_after_created_ = false;
  bool got_before_close_ = false;

 private:
  int get_frame_handler_ct_ = 0;
  int main_frame_changed_ct_ = 0;

  FrameStatus* current_main_frame_ = nullptr;
  FrameStatus* pending_main_frame_ = nullptr;

  IMPLEMENT_REFCOUNTING(OrderMainTestHandler);
};

}  // namespace

// Test the ordering and behavior of main frame callbacks.
TEST(FrameHandlerTest, OrderMain) {
  CefRefPtr<OrderMainTestHandler> handler = new OrderMainTestHandler();
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

namespace {

const char kOrderMainUrlPrefix[] = "https://tests-frame-handler";

class NavigateOrderMainTestHandler : public OrderMainTestHandler {
 public:
  NavigateOrderMainTestHandler(bool cross_origin, int additional_nav_ct = 2)
      : cross_origin_(cross_origin), additional_nav_ct_(additional_nav_ct) {}

  void RunTest() override {
    // Once for each cross-origin LoadURL call.
    expected_main_frame_changed_ct_ =
        IsCrossOriginOrSameSiteBFCacheEnabled() ? additional_nav_ct_ : 0;

    // Resources for the 2nd+ navigation.
    for (int i = 1; i <= additional_nav_ct_; i++) {
      AddResource(GetURLForNav(i), GetMainHtmlForNav(i), "text/html");
    }

    OrderMainTestHandler::RunTest();
  }

 protected:
  // Loaded when the browser is created.
  std::string GetMainURL() const override { return GetURLForNav(0); }
  std::string GetMainHtml() const override { return GetMainHtmlForNav(0); }

  std::string GetNextMainURL() override {
    if (current_nav_ct_ == additional_nav_ct_) {
      // No more navigations.
      return std::string();
    }
    return GetURLForNav(++current_nav_ct_);
  }

  bool IsFirstNavigation() const override { return current_nav_ct_ == 0U; }
  bool IsLastNavigation() const override {
    return current_nav_ct_ == additional_nav_ct_;
  }
  bool IsCrossOrigin() const override { return cross_origin_; }
  int additional_nav_ct() const { return additional_nav_ct_; }

  void VerifyTestResults() override {
    OrderMainTestHandler::VerifyTestResults();
    EXPECT_TRUE(IsLastNavigation());
  }

  virtual std::string GetMainHtmlForNav(int nav) const {
    return "<html><body>TEST " + std::to_string(nav) + "</body></html>";
  }

  std::string GetURLForNav(int nav, const std::string& suffix = "") const {
    std::stringstream ss;
    if (cross_origin_) {
      ss << kOrderMainUrlPrefix << nav << "/cross-origin" << suffix << ".html";
    } else {
      ss << kOrderMainUrlPrefix << "/" << nav << "same-origin" << suffix
         << ".html";
    }
    return ss.str();
  }

 private:
  const bool cross_origin_;
  const int additional_nav_ct_;

  int current_nav_ct_ = 0;
};

}  // namespace

// Main frame navigating to different URLs with the same origin.
TEST(FrameHandlerTest, OrderMainNavSameOrigin) {
  CefRefPtr<NavigateOrderMainTestHandler> handler =
      new NavigateOrderMainTestHandler(/*cross_origin=*/false);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame navigating cross-origin.
TEST(FrameHandlerTest, OrderMainNavCrossOrigin) {
  CefRefPtr<NavigateOrderMainTestHandler> handler =
      new NavigateOrderMainTestHandler(/*cross_origin=*/true);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

namespace {

// Tracks sub-frames for a single main frame load.
class FrameStatusMap {
 public:
  explicit FrameStatusMap(size_t expected_frame_ct)
      : expected_frame_ct_(expected_frame_ct) {}
  ~FrameStatusMap() { EXPECT_TRUE(frame_map_.empty()); }

  bool Contains(CefRefPtr<CefFrame> frame) const {
    return frame_map_.find(frame->GetIdentifier()) != frame_map_.end();
  }

  FrameStatus* CreateFrameStatus(CefRefPtr<CefFrame> frame) {
    EXPECT_UI_THREAD();

    EXPECT_LT(size(), expected_frame_ct_);

    const int64_t id = frame->GetIdentifier();
    EXPECT_NE(kInvalidFrameId, id);
    EXPECT_EQ(frame_map_.find(id), frame_map_.end());

    FrameStatus* status = new FrameStatus(frame);
    frame_map_.insert(std::make_pair(id, status));
    return status;
  }

  FrameStatus* GetFrameStatus(CefRefPtr<CefFrame> frame) const {
    EXPECT_UI_THREAD();

    const int64_t id = frame->GetIdentifier();
    EXPECT_NE(kInvalidFrameId, id);
    Map::const_iterator it = frame_map_.find(id);
    EXPECT_NE(it, frame_map_.end());
    return it->second;
  }

  void RemoveFrameStatus(CefRefPtr<CefFrame> frame) {
    const int64_t id = frame->GetIdentifier();
    Map::iterator it = frame_map_.find(id);
    EXPECT_NE(it, frame_map_.end());
    frame_map_.erase(it);
  }

  void OnBeforeClose(CefRefPtr<CefBrowser> browser) {
    Map::const_iterator it = frame_map_.begin();
    for (; it != frame_map_.end(); ++it) {
      it->second->OnBeforeClose(browser);
    }
  }

  bool AllQueriesDelivered(std::string* msg = nullptr) const {
    if (size() != expected_frame_ct_) {
#if VERBOSE_DEBUGGING
      if (msg) {
        std::stringstream ss;
        ss << " SUB COUNT MISMATCH! size=" << size()
           << " expected=" << expected_frame_ct_;
        *msg += ss.str();
      }
#endif
      return false;
    }

    Map::const_iterator it = frame_map_.begin();
    for (; it != frame_map_.end(); ++it) {
      if (!it->second->AllQueriesDelivered(msg)) {
#if VERBOSE_DEBUGGING
        if (msg) {
          *msg += " " + it->second->GetDebugString() + " PENDING";
        }
#endif
        return false;
      }
    }

    return true;
  }

  bool AllFramesLoaded(std::string* msg = nullptr) const {
    if (size() != expected_frame_ct_) {
#if VERBOSE_DEBUGGING
      if (msg) {
        std::stringstream ss;
        ss << " SUB COUNT MISMATCH! size=" << size()
           << " expected=" << expected_frame_ct_;
        *msg += ss.str();
      }
#endif
      return false;
    }

    Map::const_iterator it = frame_map_.begin();
    for (; it != frame_map_.end(); ++it) {
      if (!it->second->IsTemporary() && !it->second->IsLoaded(msg)) {
#if VERBOSE_DEBUGGING
        if (msg) {
          *msg += " " + it->second->GetDebugString() + " PENDING";
        }
#endif
        return false;
      }
    }

    return true;
  }

  bool AllFramesDetached() const {
    if (size() != expected_frame_ct_) {
      return false;
    }

    Map::const_iterator it = frame_map_.begin();
    for (; it != frame_map_.end(); ++it) {
      if (!it->second->IsDetached()) {
        return false;
      }
    }

    return true;
  }

  void VerifyAndClearTestResults() {
    EXPECT_EQ(expected_frame_ct_, size());
    Map::const_iterator it = frame_map_.begin();
    for (; it != frame_map_.end(); ++it) {
      it->second->VerifyTestResults();
      delete it->second;
    }
    frame_map_.clear();
  }

  size_t size() const { return frame_map_.size(); }

 private:
  using Map = std::map<int64_t, FrameStatus*>;
  Map frame_map_;

  // The expected number of sub-frames.
  const size_t expected_frame_ct_;
};

class OrderSubTestHandler : public NavigateOrderMainTestHandler {
 public:
  enum TestMode {
    // Two sub-frames at the same level.
    SUBFRAME_PEERS,

    // One sub-frame inside the other.
    SUBFRAME_CHILDREN,
  };

  OrderSubTestHandler(bool cross_origin,
                      int additional_nav_ct,
                      TestMode mode,
                      size_t expected_frame_ct = 2U)
      : NavigateOrderMainTestHandler(cross_origin, additional_nav_ct),
        test_mode_(mode),
        expected_frame_ct_(expected_frame_ct) {}
  ~OrderSubTestHandler() override { EXPECT_TRUE(frame_maps_.empty()); }

  void RunTest() override {
    for (int i = 0; i <= additional_nav_ct(); i++) {
      AddResource(GetSubURL1ForNav(i), GetSubFrameHtml1ForNav(i), "text/html");
      AddResource(GetSubURL2ForNav(i), GetSubFrameHtml2ForNav(i), "text/html");
    }

    NavigateOrderMainTestHandler::RunTest();
  }

  void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
    NavigateOrderMainTestHandler::OnBeforeClose(browser);

    for (auto& map : frame_maps_) {
      // Also need to notify any sub-frames.
      map->OnBeforeClose(browser);
    }
  }

  bool OnQuery(CefRefPtr<CefBrowser> browser,
               CefRefPtr<CefFrame> frame,
               int64_t query_id,
               const CefString& request,
               bool persistent,
               CefRefPtr<Callback> callback) override {
    if (!frame->IsMain()) {
      auto map = GetFrameMap(frame);
      auto status = map->GetFrameStatus(frame);
      status->OnQuery(browser, frame, request);
      if (status->AllQueriesDelivered()) {
        MaybeDestroyTest();
      }
      return true;
    }

    return NavigateOrderMainTestHandler::OnQuery(browser, frame, query_id,
                                                 request, persistent, callback);
  }

  void OnFrameCreated(CefRefPtr<CefBrowser> browser,
                      CefRefPtr<CefFrame> frame) override {
    if (!frame->IsMain()) {
      // Potentially the first notification of a new sub-frame after navigation.
      auto map = GetOrCreateFrameMap(frame);
      auto status = map->CreateFrameStatus(frame);
      status->SetAdditionalDebugInfo(GetAdditionalDebugInfo());
      status->OnFrameCreated(browser, frame);
      return;
    }

    NavigateOrderMainTestHandler::OnFrameCreated(browser, frame);
  }

  void OnFrameAttached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame,
                       bool reattached) override {
    if (!frame->IsMain()) {
      auto map = GetFrameMap(frame);
      auto status = map->GetFrameStatus(frame);
      status->OnFrameAttached(browser, frame);
      return;
    }

    NavigateOrderMainTestHandler::OnFrameAttached(browser, frame, reattached);
  }

  void OnFrameDetached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame) override {
    if (!frame->IsMain()) {
      // Potentially the last notification for an old sub-frame after
      // navigation.
      auto map = GetFrameMap(frame);
      auto status = map->GetFrameStatus(frame);
      status->OnFrameDetached(browser, frame);

      if (map->AllFramesDetached()) {
        // Verify results from the previous navigation.
        VerifyAndClearSubFrameTestResults(map);
      }
      return;
    }

    NavigateOrderMainTestHandler::OnFrameDetached(browser, frame);
  }

  void OnLoadStart(CefRefPtr<CefBrowser> browser,
                   CefRefPtr<CefFrame> frame,
                   TransitionType transition_type) override {
    if (!frame->IsMain()) {
      auto map = GetFrameMap(frame);
      auto status = map->GetFrameStatus(frame);
      status->OnLoadStart(browser, frame);
      return;
    }

    NavigateOrderMainTestHandler::OnLoadStart(browser, frame, transition_type);
  }

  void OnLoadEnd(CefRefPtr<CefBrowser> browser,
                 CefRefPtr<CefFrame> frame,
                 int httpStatusCode) override {
    if (!frame->IsMain()) {
      auto map = GetFrameMap(frame);
      auto status = map->GetFrameStatus(frame);
      status->OnLoadEnd(browser, frame);
      return;
    }

    NavigateOrderMainTestHandler::OnLoadEnd(browser, frame, httpStatusCode);
  }

 protected:
  virtual std::string GetSubURL1ForNav(int nav) const {
    return GetURLForNav(nav, "sub1");
  }

  std::string GetSubFrameHtml1ForNav(int nav) const {
    if (test_mode_ == SUBFRAME_CHILDREN) {
      return "<iframe src=\"" + GetSubURL2ForNav(nav) + "\">";
    }
    return "<html><body>Sub1</body></html>";
  }

  virtual std::string GetSubURL2ForNav(int nav) const {
    return GetURLForNav(nav, "sub2");
  }

  std::string GetSubFrameHtml2ForNav(int nav) const {
    return "<html><body>Sub2</body></html>";
  }

  std::string GetMainHtmlForNav(int nav) const override {
    if (test_mode_ == SUBFRAME_PEERS) {
      return "<html><body><iframe src=\"" + GetSubURL1ForNav(nav) +
             "\"></iframe><iframe src=\"" + GetSubURL2ForNav(nav) +
             "\"></iframe></body></html>";
    } else if (test_mode_ == SUBFRAME_CHILDREN) {
      return "<html><body><iframe src=\"" + GetSubURL1ForNav(nav) +
             "\"></iframe></iframe></body></html>";
    }
    NOTREACHED();
    return std::string();
  }

  bool AllQueriesDelivered(std::string* msg = nullptr) const override {
    if (!NavigateOrderMainTestHandler::AllQueriesDelivered(msg)) {
#if VERBOSE_DEBUGGING
      if (msg) {
        *msg += " MAIN PENDING";
      }
#endif
      return false;
    }

    if (frame_maps_.empty()) {
#if VERBOSE_DEBUGGING
      if (msg) {
        *msg += " NO SUBS";
      }
#endif
      return false;
    }

    if (!frame_maps_.back()->AllQueriesDelivered(msg)) {
#if VERBOSE_DEBUGGING
      if (msg) {
        *msg += " SUBS PENDING";
      }
#endif
      return false;
    }
    return true;
  }

  bool AllFramesLoaded(std::string* msg = nullptr) const override {
    if (!NavigateOrderMainTestHandler::AllFramesLoaded(msg)) {
#if VERBOSE_DEBUGGING
      if (msg) {
        *msg += " MAIN PENDING";
      }
#endif
      return false;
    }

    if (frame_maps_.empty()) {
#if VERBOSE_DEBUGGING
      if (msg) {
        *msg += " NO SUBS";
      }
#endif
      return false;
    }

    if (!frame_maps_.back()->AllFramesLoaded(msg)) {
#if VERBOSE_DEBUGGING
      if (msg) {
        *msg += " SUBS PENDING";
      }
#endif
      return false;
    }
    return true;
  }

  void VerifyTestResults() override {
    NavigateOrderMainTestHandler::VerifyTestResults();
    EXPECT_TRUE(frame_maps_.empty());
  }

  size_t expected_frame_ct() const { return expected_frame_ct_; }

  FrameStatusMap* GetFrameMap(CefRefPtr<CefFrame> frame) const {
    for (auto& map : frame_maps_) {
      if (map->Contains(frame)) {
        return map.get();
      }
    }
    return nullptr;
  }

 private:
  // All sub-frame objects should already have received all callbacks.
  void VerifyAndClearSubFrameTestResults(FrameStatusMap* map) {
    map->VerifyAndClearTestResults();

    bool found = false;
    FrameStatusMapVector::iterator it = frame_maps_.begin();
    for (; it != frame_maps_.end(); ++it) {
      if ((*it).get() == map) {
        frame_maps_.erase(it);
        found = true;
        break;
      }
    }

    EXPECT_TRUE(found);
  }

  FrameStatusMap* GetOrCreateFrameMap(CefRefPtr<CefFrame> frame) {
    if (auto map = GetFrameMap(frame)) {
      return map;
    }

    if (frame_maps_.empty() ||
        frame_maps_.back()->size() >= expected_frame_ct_) {
      // Start a new frame map.
      auto map = std::make_unique<FrameStatusMap>(expected_frame_ct_);
      frame_maps_.push_back(std::move(map));
    }

    return frame_maps_.back().get();
  }

  const TestMode test_mode_;

  // The expected number of sub-frames.
  const size_t expected_frame_ct_;

  using FrameStatusMapVector = std::vector<std::unique_ptr<FrameStatusMap>>;
  FrameStatusMapVector frame_maps_;
};

}  // namespace

// Main frame loads two sub-frames that are peers in the same origin.
TEST(FrameHandlerTest, OrderSubSameOriginPeers) {
  CefRefPtr<OrderSubTestHandler> handler =
      new OrderSubTestHandler(/*cross_origin=*/false, /*additional_nav_ct=*/0,
                              OrderSubTestHandler::SUBFRAME_PEERS);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads two sub-frames that are peers in the same origin, then
// navigates in the same origin and does it again twice.
TEST(FrameHandlerTest, OrderSubSameOriginPeersNavSameOrigin) {
  CefRefPtr<OrderSubTestHandler> handler =
      new OrderSubTestHandler(/*cross_origin=*/false, /*additional_nav_ct=*/2,
                              OrderSubTestHandler::SUBFRAME_PEERS);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads two sub-frames that are peers in the same origin, then
// navigates cross-origin and does it again twice.
TEST(FrameHandlerTest, OrderSubSameOriginPeersNavCrossOrigin) {
  CefRefPtr<OrderSubTestHandler> handler =
      new OrderSubTestHandler(/*cross_origin=*/true, /*additional_nav_ct=*/2,
                              OrderSubTestHandler::SUBFRAME_PEERS);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads a sub-frame that then has it's own sub-frame.
TEST(FrameHandlerTest, OrderSubSameOriginChildren) {
  CefRefPtr<OrderSubTestHandler> handler =
      new OrderSubTestHandler(/*cross_origin=*/false, /*additional_nav_ct=*/0,
                              OrderSubTestHandler::SUBFRAME_CHILDREN);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads a sub-frame that then has it's own sub-frame, then navigates
// in the same origin and does it again twice.
TEST(FrameHandlerTest, OrderSubSameOriginChildrenNavSameOrigin) {
  CefRefPtr<OrderSubTestHandler> handler =
      new OrderSubTestHandler(/*cross_origin=*/false, /*additional_nav_ct=*/2,
                              OrderSubTestHandler::SUBFRAME_CHILDREN);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads a sub-frame that then has it's own sub-frame, then navigates
// cross-origin and does it again twice.
TEST(FrameHandlerTest, OrderSubSameOriginChildrenNavCrossOrigin) {
  CefRefPtr<OrderSubTestHandler> handler =
      new OrderSubTestHandler(/*cross_origin=*/true, /*additional_nav_ct=*/2,
                              OrderSubTestHandler::SUBFRAME_CHILDREN);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

namespace {

// Like above, but also navigating the sub-frames cross-origin.
class CrossOriginOrderSubTestHandler : public OrderSubTestHandler {
 public:
  CrossOriginOrderSubTestHandler(int additional_nav_ct, TestMode mode)
      : OrderSubTestHandler(/*cross_origin=*/true,
                            additional_nav_ct,
                            mode,
                            /*expected_frame_ct=*/4U) {}

  void OnFrameDetached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame) override {
    // A sub-frame is first created in the parent's renderer process. That
    // sub-frame is then discarded after the real cross-origin sub-frame is
    // created in a different renderer process. These discarded sub-frames will
    // get OnFrameCreated/OnFrameAttached immediately followed by
    // OnFrameDetached.
    if (!frame->IsMain()) {
      auto map = GetFrameMap(frame);
      auto status = map->GetFrameStatus(frame);
      if (status && !status->DidGetCallback(FrameStatus::LOAD_START)) {
        status->SetIsTemporary(true);
        temp_frame_detached_ct_++;
      }
    }

    OrderSubTestHandler::OnFrameDetached(browser, frame);
  }

 protected:
  std::string GetSubURL1ForNav(int nav) const override {
    std::stringstream ss;
    ss << kOrderMainUrlPrefix << nav << "-sub1/sub-cross-origin.html";
    return ss.str();
  }

  std::string GetSubURL2ForNav(int nav) const override {
    std::stringstream ss;
    ss << kOrderMainUrlPrefix << nav << "-sub2/sub-cross-origin.html";
    return ss.str();
  }

  void VerifyTestResults() override {
    OrderSubTestHandler::VerifyTestResults();

    const size_t expected_temp_ct =
        (expected_frame_ct() / 2U) * (1U + additional_nav_ct());
    EXPECT_EQ(expected_temp_ct, temp_frame_detached_ct_);
  }

 private:
  size_t temp_frame_detached_ct_ = 0U;
};

}  // namespace

// Main frame loads two sub-frames that are peers in a different origin.
TEST(FrameHandlerTest, OrderSubCrossOriginPeers) {
  CefRefPtr<CrossOriginOrderSubTestHandler> handler =
      new CrossOriginOrderSubTestHandler(
          /*additional_nav_ct=*/0,
          CrossOriginOrderSubTestHandler::SUBFRAME_PEERS);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads two sub-frames that are peers in a different origin, then
// navigates cross-origin and does it again twice.
TEST(FrameHandlerTest, OrderSubCrossOriginPeersNavCrossOrigin) {
  CefRefPtr<CrossOriginOrderSubTestHandler> handler =
      new CrossOriginOrderSubTestHandler(
          /*additional_nav_ct=*/2,
          CrossOriginOrderSubTestHandler::SUBFRAME_PEERS);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads a sub-frame in a different origin that then has it's own
// sub-frame in a different origin.
TEST(FrameHandlerTest, OrderSubCrossOriginChildren) {
  CefRefPtr<CrossOriginOrderSubTestHandler> handler =
      new CrossOriginOrderSubTestHandler(
          /*additional_nav_ct=*/0,
          CrossOriginOrderSubTestHandler::SUBFRAME_CHILDREN);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Main frame loads a sub-frame in a different origin that then has it's own
// sub-frame in a different origin, then navigates cross-origin and does it
// again twice.
TEST(FrameHandlerTest, OrderSubCrossOriginChildrenNavCrossOrigin) {
  CefRefPtr<CrossOriginOrderSubTestHandler> handler =
      new CrossOriginOrderSubTestHandler(
          /*additional_nav_ct=*/2,
          CrossOriginOrderSubTestHandler::SUBFRAME_CHILDREN);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

namespace {

const char kOrderMainCrossUrl[] =
    "https://tests-frame-handler-cross/main-order.html";

// Will be assigned as popup handler via
// ParentOrderMainTestHandler::OnBeforePopup.
class PopupOrderMainTestHandler : public OrderMainTestHandler {
 public:
  PopupOrderMainTestHandler(CompletionState* completion_state,
                            bool cross_origin)
      : OrderMainTestHandler(completion_state), cross_origin_(cross_origin) {
    expected_main_frame_changed_ct_ = cross_origin_ ? 1 : 0;
  }

  void SetupTest() override {
    // Proceed to RunTest().
    SetupComplete();
  }

  void RunTest() override {
    // Add the main resource that we will navigate to/from.
    AddResource(GetMainURL(), GetMainHtml(), "text/html");

    // Time out the test after a reasonable period of time.
    SetTestTimeout();
  }

  void OnFrameCreated(CefRefPtr<CefBrowser> browser,
                      CefRefPtr<CefFrame> frame) override {
    EXPECT_UI_THREAD();

    EXPECT_TRUE(frame->IsMain());
    if (cross_origin_ && !temp_main_frame_) {
      // The first main frame in the popup will be created in the parent
      // process.
      EXPECT_FALSE(got_temp_created_);
      got_temp_created_.yes();

      temp_main_frame_ = new FrameStatus(frame);
      temp_main_frame_->SetAdditionalDebugInfo(GetAdditionalDebugInfo() +
                                               "temp ");
      temp_main_frame_->SetIsFirstMain(true);
      temp_main_frame_->OnFrameCreated(browser, frame);
      return;
    }

    OrderMainTestHandler::OnFrameCreated(browser, frame);
  }

  void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
    if (temp_main_frame_ && temp_main_frame_->IsSame(browser->GetMainFrame())) {
      EXPECT_FALSE(got_after_created_);
      got_after_created_ = true;

      EXPECT_TRUE(cross_origin_);
      temp_main_frame_->OnAfterCreated(browser);

      // Intentionally skipping the immediate parent class method.
      RoutingTestHandler::OnAfterCreated(browser);
      return;
    }

    OrderMainTestHandler::OnAfterCreated(browser);
  }

  void OnFrameAttached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame,
                       bool reattached) override {
    if (temp_main_frame_ && temp_main_frame_->IsSame(frame)) {
      EXPECT_TRUE(cross_origin_);
      temp_main_frame_->OnFrameAttached(browser, frame);
      return;
    }

    OrderMainTestHandler::OnFrameAttached(browser, frame, reattached);
  }

  void OnMainFrameChanged(CefRefPtr<CefBrowser> browser,
                          CefRefPtr<CefFrame> old_frame,
                          CefRefPtr<CefFrame> new_frame) override {
    if (temp_main_frame_ && temp_main_frame_->IsSame(new_frame)) {
      EXPECT_TRUE(cross_origin_);
      temp_main_frame_->OnMainFrameChanged(browser, old_frame, new_frame);
      return;
    }

    OrderMainTestHandler::OnMainFrameChanged(browser, old_frame, new_frame);
  }

  void OnFrameDetached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame) override {
    if (temp_main_frame_ && temp_main_frame_->IsSame(frame)) {
      EXPECT_TRUE(cross_origin_);
      EXPECT_FALSE(got_temp_destroyed_);
      got_temp_destroyed_.yes();

#if VERBOSE_DEBUGGING
      LOG(INFO) << temp_main_frame_->GetDebugString()
                << " callback OnFrameDetached(discarded)";
#endif

      // All of the initial main frame callbacks go to the proxy.
      EXPECT_TRUE(temp_main_frame_->DidGetCallback(FrameStatus::AFTER_CREATED));
      EXPECT_TRUE(temp_main_frame_->DidGetCallback(
          FrameStatus::MAIN_FRAME_INITIAL_ASSIGNED));
      EXPECT_TRUE(!temp_main_frame_->DidGetCallback(FrameStatus::LOAD_START));
      EXPECT_TRUE(temp_main_frame_->DidGetCallback(FrameStatus::FRAME_CREATED));
      EXPECT_TRUE(
          temp_main_frame_->DidGetCallback(FrameStatus::FRAME_ATTACHED));

      // Should receive queries for OnFrameCreated, OnAfterCreated,
      // OnFrameAttached.
      EXPECT_EQ(temp_main_frame_->QueriesDeliveredCount(), 3);

      delete temp_main_frame_;
      temp_main_frame_ = nullptr;
      return;
    }

    OrderMainTestHandler::OnFrameDetached(browser, frame);
  }

  bool OnQuery(CefRefPtr<CefBrowser> browser,
               CefRefPtr<CefFrame> frame,
               int64_t query_id,
               const CefString& request,
               bool persistent,
               CefRefPtr<Callback> callback) override {
    if (temp_main_frame_ && temp_main_frame_->IsSame(frame)) {
      EXPECT_TRUE(cross_origin_);
      temp_main_frame_->OnQuery(browser, frame, request);
      return true;
    }

    return OrderMainTestHandler::OnQuery(browser, frame, query_id, request,
                                         persistent, callback);
  }

  std::string GetMainURL() const override {
    return cross_origin_ ? kOrderMainCrossUrl : kOrderMainUrl;
  }

 protected:
  bool IsCrossOrigin() const override { return cross_origin_; }

  void VerifyTestResults() override {
    OrderMainTestHandler::VerifyTestResults();

    if (cross_origin_) {
      EXPECT_TRUE(got_temp_created_);
      EXPECT_TRUE(got_temp_destroyed_);
    } else {
      EXPECT_FALSE(got_temp_created_);
      EXPECT_FALSE(got_temp_destroyed_);
    }
    EXPECT_FALSE(temp_main_frame_);
  }

 private:
  std::string GetAdditionalDebugInfo() const override { return " popup: "; }

  const bool cross_origin_;

  TrackCallback got_temp_created_;
  TrackCallback got_temp_destroyed_;
  FrameStatus* temp_main_frame_ = nullptr;
};

class ParentOrderMainTestHandler : public OrderMainTestHandler {
 public:
  ParentOrderMainTestHandler(CompletionState* completion_state,
                             CefRefPtr<PopupOrderMainTestHandler> popup_handler)
      : OrderMainTestHandler(completion_state), popup_handler_(popup_handler) {}

  bool OnBeforePopup(
      CefRefPtr<CefBrowser> browser,
      CefRefPtr<CefFrame> frame,
      const CefString& target_url,
      const CefString& target_frame_name,
      CefLifeSpanHandler::WindowOpenDisposition target_disposition,
      bool user_gesture,
      const CefPopupFeatures& popupFeatures,
      CefWindowInfo& windowInfo,
      CefRefPtr<CefClient>& client,
      CefBrowserSettings& settings,
      CefRefPtr<CefDictionaryValue>& extra_info,
      bool* no_javascript_access) override {
    // Intentionally not calling the parent class method.
    EXPECT_FALSE(got_on_before_popup_);
    got_on_before_popup_.yes();

    client = popup_handler_;
    popup_handler_ = nullptr;

    // Proceed with popup creation.
    return false;
  }

  void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
    OrderMainTestHandler::OnAfterCreated(browser);

    GrantPopupPermission(browser->GetHost()->GetRequestContext(), GetMainURL());

    // Create the popup ASAP.
    browser->GetMainFrame()->ExecuteJavaScript(
        "window.open('" + popup_handler_->GetMainURL() + "');", CefString(), 0);
  }

  void SetupTest() override {
    // Proceed to RunTest().
    SetupComplete();
  }

  void DestroyTest() override {
    EXPECT_TRUE(got_on_before_popup_);
    OrderMainTestHandler::DestroyTest();
  }

 private:
  std::string GetAdditionalDebugInfo() const override { return "parent: "; }

  CefRefPtr<PopupOrderMainTestHandler> popup_handler_;

  TrackCallback got_on_before_popup_;
};

void RunOrderMainPopupTest(bool cross_origin) {
  TestHandler::CompletionState completion_state(/*count=*/2);
  TestHandler::Collection collection(&completion_state);

  CefRefPtr<PopupOrderMainTestHandler> popup_handler =
      new PopupOrderMainTestHandler(&completion_state, cross_origin);
  CefRefPtr<ParentOrderMainTestHandler> parent_handler =
      new ParentOrderMainTestHandler(&completion_state, popup_handler);

  collection.AddTestHandler(popup_handler.get());
  collection.AddTestHandler(parent_handler.get());
  collection.ExecuteTests();

  ReleaseAndWaitForDestructor(parent_handler);
  ReleaseAndWaitForDestructor(popup_handler);
}

}  // namespace

// Test the ordering and behavior of main frame callbacks in a popup with the
// same origin.
TEST(FrameHandlerTest, OrderMainPopupSameOrigin) {
  RunOrderMainPopupTest(/*cross_origin=*/false);
}

// Test the ordering and behavior of main frame callbacks in a popup with a
// different origin.
TEST(FrameHandlerTest, OrderMainPopupCrossOrigin) {
  RunOrderMainPopupTest(/*cross_origin=*/true);
}