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

#include <stddef.h>

#include <algorithm>
#include <map>
#include <vector>

#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/browser_test.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 "content/shell/common/shell_switches.h"
#include "gpu/command_buffer/service/gpu_switches.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image.h"

namespace content {

namespace {

static const char kCanvasPageString[] =
    "<body>"
    "  <canvas id=\"canvas\" width=\"64\" height=\"64\""
    "    style=\"position:absolute;top:0px;left:0px;width:100%;"
    "    height=100%;margin:0;padding:0;\">"
    "  </canvas>"
    "  <script>"
    "    window.ctx = document.getElementById(\"canvas\").getContext(\"2d\");"
    "    function fillWithColor(color) {"
    "      ctx.fillStyle = color;"
    "      ctx.fillRect(0, 0, 64, 64);"
    "      return color;"
    "    }"
    "    var offset = 150;"
    "    function openNewWindow() {"
    "      window.open(\"/test\", \"\", "
    "          \"top=\"+offset+\",left=\"+offset+\",width=200,height=200\");"
    "      offset += 50;"
    "      return true;"
    "    }"
    "    window.document.title = \"Ready\";"
    "  </script>"
    "</body>";
}

class SnapshotBrowserTest : public ContentBrowserTest {
 public:
  SnapshotBrowserTest() {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Use a smaller browser window to speed up the snapshots.
    command_line->AppendSwitchASCII(::switches::kContentShellHostWindowSize,
                                    "200x200");
  }

  void SetUp() override {
    // These tests rely on the harness producing pixel output.
    EnablePixelOutput();
    ContentBrowserTest::SetUp();
  }

  content::WebContentsImpl* GetWebContents(Shell* browser) {
    return static_cast<content::WebContentsImpl*>(browser->web_contents());
  }

  content::RenderWidgetHostImpl* GetRenderWidgetHostImpl(Shell* browser) {
    return GetWebContents(browser)->GetRenderViewHost()->GetWidget();
  }

  void SetupTestServer() {
    // Use an embedded test server so we can open multiple windows in
    // the same renderer process, all referring to the same origin.
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &SnapshotBrowserTest::HandleRequest, base::Unretained(this)));
    ASSERT_TRUE(embedded_test_server()->Start());

    ASSERT_TRUE(
        NavigateToURL(shell(), embedded_test_server()->GetURL("/test")));
  }

  std::unique_ptr<net::test_server::HttpResponse> HandleRequest(
      const net::test_server::HttpRequest& request) {
    GURL absolute_url = embedded_test_server()->GetURL(request.relative_url);
    if (absolute_url.path() != "/test")
      return nullptr;

    std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
        new net::test_server::BasicHttpResponse());
    http_response->set_code(net::HTTP_OK);
    http_response->set_content(kCanvasPageString);
    http_response->set_content_type("text/html");
    return http_response;
  }

  void WaitForAllWindowsToBeReady() {
    const std::u16string expected_title = u"Ready";
    // The subordinate windows may load asynchronously. Wait for all of
    // them to execute their script before proceeding.
    auto browser_list = Shell::windows();
    for (Shell* browser : browser_list) {
      TitleWatcher watcher(GetWebContents(browser), expected_title);
      const std::u16string& actual_title = watcher.WaitAndGetTitle();
      EXPECT_EQ(expected_title, actual_title);
    }
  }

  struct ExpectedColor {
    ExpectedColor() : r(0), g(0), b(0) {}
    bool operator==(const ExpectedColor& other) const {
      return (r == other.r && g == other.g && b == other.b);
    }
    uint8_t r;
    uint8_t g;
    uint8_t b;
  };

  void PickRandomColor(ExpectedColor* expected) {
    expected->r = static_cast<uint8_t>(base::RandInt(0, 256));
    expected->g = static_cast<uint8_t>(base::RandInt(0, 256));
    expected->b = static_cast<uint8_t>(base::RandInt(0, 256));
  }

  struct SerialSnapshot {
    SerialSnapshot() : host(nullptr) {}

    raw_ptr<content::RenderWidgetHost> host;
    ExpectedColor color;
  };
  std::vector<SerialSnapshot> expected_snapshots_;

  void SyncSnapshotCallback(content::RenderWidgetHostImpl* rwhi,
                            const gfx::Image& image) {
    for (auto iter = expected_snapshots_.begin();
         iter != expected_snapshots_.end(); ++iter) {
      const SerialSnapshot& expected = *iter;
      if (expected.host == rwhi) {
        const SkBitmap* bitmap = image.ToSkBitmap();
        SkColor color = bitmap->getColor(1, 1);

        EXPECT_EQ(static_cast<int>(SkColorGetR(color)),
                  static_cast<int>(expected.color.r))
            << "Red channels differed";
        EXPECT_EQ(static_cast<int>(SkColorGetG(color)),
                  static_cast<int>(expected.color.g))
            << "Green channels differed";
        EXPECT_EQ(static_cast<int>(SkColorGetB(color)),
                  static_cast<int>(expected.color.b))
            << "Blue channels differed";

        expected_snapshots_.erase(iter);
        break;
      }
    }
  }

  std::map<content::RenderWidgetHost*, std::vector<ExpectedColor>>
      expected_async_snapshots_map_;
  int num_remaining_async_snapshots_ = 0;

  void AsyncSnapshotCallback(content::RenderWidgetHostImpl* rwhi,
                             const gfx::Image& image) {
    --num_remaining_async_snapshots_;
    auto iterator = expected_async_snapshots_map_.find(rwhi);
    ASSERT_NE(iterator, expected_async_snapshots_map_.end());
    std::vector<ExpectedColor>& expected_snapshots = iterator->second;
    const SkBitmap* bitmap = image.ToSkBitmap();
    SkColor color = bitmap->getColor(1, 1);
    bool found = false;
    // Find first instance of this color in the list and clear out all
    // of the entries before that point. If it's not found, report
    // failure.
    for (auto iter = expected_snapshots.begin();
         iter != expected_snapshots.end(); ++iter) {
      const ExpectedColor& expected = *iter;
      if (SkColorGetR(color) == expected.r &&
          SkColorGetG(color) == expected.g &&
          SkColorGetB(color) == expected.b) {
        // Erase everything up to this color, but not this color
        // itself, since it might be returned again later on
        // subsequent snapshot requests.
        expected_snapshots.erase(expected_snapshots.begin(), iter);
        found = true;
        break;
      }
    }

    EXPECT_TRUE(found) << "Did not find color ("
                       << static_cast<int>(SkColorGetR(color)) << ", "
                       << static_cast<int>(SkColorGetG(color)) << ", "
                       << static_cast<int>(SkColorGetB(color))
                       << ") in expected snapshots for RWH 0x" << rwhi;
  }
};

