// Copyright 2022 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/isolated_web_apps/reading/validator.h"

#include <memory>
#include <optional>
#include <string>
#include <tuple>

#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "components/web_package/mojom/web_bundle_parser.mojom.h"
#include "components/web_package/signed_web_bundles/ed25519_public_key.h"
#include "components/web_package/signed_web_bundles/ed25519_signature.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_integrity_block.h"
#include "components/web_package/test_support/signed_web_bundles/signature_verifier_test_utils.h"
#include "components/webapps/isolated_web_apps/error/unusable_swbn_file_error.h"
#include "components/webapps/isolated_web_apps/identity/iwa_identity_validator.h"
#include "components/webapps/isolated_web_apps/test_support/signed_web_bundle_utils.h"
#include "components/webapps/isolated_web_apps/test_support/signing_keys.h"
#include "components/webapps/isolated_web_apps/test_support/test_iwa_client.h"
#include "components/webapps/isolated_web_apps/types/iwa_origin.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "url/gurl.h"

namespace web_app {

namespace {

using base::test::ErrorIs;
using base::test::HasValue;
using testing::_;
using testing::AllOf;
using testing::Eq;
using testing::HasSubstr;
using testing::Property;
using testing::Return;

using Error = UnusableSwbnFileError::Error;

auto UnusableSwbnErrorIs(Error type, const std::string& error) {
  return ErrorIs(
      AllOf(Property(&UnusableSwbnFileError::value, type),
            Property(&UnusableSwbnFileError::message, HasSubstr(error))));
}

}  // namespace

class IsolatedWebAppValidatorTest : public ::testing::Test {
 protected:
  IsolatedWebAppValidatorTest() { IwaIdentityValidator::CreateSingleton(); }

  using IntegrityBlockFuture =
      base::test::TestFuture<base::expected<void, std::string>>;

  web_package::SignedWebBundleIntegrityBlock MakeIntegrityBlock(
      const std::vector<web_package::Ed25519PublicKey>& public_keys = {
          test::GetDefaultEd25519KeyPair().public_key}) {
    auto raw_integrity_block = web_package::mojom::BundleIntegrityBlock::New();
    raw_integrity_block->size = 123;
    raw_integrity_block
        ->signature_stack = base::ToVector(public_keys, [](const auto&
                                                               public_key) {
      auto entry =
          web_package::mojom::BundleIntegrityBlockSignatureStackEntry::New();
      entry->signature_info = web_package::mojom::SignatureInfo::NewEd25519(
          web_package::mojom::SignatureInfoEd25519::New());
      entry->signature_info->get_ed25519()->public_key = public_key;
      return entry;
    });

    auto signed_web_bundle_id =
        web_package::SignedWebBundleId::CreateForPublicKey(public_keys[0]);
    raw_integrity_block->attributes =
        web_package::test::GetAttributesForSignedWebBundleId(
            signed_web_bundle_id.id());
    auto integrity_block = web_package::SignedWebBundleIntegrityBlock::Create(
        std::move(raw_integrity_block));
    CHECK(integrity_block.has_value()) << integrity_block.error();

    return *integrity_block;
  }

