// Copyright 2023 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/feature_engagement/internal/blocked_iph_features.h"

#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/strings/string_util.h"
#include "base/synchronization/lock.h"
#include "components/feature_engagement/test/scoped_iph_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace feature_engagement {

namespace {

BASE_FEATURE(kBlockedIphFeaturesTestFeature1,
             base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kBlockedIphFeaturesTestFeature2,
             base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE(kBlockedIphFeaturesTestFeature3,
             base::FEATURE_DISABLED_BY_DEFAULT);
}  // namespace

class BlockedIphFeaturesTest : public testing::Test {
 public:
  void SetUp() override { ClearData(); }

  void TearDown() override {
    {
      auto* const allowed = BlockedIphFeatures::GetInstance();
      base::AutoLock lock(allowed->GetLock());
      EXPECT_EQ(expect_refcount_on_teardown_,
                allowed->global_block_count_ != 0);
    }

    // Clear out the map data so it doesn't pollute later tests.
    ClearData();
  }

  void ExpectRefCountOnTeardown() { expect_refcount_on_teardown_ = true; }

  static const char* GetCommandLineSwitch() {
    return BlockedIphFeatures::kPropagateIPHForTestingSwitch;
  }

 private:
  void ClearData() {
    auto* const allowed = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(allowed->GetLock());
    allowed->read_from_command_line_ = false;
    allowed->global_block_count_ = 0U;
    allowed->allow_feature_counts_.clear();
  }

  bool expect_refcount_on_teardown_ = false;
};

TEST_F(BlockedIphFeaturesTest, IsFeatureBlockedDefaultValue) {
  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());

  // Allowed should be irrespective of enabled or disabled.
  EXPECT_FALSE(blocked->AreAnyFeaturesBlocked());
  EXPECT_FALSE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
  EXPECT_FALSE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
}

TEST_F(BlockedIphFeaturesTest, IsFeatureBlockedWithEmptyScope) {
  {
    test::ScopedIphFeatureList list;
    list.InitWithNoFeaturesAllowed();

    // Blocked should be irrespective of enabled or disabled.
    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());
    EXPECT_TRUE(blocked->AreAnyFeaturesBlocked());
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }

  {
    // Now no one is blocking IPH.
    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());
    EXPECT_FALSE(blocked->AreAnyFeaturesBlocked());
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }
}

TEST_F(BlockedIphFeaturesTest, IsFeatureBlockedInnerScopeHasException) {
  test::ScopedIphFeatureList list1;
  list1.InitWithNoFeaturesAllowed();
  {
    test::ScopedIphFeatureList list2;
    list2.InitAndEnableFeatures({kBlockedIphFeaturesTestFeature1});

    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());

    // This feature is explicitly allowed.
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }

  {
    // Now no features are allowed.
    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }
}

TEST_F(BlockedIphFeaturesTest, IsFeatureBlockedOuterScopeHasException) {
  test::ScopedIphFeatureList list1;
  list1.InitAndEnableFeatures({kBlockedIphFeaturesTestFeature1});
  {
    test::ScopedIphFeatureList list2;
    list2.InitWithNoFeaturesAllowed();

    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());

    // This feature is explicitly allowed.
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }

  {
    // Allowed feature should persist.
    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }
}

TEST_F(BlockedIphFeaturesTest, IsFeatureBlockedMultipleExceptions) {
  test::ScopedIphFeatureList list1;
  list1.InitAndEnableFeatures({kBlockedIphFeaturesTestFeature1});
  {
    test::ScopedIphFeatureList list2;
    list2.InitAndEnableFeatures({kBlockedIphFeaturesTestFeature2});

    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());

    // Both features are allowed.
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }

  {
    // Outer allowed feature should persist.
    auto* const blocked = BlockedIphFeatures::GetInstance();
    base::AutoLock lock(blocked->GetLock());
    EXPECT_FALSE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
    EXPECT_TRUE(
        blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
  }
}
TEST_F(BlockedIphFeaturesTest, MaybeWriteToCommandLineWithNoBlocking) {
  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  blocked->MaybeWriteToCommandLine(command_line);
  EXPECT_FALSE(command_line.HasSwitch(GetCommandLineSwitch()));
  EXPECT_FALSE(command_line.HasSwitch(switches::kEnableFeatures));
}

TEST_F(BlockedIphFeaturesTest, MaybeWriteToCommandLineBlockingAllIph) {
  test::ScopedIphFeatureList list;
  list.InitWithNoFeaturesAllowed();

  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  blocked->MaybeWriteToCommandLine(command_line);
  EXPECT_TRUE(command_line.HasSwitch(GetCommandLineSwitch()));
  EXPECT_TRUE(command_line.GetSwitchValueASCII(GetCommandLineSwitch()).empty());
  EXPECT_FALSE(command_line.HasSwitch(switches::kEnableFeatures));
}

