// Copyright 2014 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/policy/core/common/policy_loader_ios.h"

#import <UIKit/UIKit.h>

#include <memory>

#include "base/apple/scoped_cftyperef.h"
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/json/json_string_value_serializer.h"
#include "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/test_simple_task_runner.h"
#import "base/time/time.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "components/policy/core/common/async_policy_provider.h"
#include "components/policy/core/common/configuration_policy_provider_test.h"
#include "components/policy/core/common/policy_bundle.h"
#import "components/policy/core/common/policy_loader_ios_constants.h"
#include "components/policy/core/common/policy_map.h"
#import "components/policy/core/common/policy_namespace.h"
#include "components/policy/core/common/policy_test_utils.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/core/common/schema.h"
#include "components/policy/core/common/schema_registry.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"

namespace policy {

namespace {

class TestHarness : public PolicyProviderTestHarness {
 public:
  TestHarness(bool encode_complex_data_as_json);
  TestHarness(const TestHarness&) = delete;
  TestHarness& operator=(const TestHarness&) = delete;
  ~TestHarness() override;

  void SetUp() override;

  ConfigurationPolicyProvider* CreateProvider(
      SchemaRegistry* registry,
      scoped_refptr<base::SequencedTaskRunner> task_runner) override;

  void InstallEmptyPolicy() override;
  void InstallStringPolicy(const std::string& policy_name,
                           const std::string& policy_value) override;
  void InstallIntegerPolicy(const std::string& policy_name,
                            int policy_value) override;
  void InstallBooleanPolicy(const std::string& policy_name,
                            bool policy_value) override;
  void InstallStringListPolicy(const std::string& policy_name,
                               const base::ListValue& policy_value) override;
  void InstallDictionaryPolicy(const std::string& policy_name,
                               const base::DictValue& policy_value) override;

  static PolicyProviderTestHarness* Create();
  static PolicyProviderTestHarness* CreateWithJSONEncoding();

 private:
  // Merges the policies in |policy| into the current policy dictionary
  // in NSUserDefaults, after making sure that the policy dictionary
  // exists.
  void AddPolicies(NSDictionary* policy);