// Even the single-window test doesn't work on Android yet. It's expected
// that the multi-window tests would never work on that platform.
#if !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_MAC)
// TODO(crbug.com/1347296): This test is flakey on macOS.
#define MAYBE_SingleWindowTest DISABLED_SingleWindowTest
#else
#define MAYBE_SingleWindowTest SingleWindowTest
#endif
IN_PROC_BROWSER_TEST_F(SnapshotBrowserTest, MAYBE_SingleWindowTest) {
  SetupTestServer();

  content::RenderWidgetHostImpl* rwhi = GetRenderWidgetHostImpl(shell());

  for (int i = 0; i < 40; ++i) {
    SerialSnapshot expected;
    expected.host = rwhi;
    PickRandomColor(&expected.color);

    std::string colorString = base::StringPrintf(
        "#%02x%02x%02x", expected.color.r, expected.color.g, expected.color.b);
    std::string script = std::string("fillWithColor(\"") + colorString + "\");";
    EXPECT_EQ(colorString, EvalJs(GetWebContents(shell()), script));

    expected_snapshots_.push_back(expected);

    // Get the snapshot from the surface rather than the window. The
    // on-screen display path is verified by the GPU tests, and it
    // seems difficult to figure out the colorspace transformation
    // required to make these color comparisons.
    rwhi->GetSnapshotFromBrowser(
        base::BindOnce(&SnapshotBrowserTest::SyncSnapshotCallback,
                       base::Unretained(this), base::Unretained(rwhi)),
        true);
    while (expected_snapshots_.size() > 0) {
      base::RunLoop().RunUntilIdle();
    }
  }
}

// Timing out either all the time, or infrequently, apparently because
// they're too slow, on the following configurations:
//   Windows Debug
//   Windows Release (https://crbug.com/1376441)
//   Linux Chromium OS ASAN LSAN Tests (1)
//   Linux TSAN Tests
// See crbug.com/771119
// TODO(https://crbug.com/1317446): Fix and enable on Fuchsia.
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH) ||                       \
    ((BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)) &&                      \
     defined(THREAD_SANITIZER)) ||                                           \
    BUILDFLAG(IS_FUCHSIA)
#define MAYBE_SyncMultiWindowTest DISABLED_SyncMultiWindowTest
#else
#define MAYBE_SyncMultiWindowTest SyncMultiWindowTest
#endif
IN_PROC_BROWSER_TEST_F(SnapshotBrowserTest, MAYBE_SyncMultiWindowTest) {
  SetupTestServer();

  for (int i = 0; i < 3; ++i) {
    EXPECT_EQ(true, EvalJs(GetWebContents(shell()), "openNewWindow()"));
  }

  base::RunLoop().RunUntilIdle();

  WaitForAllWindowsToBeReady();

  auto browser_list = Shell::windows();
  EXPECT_EQ(4u, browser_list.size());

  for (int i = 0; i < 20; ++i) {
    for (int j = 0; j < 4; j++) {
      // Start each iteration by taking a snapshot with a different
      // browser instance.
      int browser_index = (i + j) % 4;
      Shell* browser = browser_list[browser_index];
      content::RenderWidgetHostImpl* rwhi = GetRenderWidgetHostImpl(browser);

      SerialSnapshot expected;
      expected.host = rwhi;
      PickRandomColor(&expected.color);

      std::string colorString =
          base::StringPrintf("#%02x%02x%02x", expected.color.r,
                             expected.color.g, expected.color.b);
      std::string script =
          std::string("fillWithColor(\"") + colorString + "\");";
      EXPECT_EQ(colorString, EvalJs(GetWebContents(browser), script));
      expected_snapshots_.push_back(expected);
      // Get the snapshot from the surface rather than the window. The
      // on-screen display path is verified by the GPU tests, and it
      // seems difficult to figure out the colorspace transformation
      // required to make these color comparisons.
      rwhi->GetSnapshotFromBrowser(
          base::BindOnce(&SnapshotBrowserTest::SyncSnapshotCallback,
                         base::Unretained(this), base::Unretained(rwhi)),
          true);
    }

    while (expected_snapshots_.size() > 0) {
      base::RunLoop().RunUntilIdle();
    }
  }
}

