// Copyright (c) 2015 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 "include/base/cef_callback.h"
#include "include/wrapper/cef_closure_task.h"
#include "tests/ceftests/test_handler.h"
#include "tests/gtest/include/gtest/gtest.h"

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

namespace {

const char kTestHTMLWithRegions[] =
    "<html>"
    "  <body>"
    "    <div style=\"position: absolute; top: 50px; left: 50px; width: 200px; "
    "height: 200px; background-color: red; -webkit-app-region: drag;\">"
    "      <div style=\"position: absolute; top: 50%; left: 50%; "
    "transform: translate(-50%, -50%); width: 50px; height: 50px; "
    "background-color: blue; -webkit-app-region: no-drag;\">"
    "      </div>"
    "    </div>"
    "  </body>"
    "</html>";

const char kTestHTMLWithoutRegions[] = "<html><body>Hello World!</body></html>";

const char kTestHTMLWithChangingRegions[] =
    "<html>"
    "  <body>"
    "    <div id=\"layer\" style=\"position: absolute; top: 50px; left: 50px; "
    "width: 200px; height: 200px; background-color: red; "
    "-webkit-app-region: drag;\">"
    "      <div style=\"position: absolute; top: 50%; left: 50%; "
    "transform: translate(-50%, -50%); width: 50px; height: 50px; "
    "background-color: blue; -webkit-app-region: no-drag;\">"
    "      </div>"
    "    </div>"
    "    <script>"
    "      window.setTimeout(function() {"
    "        var layer = document.getElementById('layer');"
    "        layer.style.top = '0px';"
    "        layer.style.left = '0px';"
    "      }, 500);"
    "    </script>"
    "  </body>"
    "</html>";

class DraggableRegionsTestHandler : public TestHandler,
                                    public CefDragHandler,
                                    public CefFrameHandler {
 public:
  // Test steps executed in order.
  enum Step {
    // Nav 1: Two regions (get notification).
    kStepWithRegions = 1,
    // Nav 2: Starts with the same region as Nav 1 (no notification),
    // then a changed region (get notification).
    kStepWithChangingRegions,
    // Nav 3: No regions (get notification).
    kStepWithoutRegions,
    // GoBack: Two regions (get notification), then a changed region (get
    // notification). Note the first notification is not sent if
    // BackForwardCache is enabled.
    kStepWithChangingRegions2,
    kStepWithChangingRegions3,
    // GoForward: No regions (get notification).
    kStepWithoutRegions2,

    kStepMax = kStepWithoutRegions2,
  };

  explicit DraggableRegionsTestHandler(bool same_origin)
      : same_origin_(same_origin) {}

  void RunTest() override {
    // Add HTML documents with and without draggable regions.
    AddResource(GetURL(kStepWithRegions), kTestHTMLWithRegions, "text/html");
    AddResource(GetURL(kStepWithChangingRegions), kTestHTMLWithChangingRegions,
                "text/html");
    AddResource(GetURL(kStepWithoutRegions), kTestHTMLWithoutRegions,
                "text/html");

    // Create the browser
    CreateBrowser(GetURL(kStepWithRegions));

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

  CefRefPtr<CefDragHandler> GetDragHandler() override { return this; }
  CefRefPtr<CefFrameHandler> GetFrameHandler() override { return this; }

  void OnDraggableRegionsChanged(
      CefRefPtr<CefBrowser> browser,
      CefRefPtr<CefFrame> frame,
      const std::vector<CefDraggableRegion>& regions) override {
    EXPECT_UI_THREAD();
    EXPECT_TRUE(browser->IsSame(GetBrowser()));
    EXPECT_TRUE(frame->IsMain());

    draggable_regions_changed_ct_++;

#if VERBOSE_DEBUGGING
    LOG(INFO) << "step " << step_ << " regions.size " << regions.size()
              << " url " << frame->GetURL().ToString();
    if (regions.size() == 2) {
      LOG(INFO) << "  region[0] x " << regions[0].bounds.x << " y "
                << regions[0].bounds.y << " width " << regions[0].bounds.width
                << " height " << regions[0].bounds.height;
      LOG(INFO) << "  region[1] x " << regions[1].bounds.x << " y "
                << regions[1].bounds.y << " width " << regions[1].bounds.width
                << " height " << regions[1].bounds.height;
    }
#endif  // VERBOSE_DEBUGGING

    switch (step_) {
      case kStepWithRegions:
      case kStepWithChangingRegions2:
        EXPECT_EQ(2U, regions.size()) << step_;
        if (regions.size() != 2U) {
          break;
        }
        EXPECT_NEAR(50, regions[0].bounds.x, 1);
        EXPECT_NEAR(50, regions[0].bounds.y, 1);
        EXPECT_NEAR(200, regions[0].bounds.width, 1);
        EXPECT_NEAR(200, regions[0].bounds.height, 1);
        EXPECT_EQ(1, regions[0].draggable);
        EXPECT_NEAR(125, regions[1].bounds.x, 1);
        EXPECT_NEAR(125, regions[1].bounds.y, 1);
        EXPECT_NEAR(50, regions[1].bounds.width, 1);
        EXPECT_NEAR(50, regions[1].bounds.height, 1);
        EXPECT_EQ(0, regions[1].draggable);
        break;
      case kStepWithChangingRegions:
      case kStepWithChangingRegions3:
        EXPECT_EQ(2U, regions.size()) << step_;
        if (regions.size() != 2U) {
          break;
        }
        EXPECT_EQ(0, regions[0].bounds.x);
        EXPECT_EQ(0, regions[0].bounds.y);
        EXPECT_NEAR(200, regions[0].bounds.width, 1);
        EXPECT_NEAR(200, regions[0].bounds.height, 1);
        EXPECT_EQ(1, regions[0].draggable);
        EXPECT_NEAR(75, regions[1].bounds.x, 1);
        EXPECT_NEAR(75, regions[1].bounds.y, 1);
        EXPECT_NEAR(50, regions[1].bounds.width, 2);
        EXPECT_NEAR(50, regions[1].bounds.height, 2);
        EXPECT_EQ(0, regions[1].draggable);
        break;
      case kStepWithoutRegions:
      case kStepWithoutRegions2:
        EXPECT_TRUE(regions.empty()) << step_;
        break;
    }

    NextTest(browser);
  }

  void OnFrameAttached(CefRefPtr<CefBrowser> browser,
                       CefRefPtr<CefFrame> frame,
                       bool reattached) override {
    EXPECT_UI_THREAD();
    EXPECT_TRUE(browser->IsSame(GetBrowser()));
    EXPECT_TRUE(frame->IsMain());

    if (reattached) {
      // When BackForwardCache is enabled and we go back to
      // kTestHTMLWithChangingRegions, draggable regions will already be in the
      // final position because the page content is not reloaded.
      if (step_ == kStepWithChangingRegions2) {
        step_ = kStepWithChangingRegions3;
        expected_draggable_regions_changed_ct_--;
      }
    }
  }

  void DestroyTest() override {
    EXPECT_EQ(expected_draggable_regions_changed_ct_,
              draggable_regions_changed_ct_);

    TestHandler::DestroyTest();
  }

 private:
  void NextTest(CefRefPtr<CefBrowser> browser) {
    CefRefPtr<CefFrame> frame(browser->GetMainFrame());

    switch (step_) {
      case kStepWithRegions:
        step_ = kStepWithChangingRegions;
        frame->LoadURL(GetURL(kStepWithChangingRegions));
        break;
      case kStepWithChangingRegions:
        step_ = kStepWithoutRegions;
        frame->LoadURL(GetURL(kStepWithoutRegions));
        break;
      case kStepWithoutRegions: {
        step_ = kStepWithChangingRegions2;
        browser->GoBack();
        break;
      }
      case kStepWithChangingRegions2: {
        step_ = kStepWithChangingRegions3;
        break;
      }
      case kStepWithChangingRegions3: {
        step_ = kStepWithoutRegions2;
        browser->GoForward();
        break;
      }
      case kStepWithoutRegions2: {
        DestroyTest();
        break;
      }
    }
  }

  std::string GetURL(Step step) const {
    // When |same_origin_| is true every other URL gets a different origin.
    switch (step) {
      case kStepWithRegions:
        return same_origin_ ? "https://test.com/regions"
                            : "https://test2.com/regions";
      case kStepWithChangingRegions:
      case kStepWithChangingRegions2:
      case kStepWithChangingRegions3:
        return "https://test.com/changing-regions";
      case kStepWithoutRegions:
      case kStepWithoutRegions2:
        return same_origin_ ? "https://test.com/no-regions"
                            : "https://test2.com/no-regions";
    }

    NOTREACHED();
    return "";
  }

  const bool same_origin_;

  Step step_ = kStepWithRegions;
  int draggable_regions_changed_ct_ = 0;
  int expected_draggable_regions_changed_ct_ = kStepMax;

  IMPLEMENT_REFCOUNTING(DraggableRegionsTestHandler);
};

}  // namespace

// Verify that draggable regions work in the same origin.
TEST(DraggableRegionsTest, DraggableRegionsSameOrigin) {
  CefRefPtr<DraggableRegionsTestHandler> handler =
      new DraggableRegionsTestHandler(/*same_origin=*/true);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}

// Verify that draggable regions work cross-origin.
TEST(DraggableRegionsTest, DraggableRegionsCrossOrigin) {
  CefRefPtr<DraggableRegionsTestHandler> handler =
      new DraggableRegionsTestHandler(/*same_origin=*/false);
  handler->ExecuteTest();
  ReleaseAndWaitForDestructor(handler);
}
