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

#include <array>
#include <fstream>
#include <memory>

#include "base/base_paths.h"
#include "base/containers/span.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "content/browser/webui/url_data_source_impl.h"
#include "content/browser/webui/web_ui_impl.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/browser/web_ui_controller_interface_binder.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/url_constants.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_content_browser_client.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_ui_browsertest_util.h"
#include "content/shell/browser/shell.h"
#include "content/test/grit/web_ui_mojo_test_resources.h"
#include "content/test/grit/web_ui_mojo_test_resources_map.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/tests/validation_test_input_parser.h"
#include "mojo/public/interfaces/bindings/tests/validation_test_interfaces.test-mojom.h"
#include "ui/base/webui/resource_path.h"

namespace content {
namespace {

std::vector<base::FilePath> EnumerateFilesInDirectory(
    const base::FilePath& dir,
    const base::FilePath::StringType& pattern) {
  std::vector<base::FilePath> test_files;

  base::FileEnumerator e(dir, false, base::FileEnumerator::FILES, pattern);
  e.ForEach([&test_files](const base::FilePath& file_path) -> void {
    test_files.push_back(file_path);
  });

  return test_files;
}

// Reads the content of a file to a string. Crashes if reading failed.
std::string ReadFile(const base::FilePath& file_path) {
  std::string content;
  CHECK(base::ReadFileToString(file_path, &content));
  return std::string(
      base::TrimWhitespaceASCII(content, base::TrimPositions::TRIM_TRAILING));
}

std::vector<mojo::test::TestCasePtr> ParseTestCases() {
  base::FilePath base_dir;
  base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &base_dir);
  const auto test_dir_relative_path = base::FilePath::FromASCII(
      "mojo/public/interfaces/bindings/tests/data/"
      "validation/");
  base::FilePath test_cases_dir = base_dir.Append(test_dir_relative_path);
  std::vector<base::FilePath> test_cases = EnumerateFilesInDirectory(
      test_cases_dir, FILE_PATH_LITERAL("conformance_*.data"));

  std::vector<mojo::test::TestCasePtr> parsed_test_cases;
  for (const base::FilePath& test_file : test_cases) {
    std::string test_data_raw = ReadFile(test_file);

    std::vector<uint8_t> bytes;
    size_t num_handles;
    std::string error_msg;
    CHECK(mojo::test::ParseValidationTestInput(test_data_raw, &bytes,
                                               &num_handles, &error_msg));

    std::string expectation = ReadFile(
        test_file.RemoveFinalExtension().AddExtensionASCII("expected"));

    parsed_test_cases.push_back(mojo::test::TestCase::New(
        test_file.BaseName().RemoveFinalExtension().MaybeAsASCII(), bytes,
        expectation));
  }
  CHECK(parsed_test_cases.size() > 0)
      << "No test cases were found, there might be an issue with pathing, "
         "looked at: "
      << test_cases_dir;