// Timing out either all the time, or infrequently, apparently because
// they're too slow, on the following configurations:
//   Windows Debug
//   Linux Chromium OS ASAN LSAN Tests (1)
//   Linux TSAN Tests
// See crbug.com/771119
// TODO(crbug.com/1164581): recently crashes flakily on
// linux_chromium_asan_rel_ng and linux-rel.
// TODO(https://crbug.com/1317446): Fix and enable on Fuchsia.
#if (BUILDFLAG(IS_WIN) && !defined(NDEBUG)) || BUILDFLAG(IS_CHROMEOS_ASH) || \
    (BUILDFLAG(IS_CHROMEOS) && defined(THREAD_SANITIZER)) ||                 \
    BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_FUCHSIA)
#define MAYBE_AsyncMultiWindowTest DISABLED_AsyncMultiWindowTest
#else
#define MAYBE_AsyncMultiWindowTest AsyncMultiWindowTest
#endif
IN_PROC_BROWSER_TEST_F(SnapshotBrowserTest, MAYBE_AsyncMultiWindowTest) {
  SetupTestServer();

  for (int i = 0; i < 3; ++i) {
    EXPECT_EQ(true, EvalJs(GetWebContents(shell()), "openNewWindow()"));
  }

  base::RunLoop().RunUntilIdle();

  WaitForAllWindowsToBeReady();

  auto browser_list = Shell::windows();
  EXPECT_EQ(4u, browser_list.size());

  // This many pending snapshots per window will be put on the queue
  // before draining the requests. Anything more than 1 seems to catch
  // bugs which might otherwise be introduced in LatencyInfo's
  // propagation of the BROWSER_SNAPSHOT_FRAME_NUMBER_COMPONENT
  // component type.
  int divisor = 3;

  for (int i = 0; i < 10 * divisor; ++i) {
    for (int j = 0; j < 4; j++) {
      // Start each iteration by taking a snapshot with a different
      // browser instance.
      int browser_index = (i + j) % 4;
      Shell* browser = browser_list[browser_index];
      content::RenderWidgetHostImpl* rwhi = GetRenderWidgetHostImpl(browser);

      std::vector<ExpectedColor>& expected_snapshots =
          expected_async_snapshots_map_[rwhi];

      // Pick a unique random color.
      ExpectedColor expected;
      do {
        PickRandomColor(&expected);
      } while (base::Contains(expected_snapshots, expected));
      expected_snapshots.push_back(expected);

      std::string colorString = base::StringPrintf("#%02x%02x%02x", expected.r,
                                                   expected.g, expected.b);
      std::string script =
          std::string("fillWithColor(\"") + colorString + "\");";
      EXPECT_EQ(colorString, EvalJs(GetWebContents(browser), script));
      // Get the snapshot from the surface rather than the window. The
      // on-screen display path is verified by the GPU tests, and it
      // seems difficult to figure out the colorspace transformation
      // required to make these color comparisons.
      rwhi->GetSnapshotFromBrowser(
          base::BindOnce(&SnapshotBrowserTest::AsyncSnapshotCallback,
                         base::Unretained(this), base::Unretained(rwhi)),
          true);
      ++num_remaining_async_snapshots_;
    }

    // Periodically yield and drain the async snapshot requests.
    if ((i % divisor) == 0) {
      bool drained;
      do {
        drained = true;
        for (auto iter = expected_async_snapshots_map_.begin();
             iter != expected_async_snapshots_map_.end(); ++iter) {
          if (iter->second.size() > 1) {
            drained = false;
            break;
          }
        }
        if (!drained) {
          base::RunLoop().RunUntilIdle();
        }
      } while (!drained);
    }
  }

  // At the end of the test, cooperatively wait for all of the snapshot
  // requests to be drained before exiting. This works around crashes
  // seen when tearing down the compositor while these requests are in
  // flight. Likely, user-visible APIs that use this facility are safe
  // in this regard.
  while (num_remaining_async_snapshots_ > 0) {
    base::RunLoop().RunUntilIdle();
  }
}

#endif  // !BUILDFLAG(IS_ANDROID)

}  // namespace content