  // If true, the test harness will encode complex data (dicts and lists) as
  // JSON strings.
  bool encode_complex_data_as_json_;
};

TestHarness::TestHarness(bool encode_complex_data_as_json)
    : PolicyProviderTestHarness(POLICY_LEVEL_MANDATORY,
                                POLICY_SCOPE_MACHINE,
                                POLICY_SOURCE_PLATFORM),
      encode_complex_data_as_json_(encode_complex_data_as_json) {}

TestHarness::~TestHarness() {
  // Cleanup any policies left from the test.
  [[NSUserDefaults standardUserDefaults]
      removeObjectForKey:kPolicyLoaderIOSConfigurationKey];
}

void TestHarness::SetUp() {
  // Make sure there is no pre-existing policy present.
  [[NSUserDefaults standardUserDefaults]
      removeObjectForKey:kPolicyLoaderIOSConfigurationKey];
}

ConfigurationPolicyProvider* TestHarness::CreateProvider(
    SchemaRegistry* registry,
    scoped_refptr<base::SequencedTaskRunner> task_runner) {
  return new AsyncPolicyProvider(
      registry, std::make_unique<PolicyLoaderIOS>(registry, task_runner));
}

void TestHarness::InstallEmptyPolicy() {
  AddPolicies(@{});
}

void TestHarness::InstallStringPolicy(const std::string& policy_name,
                                      const std::string& policy_value) {
  NSString* key = base::SysUTF8ToNSString(policy_name);
  NSString* value = base::SysUTF8ToNSString(policy_value);
  AddPolicies(@{
      key: value
  });
}

void TestHarness::InstallIntegerPolicy(const std::string& policy_name,
                                       int policy_value) {
  NSString* key = base::SysUTF8ToNSString(policy_name);
  AddPolicies(@{
      key: [NSNumber numberWithInt:policy_value]
  });
}

void TestHarness::InstallBooleanPolicy(const std::string& policy_name,
                                       bool policy_value) {
  NSString* key = base::SysUTF8ToNSString(policy_name);
  AddPolicies(@{
      key: [NSNumber numberWithBool:policy_value]
  });
}

void TestHarness::InstallStringListPolicy(const std::string& policy_name,
                                          const base::ListValue& policy_value) {
  NSString* key = base::SysUTF8ToNSString(policy_name);
  base::apple::ScopedCFTypeRef<CFPropertyListRef> value =
      ValueToProperty(base::Value(policy_value.Clone()));

  if (encode_complex_data_as_json_) {
    // Convert |policy_value| to a JSON-encoded string.
    std::string json_string;
    JSONStringValueSerializer serializer(&json_string);
    ASSERT_TRUE(serializer.Serialize(policy_value));

    AddPolicies(@{key : base::SysUTF8ToNSString(json_string)});
  } else {
    AddPolicies(@{key : (__bridge NSArray*)(value.get())});
  }
}

void TestHarness::InstallDictionaryPolicy(const std::string& policy_name,
                                          const base::DictValue& policy_value) {
  NSString* key = base::SysUTF8ToNSString(policy_name);

  if (encode_complex_data_as_json_) {
    // Convert |policy_value| to a JSON-encoded string.
    std::string json_string;
    JSONStringValueSerializer serializer(&json_string);
    ASSERT_TRUE(serializer.Serialize(policy_value));

    AddPolicies(@{key : base::SysUTF8ToNSString(json_string)});
  } else {
    base::apple::ScopedCFTypeRef<CFPropertyListRef> value =
        ValueToProperty(base::Value(policy_value.Clone()));
    AddPolicies(@{key : (__bridge NSDictionary*)(value.get())});
  }
}

// static
PolicyProviderTestHarness* TestHarness::Create() {
  return new TestHarness(false);
}

// static
PolicyProviderTestHarness* TestHarness::CreateWithJSONEncoding() {
  return new TestHarness(true);
}

void TestHarness::AddPolicies(NSDictionary* policy) {
  NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
  NSMutableDictionary* chromePolicy = [[NSMutableDictionary alloc] init];

  NSDictionary* previous =
      [defaults dictionaryForKey:kPolicyLoaderIOSConfigurationKey];
  if (previous) {
    [chromePolicy addEntriesFromDictionary:previous];
  }

  [chromePolicy addEntriesFromDictionary:policy];
  [[NSUserDefaults standardUserDefaults]
      setObject:chromePolicy
         forKey:kPolicyLoaderIOSConfigurationKey];
}

}  // namespace

INSTANTIATE_TEST_SUITE_P(PolicyProviderIOSChromePolicyTest,
                         ConfigurationPolicyProviderTest,
                         testing::Values(TestHarness::Create));

INSTANTIATE_TEST_SUITE_P(PolicyProviderIOSChromePolicyJSONTest,
                         ConfigurationPolicyProviderTest,
                         testing::Values(TestHarness::CreateWithJSONEncoding));

const char kTestChromeSchema[] =
    "{"
    "  \"type\": \"object\","
    "  \"properties\": {"
    "    \"StringPolicy\": { \"type\": \"string\" },"
    "  }"
    "}";

PolicyNamespace GetPolicyNamespace() {
  return PolicyNamespace(POLICY_DOMAIN_CHROME, "");
}

// Tests cases not covered by the test harnest.
class PolicyLoaderIosTest : public PlatformTest {
 protected:
  PolicyLoaderIosTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

  void SetUp() override {
    auto result = RegisterSchema(GetPolicyNamespace(), kTestChromeSchema);
    ASSERT_TRUE(result.has_value())
        << "Registration of schema failed with error: " << result.error();
  }

  void TearDown() override {
    task_environment_.RunUntilIdle();
    provider_->Shutdown();
    // Clear App Config.
    [[NSUserDefaults standardUserDefaults]
        removeObjectForKey:kPolicyLoaderIOSConfigurationKey];
  }

  void InitProvider() {
    std::unique_ptr<PolicyLoaderIOS> loader = std::make_unique<PolicyLoaderIOS>(
        &schema_registry_, task_environment_.GetMainThreadTaskRunner());
    provider_ = std::make_unique<AsyncPolicyProvider>(&schema_registry_,
                                                      std::move(loader));
    provider_->Init(&schema_registry_);
    task_environment_.RunUntilIdle();
  }