  content::BrowserTaskEnvironment task_environment_;
  content::TestBrowserContext browser_context_;
  test::TestIwaClient iwa_client_;
};

using IsolatedWebAppValidatorIntegrityBlockTest = IsolatedWebAppValidatorTest;

TEST_F(IsolatedWebAppValidatorIntegrityBlockTest,
       WebBundleIdAndPublicKeyDiffer) {
  auto integrity_block = MakeIntegrityBlock();

  EXPECT_THAT(IsolatedWebAppValidator::ValidateIntegrityBlock(
                  &browser_context_, test::GetDefaultEcdsaP256WebBundleId(),
                  integrity_block, /*allow_soft_key_rotation=*/false),
              UnusableSwbnErrorIs(Error::kIntegrityBlockValidationError,
                                  "does not match the expected Web Bundle ID"));
}

struct RelativeURL {
  web_package::SignedWebBundleId web_bundle_id;
  std::optional<std::string> relative_url;
};

struct FullURL {
  std::string full_url;
};

using WebBundleEntry = std::variant<RelativeURL, FullURL>;

class IsolatedWebAppValidatorMetadataTest
    : public IsolatedWebAppValidatorTest,
      public ::testing::WithParamInterface<
          std::tuple<std::optional<web_package::SignedWebBundleId>,
                     std::vector<WebBundleEntry>,
                     base::expected<void, UnusableSwbnFileError>>> {
 public:
  IsolatedWebAppValidatorMetadataTest()
      : primary_url_(std::get<0>(GetParam())),
        status_(std::get<2>(GetParam())) {
    for (const WebBundleEntry& entry : std::get<1>(GetParam())) {
      entries_.emplace_back(std::visit(
          absl::Overload{
              [](const FullURL& entry) { return entry.full_url; },
              [](const RelativeURL& entry) {
                auto base_url =
                    IwaOrigin(entry.web_bundle_id).origin().GetURL();
                if (entry.relative_url) {
                  return base_url.Resolve(*entry.relative_url).spec();
                }
                return base_url.spec();
              }},
          entry));
    }
  }

 protected:
  std::optional<web_package::SignedWebBundleId> primary_url_;
  std::vector<GURL> entries_;
  base::expected<void, UnusableSwbnFileError> status_;
};

TEST_P(IsolatedWebAppValidatorMetadataTest, Validate) {
  EXPECT_EQ(IsolatedWebAppValidator::ValidateMetadata(
                test::GetDefaultEd25519WebBundleId(),
                [&]() -> std::optional<GURL> {
                  if (!primary_url_) {
                    return std::nullopt;
                  }
                  return IwaOrigin(*primary_url_).origin().GetURL();
                }(),
                entries_),
            status_);
}

INSTANTIATE_TEST_SUITE_P(
    All,
    IsolatedWebAppValidatorMetadataTest,
    ::testing::Values(
        std::make_tuple(
            std::nullopt,
            std::vector<WebBundleEntry>({RelativeURL{
                .web_bundle_id = test::GetDefaultEd25519WebBundleId()}}),
            base::ok()),
        std::make_tuple(
            std::nullopt,
            std::vector<WebBundleEntry>(
                {RelativeURL{
                     .web_bundle_id = test::GetDefaultEd25519WebBundleId()},
                 RelativeURL{
                     .web_bundle_id = test::GetDefaultEd25519WebBundleId(),
                     .relative_url = "/foo#bar"}}),
            base::unexpected(UnusableSwbnFileError(
                UnusableSwbnFileError::Error::kMetadataValidationError,
                "The URL of an exchange is invalid: URLs must not have "
                "a fragment part."))),
        std::make_tuple(
            std::nullopt,
            std::vector<WebBundleEntry>(
                {RelativeURL{
                     .web_bundle_id = test::GetDefaultEd25519WebBundleId()},
                 RelativeURL{
                     .web_bundle_id = test::GetDefaultEd25519WebBundleId(),
                     .relative_url = "/foo?bar"}}),
            base::unexpected(UnusableSwbnFileError(
                UnusableSwbnFileError::Error::kMetadataValidationError,
                "The URL of an exchange is invalid: URLs must not have "
                "a query part."))),
        std::make_tuple(
            test::GetDefaultEd25519WebBundleId(),
            std::vector<WebBundleEntry>({RelativeURL{
                .web_bundle_id = test::GetDefaultEd25519WebBundleId()}}),
            base::unexpected(UnusableSwbnFileError(
                UnusableSwbnFileError::Error::kMetadataValidationError,
                "Primary URL must not be present, but was isolated-app://"
                "4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/"))),
        std::make_tuple(
            std::nullopt,
            std::vector<WebBundleEntry>(
                {RelativeURL{
                     .web_bundle_id = test::GetDefaultEd25519WebBundleId()},
                 FullURL{.full_url = "https://foo/"}}),
            base::unexpected(UnusableSwbnFileError(
                UnusableSwbnFileError::Error::kMetadataValidationError,
                "The URL of an exchange is invalid: The URL scheme must be "
                "isolated-app, but was https"))),
        std::make_tuple(
            std::nullopt,
            std::vector<WebBundleEntry>(
                {RelativeURL{
                     .web_bundle_id = test::GetDefaultEd25519WebBundleId()},
                 RelativeURL{
                     .web_bundle_id = test::GetDefaultEcdsaP256WebBundleId()}}),
            base::unexpected(UnusableSwbnFileError(
                UnusableSwbnFileError::Error::kMetadataValidationError,
                "The URL of an exchange contains the wrong Signed Web "
                "Bundle "
                "ID: "
                "amfcf7c4bmpbjbmq4h4yptcobves56hfdyr7tm3doxqvfmsk5ss6maaca"
                "i")))));

}  // namespace web_app