TEST_F(BlockedIphFeaturesTest, MaybeWriteToCommandLineBlockingAllButOneIph) {
  test::ScopedIphFeatureList list;
  list.InitAndEnableFeatures({kBlockedIphFeaturesTestFeature1});

  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  blocked->MaybeWriteToCommandLine(command_line);
  EXPECT_TRUE(command_line.HasSwitch(GetCommandLineSwitch()));
  EXPECT_EQ(kBlockedIphFeaturesTestFeature1.name,
            command_line.GetSwitchValueASCII(GetCommandLineSwitch()));
  EXPECT_TRUE(command_line.HasSwitch(switches::kEnableFeatures));
  EXPECT_EQ(kBlockedIphFeaturesTestFeature1.name,
            command_line.GetSwitchValueASCII(switches::kEnableFeatures));
}

TEST_F(BlockedIphFeaturesTest, MaybeWriteToCommandLineBlockingAllButTwoIph) {
  test::ScopedIphFeatureList list;
  list.InitAndEnableFeatures(
      {kBlockedIphFeaturesTestFeature1, kBlockedIphFeaturesTestFeature2});

  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
  blocked->MaybeWriteToCommandLine(command_line);
  const std::string expected =
      base::JoinString({kBlockedIphFeaturesTestFeature1.name,
                        kBlockedIphFeaturesTestFeature2.name},
                       ",");
  EXPECT_TRUE(command_line.HasSwitch(GetCommandLineSwitch()));
  EXPECT_EQ(expected, command_line.GetSwitchValueASCII(GetCommandLineSwitch()));
  EXPECT_TRUE(command_line.HasSwitch(switches::kEnableFeatures));
  EXPECT_EQ(expected,
            command_line.GetSwitchValueASCII(switches::kEnableFeatures));
}

TEST_F(BlockedIphFeaturesTest,
       MaybeWriteToCommandLinePreviouslyHadEnabledFeatures) {
  test::ScopedIphFeatureList list;
  list.InitAndEnableFeatures({kBlockedIphFeaturesTestFeature2});

  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  base::CommandLine command_line(base::CommandLine::NO_PROGRAM);

  // Add feature 1 to the general enabled list.
  command_line.AppendSwitchASCII(switches::kEnableFeatures,
                                 kBlockedIphFeaturesTestFeature1.name);
  blocked->MaybeWriteToCommandLine(command_line);

  // Only the explicitly allowed IPH should be present in the BlockedIphFeatures
  // command-line switch.
  EXPECT_TRUE(command_line.HasSwitch(GetCommandLineSwitch()));
  EXPECT_EQ(kBlockedIphFeaturesTestFeature2.name,
            command_line.GetSwitchValueASCII(GetCommandLineSwitch()));

  // Both values should be in the final enabled features argument.
  EXPECT_TRUE(command_line.HasSwitch(switches::kEnableFeatures));
  const std::string expected =
      base::JoinString({kBlockedIphFeaturesTestFeature1.name,
                        kBlockedIphFeaturesTestFeature2.name},
                       ",");
  EXPECT_EQ(expected,
            command_line.GetSwitchValueASCII(switches::kEnableFeatures));
}

TEST_F(BlockedIphFeaturesTest, ReadFromCommandLineBlockAll) {
  ExpectRefCountOnTeardown();

  // Append a switch that indicates blocked IPH with no exceptions.
  base::CommandLine::ForCurrentProcess()->AppendSwitch(GetCommandLineSwitch());

  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  EXPECT_TRUE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
  EXPECT_TRUE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
}

TEST_F(BlockedIphFeaturesTest, ReadFromCommandLineAllowOneIph) {
  ExpectRefCountOnTeardown();

  // Append a switch that indicates blocked IPH with one exceptions.
  base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
      GetCommandLineSwitch(), kBlockedIphFeaturesTestFeature1.name);

  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  EXPECT_FALSE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
  EXPECT_TRUE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));
}

TEST_F(BlockedIphFeaturesTest, ReadFromCommandLineAllowTwoIph) {
  ExpectRefCountOnTeardown();

  // Append a switch that indicates blocked IPH with two exceptions.
  base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
      GetCommandLineSwitch(),
      base::JoinString({kBlockedIphFeaturesTestFeature1.name,
                        kBlockedIphFeaturesTestFeature2.name},
                       ","));

  auto* const blocked = BlockedIphFeatures::GetInstance();
  base::AutoLock lock(blocked->GetLock());
  EXPECT_FALSE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature1.name));
  EXPECT_FALSE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature2.name));

  // An unrelated IPH should still be blocked.
  EXPECT_TRUE(blocked->IsFeatureBlocked(kBlockedIphFeaturesTestFeature3.name));
}

}  // namespace feature_engagement