  base::test::TaskEnvironment task_environment_;
  SchemaRegistry schema_registry_;
  std::unique_ptr<AsyncPolicyProvider> provider_;

 private:
  base::expected<void, std::string> RegisterSchema(const PolicyNamespace& ns,
                                                   const std::string& schema) {
    ASSIGN_OR_RETURN(const auto parsed_schema, Schema::Parse(schema));
    schema_registry_.RegisterComponent(ns, parsed_schema);
    return base::ok();
  }
};

TEST_F(PolicyLoaderIosTest, ReloadIntervalWhenBrowserManagedPostStartup) {
  InitProvider();

  // Verify that there are no policies loaded at startup.
  EXPECT_TRUE(provider_->policies().Equals(PolicyBundle()));

  // Set a policy in the App Config at runtime to replicate the situation where
  // the browser is put under management after startup.
  NSDictionary* policies = @{@"StringPolicy" : @"string_value"};
  [[NSUserDefaults standardUserDefaults]
      setObject:policies
         forKey:kPolicyLoaderIOSConfigurationKey];

  // Verify that the new policy changes aren't picked up after 10 minutes
  // when the browser wasn't managed at startup. This is to make sure that the
  // first reload was scheduled with an interval of 15 minutes.
  task_environment_.FastForwardBy(base::Minutes(10));
  EXPECT_TRUE(provider_->policies().Equals(PolicyBundle()));

  // Verify that the new policy changes are picked up after 15 minutes +
  // epsilon.
  task_environment_.FastForwardBy(base::Minutes(5) + base::Seconds(1));
  ASSERT_TRUE(task_environment_.MainThreadIsIdle());
  PolicyBundle bundle;
  bundle.Get(GetPolicyNamespace())
      .Set("StringPolicy", POLICY_LEVEL_MANDATORY, POLICY_SCOPE_MACHINE,
           POLICY_SOURCE_PLATFORM, base::Value("string_value"), nullptr);
  EXPECT_TRUE(provider_->policies().Equals(bundle));

  // Simulate the situation where the App Config is updated with new values
  // before the next reload.
  NSDictionary* new_policies = @{@"StringPolicy" : @"string_value_new"};
  [[NSUserDefaults standardUserDefaults]
      setObject:new_policies
         forKey:kPolicyLoaderIOSConfigurationKey];

  // Verify that the interval was adjusted to 30 seconds after first reload.
  task_environment_.FastForwardBy(base::Seconds(31));
  ASSERT_TRUE(task_environment_.MainThreadIsIdle());
  PolicyBundle new_bundle;
  new_bundle.Get(GetPolicyNamespace())
      .Set("StringPolicy", POLICY_LEVEL_MANDATORY, POLICY_SCOPE_MACHINE,
           POLICY_SOURCE_PLATFORM, base::Value("string_value_new"), nullptr);
  EXPECT_TRUE(provider_->policies().Equals(new_bundle));
}

TEST_F(PolicyLoaderIosTest, ReloadIntervalWhenManagedByPlatformBeforeStartup) {
  // Set a policy in the App Config to replicate the situation where the browser
  // is put under management before startup.
  NSDictionary* policies = @{@"StringPolicy" : @"string_value"};
  [[NSUserDefaults standardUserDefaults]
      setObject:policies
         forKey:kPolicyLoaderIOSConfigurationKey];

  InitProvider();

  // Verify that the new policy changes are picked up after 30 seconds +
  // epsilon.
  task_environment_.FastForwardBy(base::Seconds(31));
  ASSERT_TRUE(task_environment_.MainThreadIsIdle());
  PolicyBundle bundle;
  bundle.Get(GetPolicyNamespace())
      .Set("StringPolicy", POLICY_LEVEL_MANDATORY, POLICY_SCOPE_MACHINE,
           POLICY_SOURCE_PLATFORM, base::Value("string_value"), nullptr);
  EXPECT_TRUE(provider_->policies().Equals(bundle));
}

}  // namespace policy