  return parsed_test_cases;
}

// WebUIController that sets up mojo bindings.
class ConformanceTestWebUIController : public WebUIController,
                                       public mojo::test::PageHandlerFactory {
 public:
  explicit ConformanceTestWebUIController(WebUI* web_ui)
      : WebUIController(web_ui) {
    web_ui->SetBindings(
        {BindingsPolicyValue::kMojoWebUi, BindingsPolicyValue::kWebUi});
    WebUIDataSource* data_source = WebUIDataSource::CreateAndAdd(
        web_ui->GetWebContents()->GetBrowserContext(),
        "mojo-web-ui-conformance");
    data_source->OverrideContentSecurityPolicy(
        network::mojom::CSPDirectiveName::ScriptSrc,
        "script-src chrome://resources 'self';");
    data_source->AddResourcePaths(kWebUiMojoTestResources);
    data_source->SetDefaultResource(IDR_WEB_UI_TS_TEST_CONFORMANCE_HTML);
  }

  ConformanceTestWebUIController(const ConformanceTestWebUIController&) =
      delete;
  ConformanceTestWebUIController& operator=(
      const ConformanceTestWebUIController&) = delete;

  void BindInterface(
      mojo::PendingReceiver<mojo::test::PageHandlerFactory> receiver) {
    page_factory_receiver_.reset();
    page_factory_receiver_.Bind(std::move(receiver));
  }

 private:
  void GetTestCases(GetTestCasesCallback cb) override {
    base::ThreadPool::PostTask(
        FROM_HERE, {base::MayBlock()},
        base::BindOnce(
            [](GetTestCasesCallback cb,
               const scoped_refptr<base::SequencedTaskRunner>& runner) {
              auto test_cases = ParseTestCases();
              runner->PostTask(
                  FROM_HERE,
                  base::BindOnce(std::move(cb), std::move(test_cases)));
            },
            std::move(cb), base::SequencedTaskRunner::GetCurrentDefault()));
  }

  mojo::Receiver<mojo::test::PageHandlerFactory> page_factory_receiver_{this};

  WEB_UI_CONTROLLER_TYPE_DECL();
};

WEB_UI_CONTROLLER_TYPE_IMPL(ConformanceTestWebUIController)

class ConformanceTestWebUIControllerFactory : public WebUIControllerFactory {
 public:
  ConformanceTestWebUIControllerFactory() = default;
  ConformanceTestWebUIControllerFactory(const TestWebUIControllerFactory&) =
      delete;
  ConformanceTestWebUIControllerFactory& operator=(
      const TestWebUIControllerFactory&) = delete;

  std::unique_ptr<WebUIController> CreateWebUIControllerForURL(
      WebUI* web_ui,
      const GURL& url) override {
    return std::make_unique<ConformanceTestWebUIController>(web_ui);
  }

  WebUI::TypeID GetWebUIType(BrowserContext* browser_context,
                             const GURL& url) override {
    if (!url.SchemeIs(kChromeUIScheme)) {
      return WebUI::kNoWebUI;
    }
    return reinterpret_cast<WebUI::TypeID>(1);
  }

  bool UseWebUIForURL(BrowserContext* browser_context,
                      const GURL& url) override {
    return true;
  }
};

class ConformanceTestWebUIClient
    : public ContentBrowserTestContentBrowserClient {
 public:
  ConformanceTestWebUIClient() = default;
  ConformanceTestWebUIClient(const ConformanceTestWebUIClient&) = delete;
  ConformanceTestWebUIClient& operator=(const ConformanceTestWebUIClient&) =
      delete;

  void RegisterBrowserInterfaceBindersForFrame(
      RenderFrameHost* render_frame_host,
      mojo::BinderMapWithContext<content::RenderFrameHost*>* map) override {
    RegisterWebUIControllerInterfaceBinder<mojo::test::PageHandlerFactory,
                                           ConformanceTestWebUIController>(map);
  }
};

class WebUIMojoConformanceTest : public ContentBrowserTest {
 public:
  WebUIMojoConformanceTest() = default;
  WebUIMojoConformanceTest(const WebUIMojoConformanceTest&) = delete;
  WebUIMojoConformanceTest& operator=(const WebUIMojoConformanceTest&) = delete;

 protected:
  void SetUpOnMainThread() override {
    client_ = std::make_unique<ConformanceTestWebUIClient>();
  }
  void TearDownOnMainThread() override {}

  ConformanceTestWebUIControllerFactory factory_;
  ScopedWebUIControllerFactoryRegistration factory_registration_{&factory_};
  std::unique_ptr<ConformanceTestWebUIClient> client_;
};

IN_PROC_BROWSER_TEST_F(WebUIMojoConformanceTest, ConformanceTest) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL("data:,foo")));

  GURL kTestUrl(GetWebUIURL("mojo-web-ui-conformance/"));
  const std::string kTestScript = "runTest();";
  EXPECT_TRUE(NavigateToURL(shell(), kTestUrl));
  EXPECT_EQ(true, EvalJs(shell()->web_contents(), kTestScript));
}

}  // namespace
}  // namespace content
