// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/chromeos/proxy_config_service_impl.h"

#include <map>
#include <string>
#include <vector>

#include "base/format_macros.h"
#include "base/logging.h"
#include "base/message_loop.h"
#include "base/string_util.h"
#include "base/stringprintf.h"
#include "chrome/browser/chromeos/cros/cros_library.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_pref_service.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "content/public/test/test_browser_thread.h"
#include "net/proxy/proxy_config_service_common_unittest.h"
#include "testing/gtest/include/gtest/gtest.h"

using content::BrowserThread;

namespace chromeos {

namespace {

struct Input {  // Fields of chromeos::ProxyConfigServiceImpl::ProxyConfig.
  ProxyConfigServiceImpl::ProxyConfig::Mode mode;
  const char* pac_url;
  const char* single_uri;
  const char* http_uri;
  const char* https_uri;
  const char* ftp_uri;
  const char* socks_uri;
  const char* bypass_rules;
};

// Builds an identifier for each test in an array.
#define TEST_DESC(desc) base::StringPrintf("at line %d <%s>", __LINE__, desc)

// Shortcuts to declare enums within chromeos's ProxyConfig.
#define MK_MODE(mode) ProxyConfigServiceImpl::ProxyConfig::MODE_##mode
#define MK_SCHM(scheme) net::ProxyServer::SCHEME_##scheme
#define MK_AVAIL(avail) net::ProxyConfigService::CONFIG_##avail

// Inspired from net/proxy/proxy_config_service_linux_unittest.cc.
const struct TestParams {
  // Short description to identify the test
  std::string description;

  bool is_valid;

  Input input;

  // Expected outputs from fields of net::ProxyConfig (via IO).
  bool auto_detect;
  GURL pac_url;
  net::ProxyRulesExpectation proxy_rules;
} tests[] = {
  {  // 0
    TEST_DESC("No proxying"),

    true,  // is_valid

    { // Input.
      MK_MODE(DIRECT),  // mode
    },

    // Expected result.
    false,                                   // auto_detect
    GURL(),                                  // pac_url
    net::ProxyRulesExpectation::Empty(),     // proxy_rules
  },

  {  // 1
    TEST_DESC("Auto detect"),

    true,  // is_valid

    { // Input.
      MK_MODE(AUTO_DETECT),  // mode
    },

    // Expected result.
    true,                                    // auto_detect
    GURL(),                                  // pac_url
    net::ProxyRulesExpectation::Empty(),     // proxy_rules
  },

  {  // 2
    TEST_DESC("Valid PAC URL"),

    true,  // is_valid

    { // Input.
      MK_MODE(PAC_SCRIPT),     // mode
      "http://wpad/wpad.dat",  // pac_url
    },

    // Expected result.
    false,                                   // auto_detect
    GURL("http://wpad/wpad.dat"),            // pac_url
    net::ProxyRulesExpectation::Empty(),     // proxy_rules
  },

  {  // 3
    TEST_DESC("Invalid PAC URL"),

    false,  // is_valid

    { // Input.
      MK_MODE(PAC_SCRIPT),  // mode
      "wpad.dat",           // pac_url
    },

    // Expected result.
    false,                                   // auto_detect
    GURL(),                                  // pac_url
    net::ProxyRulesExpectation::Empty(),     // proxy_rules
  },

  {  // 4
    TEST_DESC("Single-host in proxy list"),

    true,  // is_valid

    { // Input.
      MK_MODE(SINGLE_PROXY),  // mode
      NULL,                   // pac_url
      "www.google.com",       // single_uri
    },

    // Expected result.
    false,                                   // auto_detect
    GURL(),                                  // pac_url
    net::ProxyRulesExpectation::Single(      // proxy_rules
        "www.google.com:80",                 // single proxy
        "<local>"),                          // bypass rules
  },

  {  // 5
    TEST_DESC("Single-host, different port"),

    true,   // is_valid

    { // Input.
      MK_MODE(SINGLE_PROXY),  // mode
      NULL,                   // pac_url
      "www.google.com:99",    // single_uri
    },

    // Expected result.
    false,                                   // auto_detect
    GURL(),                                  // pac_url
    net::ProxyRulesExpectation::Single(      // proxy_rules
        "www.google.com:99",                 // single
        "<local>"),                          // bypass rules
  },

  {  // 6
    TEST_DESC("Tolerate a scheme"),

    true,   // is_valid

    { // Input.
      MK_MODE(SINGLE_PROXY),       // mode
      NULL,                        // pac_url
      "http://www.google.com:99",  // single_uri
    },

    // Expected result.
    false,                                   // auto_detect
    GURL(),                                  // pac_url
    net::ProxyRulesExpectation::Single(      // proxy_rules
        "www.google.com:99",                 // single proxy
        "<local>"),                          // bypass rules
  },

  {  // 7
    TEST_DESC("Per-scheme proxy rules"),

    true,  // is_valid

    { // Input.
      MK_MODE(PROXY_PER_SCHEME),  // mode
      NULL,                       // pac_url
      NULL,                       // single_uri
      "www.google.com:80",        // http_uri
      "www.foo.com:110",          // https_uri
      "ftp.foo.com:121",          // ftp_uri
      "socks.com:888",            // socks_uri
    },

    // Expected result.
    false,                          // auto_detect
    GURL(),                         // pac_url
    net::ProxyRulesExpectation::PerSchemeWithSocks(  // proxy_rules
        "www.google.com:80",        // http
        "https://www.foo.com:110",  // https
        "ftp.foo.com:121",          // ftp
        "socks5://socks.com:888",   // fallback proxy
        "<local>"),                 // bypass rules
  },

  {  // 8
    TEST_DESC("Bypass rules"),

    true,  // is_valid

    { // Input.
      MK_MODE(SINGLE_PROXY),      // mode
      NULL,                       // pac_url
      "www.google.com",           // single_uri
      NULL, NULL, NULL, NULL,     // per-proto
      "*.google.com, *foo.com:99, 1.2.3.4:22, 127.0.0.1/8",  // bypass_rules
    },

    // Expected result.
    false,                          // auto_detect
    GURL(),                         // pac_url
    net::ProxyRulesExpectation::Single(  // proxy_rules
        "www.google.com:80",             // single proxy
        // bypass_rules
        "*.google.com,*foo.com:99,1.2.3.4:22,127.0.0.1/8,<local>"),
  },
};  // tests

template<typename TESTBASE>
class ProxyConfigServiceImplTestBase : public TESTBASE {
 protected:
  ProxyConfigServiceImplTestBase()
      : ui_thread_(BrowserThread::UI, &loop_),
        io_thread_(BrowserThread::IO, &loop_) {}

  virtual void Init(PrefService* pref_service) {
    ASSERT_TRUE(pref_service);
    DBusThreadManager::Initialize();
    PrefProxyConfigTrackerImpl::RegisterPrefs(pref_service);
    ProxyConfigServiceImpl::RegisterPrefs(pref_service);
    proxy_config_service_.reset(new ChromeProxyConfigService(NULL, true));
    config_service_impl_.reset(new ProxyConfigServiceImpl(pref_service));
    config_service_impl_->SetChromeProxyConfigService(
        proxy_config_service_.get());
    // SetChromeProxyConfigService triggers update of initial prefs proxy
    // config by tracker to chrome proxy config service, so flush all pending
    // tasks so that tests start fresh.
    loop_.RunAllPending();
  }

  virtual void TearDown() {
    config_service_impl_->DetachFromPrefService();
    loop_.RunAllPending();
    config_service_impl_.reset();
    proxy_config_service_.reset();
    DBusThreadManager::Shutdown();
  }

  void SetAutomaticProxy(
      ProxyConfigServiceImpl::ProxyConfig::Mode mode,
      const char* pac_url,
      ProxyConfigServiceImpl::ProxyConfig* config,
      ProxyConfigServiceImpl::ProxyConfig::AutomaticProxy* automatic_proxy) {
    config->mode = mode;
    config->state = ProxyPrefs::CONFIG_SYSTEM;
    if (pac_url)
      automatic_proxy->pac_url = GURL(pac_url);
  }

  void SetManualProxy(
      ProxyConfigServiceImpl::ProxyConfig::Mode mode,
      const char* server_uri,
      net::ProxyServer::Scheme scheme,
      ProxyConfigServiceImpl::ProxyConfig* config,
      ProxyConfigServiceImpl::ProxyConfig::ManualProxy* manual_proxy) {
    if (!server_uri)
      return;
    config->mode = mode;
    config->state = ProxyPrefs::CONFIG_SYSTEM;
    manual_proxy->server = net::ProxyServer::FromURI(server_uri, scheme);
  }

  void InitConfigWithTestInput(
      const Input& input, ProxyConfigServiceImpl::ProxyConfig* test_config) {
    switch (input.mode) {
      case MK_MODE(DIRECT):
      case MK_MODE(AUTO_DETECT):
      case MK_MODE(PAC_SCRIPT):
        SetAutomaticProxy(input.mode, input.pac_url, test_config,
                          &test_config->automatic_proxy);
        return;
      case MK_MODE(SINGLE_PROXY):
        SetManualProxy(input.mode, input.single_uri, MK_SCHM(HTTP),
                       test_config, &test_config->single_proxy);
        break;
      case MK_MODE(PROXY_PER_SCHEME):
        SetManualProxy(input.mode, input.http_uri, MK_SCHM(HTTP),
                       test_config, &test_config->http_proxy);
        SetManualProxy(input.mode, input.https_uri, MK_SCHM(HTTPS),
                       test_config, &test_config->https_proxy);
        SetManualProxy(input.mode, input.ftp_uri, MK_SCHM(HTTP),
                       test_config, &test_config->ftp_proxy);
        SetManualProxy(input.mode, input.socks_uri, MK_SCHM(SOCKS5),
                       test_config, &test_config->socks_proxy);
        break;
    }
    if (input.bypass_rules)
      test_config->bypass_rules.ParseFromString(input.bypass_rules);
  }

  // Synchronously gets the latest proxy config.
  net::ProxyConfigService::ConfigAvailability SyncGetLatestProxyConfig(
      net::ProxyConfig* config) {
    *config = net::ProxyConfig();
    // Let message loop process all messages.
    loop_.RunAllPending();
    // Calls ChromeProIOGetProxyConfig (which is called from
    // ProxyConfigService::GetLatestProxyConfig), running on faked IO thread.
    return proxy_config_service_->GetLatestProxyConfig(config);
  }

  MessageLoop loop_;
  scoped_ptr<ChromeProxyConfigService> proxy_config_service_;
  scoped_ptr<ProxyConfigServiceImpl> config_service_impl_;

 private:
  // Default stub state has ethernet as the active connected network and
  // PROFILE_SHARED as profile type, which this unittest expects.
  ScopedStubCrosEnabler stub_cros_enabler_;
  content::TestBrowserThread ui_thread_;
  content::TestBrowserThread io_thread_;
};

class ProxyConfigServiceImplTest
    : public ProxyConfigServiceImplTestBase<testing::Test> {
 protected:
  virtual void SetUp() {
    Init(&pref_service_);
  }

  TestingPrefService pref_service_;
};

TEST_F(ProxyConfigServiceImplTest, NetworkProxy) {
  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) {
    SCOPED_TRACE(StringPrintf("Test[%" PRIuS "] %s", i,
                              tests[i].description.c_str()));

    ProxyConfigServiceImpl::ProxyConfig test_config;
    InitConfigWithTestInput(tests[i].input, &test_config);
    config_service_impl_->SetTesting(&test_config);

    net::ProxyConfig config;
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&config));

    EXPECT_EQ(tests[i].auto_detect, config.auto_detect());
    EXPECT_EQ(tests[i].pac_url, config.pac_url());
    EXPECT_TRUE(tests[i].proxy_rules.Matches(config.proxy_rules()));
  }
}

TEST_F(ProxyConfigServiceImplTest, ModifyFromUI) {
  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) {
    SCOPED_TRACE(StringPrintf("Test[%" PRIuS "] %s", i,
                              tests[i].description.c_str()));

    // Init with direct.
    ProxyConfigServiceImpl::ProxyConfig test_config;
    SetAutomaticProxy(MK_MODE(DIRECT), NULL, &test_config,
                      &test_config.automatic_proxy);
    config_service_impl_->SetTesting(&test_config);

    // Set config to tests[i].input via UI.
    net::ProxyBypassRules bypass_rules;
    const Input& input = tests[i].input;
    switch (input.mode) {
      case MK_MODE(DIRECT) :
        config_service_impl_->UISetProxyConfigToDirect();
        break;
      case MK_MODE(AUTO_DETECT) :
        config_service_impl_->UISetProxyConfigToAutoDetect();
        break;
      case MK_MODE(PAC_SCRIPT) :
        config_service_impl_->UISetProxyConfigToPACScript(GURL(input.pac_url));
        break;
      case MK_MODE(SINGLE_PROXY) :
        config_service_impl_->UISetProxyConfigToSingleProxy(
            net::ProxyServer::FromURI(input.single_uri, MK_SCHM(HTTP)));
        if (input.bypass_rules) {
          bypass_rules.ParseFromString(input.bypass_rules);
          config_service_impl_->UISetProxyConfigBypassRules(bypass_rules);
        }
        break;
      case MK_MODE(PROXY_PER_SCHEME) :
        if (input.http_uri) {
          config_service_impl_->UISetProxyConfigToProxyPerScheme("http",
                  net::ProxyServer::FromURI(input.http_uri, MK_SCHM(HTTP)));
        }
        if (input.https_uri) {
          config_service_impl_->UISetProxyConfigToProxyPerScheme("https",
              net::ProxyServer::FromURI(input.https_uri, MK_SCHM(HTTPS)));
        }
        if (input.ftp_uri) {
          config_service_impl_->UISetProxyConfigToProxyPerScheme("ftp",
              net::ProxyServer::FromURI(input.ftp_uri, MK_SCHM(HTTP)));
        }
        if (input.socks_uri) {
          config_service_impl_->UISetProxyConfigToProxyPerScheme("socks",
              net::ProxyServer::FromURI(input.socks_uri, MK_SCHM(SOCKS5)));
        }
        if (input.bypass_rules) {
          bypass_rules.ParseFromString(input.bypass_rules);
          config_service_impl_->UISetProxyConfigBypassRules(bypass_rules);
        }
        break;
    }

    // Retrieve config from IO thread.
    net::ProxyConfig io_config;
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&io_config));
    EXPECT_EQ(tests[i].auto_detect, io_config.auto_detect());
    EXPECT_EQ(tests[i].pac_url, io_config.pac_url());
    EXPECT_TRUE(tests[i].proxy_rules.Matches(io_config.proxy_rules()));

    // Retrieve config from UI thread.
    ProxyConfigServiceImpl::ProxyConfig ui_config;
    config_service_impl_->UIGetProxyConfig(&ui_config);
    EXPECT_EQ(input.mode, ui_config.mode);
    if (tests[i].is_valid) {
      if (input.pac_url)
        EXPECT_EQ(GURL(input.pac_url), ui_config.automatic_proxy.pac_url);
      const net::ProxyRulesExpectation& proxy_rules = tests[i].proxy_rules;
      if (input.single_uri)
        EXPECT_EQ(proxy_rules.single_proxy,
                  ui_config.single_proxy.server.ToURI());
      if (input.http_uri)
        EXPECT_EQ(proxy_rules.proxy_for_http,
                  ui_config.http_proxy.server.ToURI());
      if (input.https_uri)
        EXPECT_EQ(proxy_rules.proxy_for_https,
                  ui_config.https_proxy.server.ToURI());
      if (input.ftp_uri)
        EXPECT_EQ(proxy_rules.proxy_for_ftp,
                  ui_config.ftp_proxy.server.ToURI());
      if (input.socks_uri) {
        EXPECT_EQ(proxy_rules.fallback_proxy,
                  ui_config.socks_proxy.server.ToURI());
      }
      if (input.bypass_rules)
        EXPECT_TRUE(bypass_rules.Equals(ui_config.bypass_rules));
    }
  }
}

TEST_F(ProxyConfigServiceImplTest, DynamicPrefsOverride) {
  // Groupings of 3 test inputs to use for managed, recommended and network
  // proxies respectively.  Only valid and non-direct test inputs are used.
  const size_t proxies[][3] = {
    { 1, 2, 4, },
    { 1, 4, 2, },
    { 4, 2, 1, },
    { 2, 1, 4, },
    { 2, 4, 5, },
    { 2, 5, 4, },
    { 5, 4, 2, },
    { 4, 2, 5, },
    { 4, 5, 6, },
    { 4, 6, 5, },
    { 6, 5, 4, },
    { 5, 4, 6, },
    { 5, 6, 7, },
    { 5, 7, 6, },
    { 7, 6, 5, },
    { 6, 5, 7, },
    { 6, 7, 8, },
    { 6, 8, 7, },
    { 8, 7, 6, },
    { 7, 6, 8, },
  };
  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(proxies); ++i) {
    const TestParams& managed_params = tests[proxies[i][0]];
    const TestParams& recommended_params = tests[proxies[i][1]];
    const TestParams& network_params = tests[proxies[i][2]];

    SCOPED_TRACE(StringPrintf(
        "Test[%" PRIuS "] managed=[%s], recommended=[%s], network=[%s]", i,
        managed_params.description.c_str(),
        recommended_params.description.c_str(),
        network_params.description.c_str()));

    ProxyConfigServiceImpl::ProxyConfig managed_config;
    InitConfigWithTestInput(managed_params.input, &managed_config);
    ProxyConfigServiceImpl::ProxyConfig recommended_config;
    InitConfigWithTestInput(recommended_params.input, &recommended_config);
    ProxyConfigServiceImpl::ProxyConfig network_config;
    InitConfigWithTestInput(network_params.input, &network_config);

    // Managed proxy pref should take effect over recommended proxy and
    // non-existent network proxy.
    config_service_impl_->SetTesting(NULL);
    pref_service_.SetManagedPref(prefs::kProxy,
                                 managed_config.ToPrefProxyConfig());
    pref_service_.SetRecommendedPref(prefs::kProxy,
                                     recommended_config.ToPrefProxyConfig());
    net::ProxyConfig actual_config;
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&actual_config));
    EXPECT_EQ(managed_params.auto_detect, actual_config.auto_detect());
    EXPECT_EQ(managed_params.pac_url, actual_config.pac_url());
    EXPECT_TRUE(managed_params.proxy_rules.Matches(
        actual_config.proxy_rules()));

    // Recommended proxy pref should take effect when managed proxy pref is
    // removed.
    pref_service_.RemoveManagedPref(prefs::kProxy);
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&actual_config));
    EXPECT_EQ(recommended_params.auto_detect, actual_config.auto_detect());
    EXPECT_EQ(recommended_params.pac_url, actual_config.pac_url());
    EXPECT_TRUE(recommended_params.proxy_rules.Matches(
        actual_config.proxy_rules()));

    // Network proxy should take take effect over recommended proxy pref.
    config_service_impl_->SetTesting(&network_config);
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&actual_config));
    EXPECT_EQ(network_params.auto_detect, actual_config.auto_detect());
    EXPECT_EQ(network_params.pac_url, actual_config.pac_url());
    EXPECT_TRUE(network_params.proxy_rules.Matches(
        actual_config.proxy_rules()));

    // Managed proxy pref should take effect over network proxy.
    pref_service_.SetManagedPref(prefs::kProxy,
                                 managed_config.ToPrefProxyConfig());
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&actual_config));
    EXPECT_EQ(managed_params.auto_detect, actual_config.auto_detect());
    EXPECT_EQ(managed_params.pac_url, actual_config.pac_url());
    EXPECT_TRUE(managed_params.proxy_rules.Matches(
        actual_config.proxy_rules()));

    // Network proxy should take effect over recommended proxy pref when managed
    // proxy pref is removed.
    pref_service_.RemoveManagedPref(prefs::kProxy);
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&actual_config));
    EXPECT_EQ(network_params.auto_detect, actual_config.auto_detect());
    EXPECT_EQ(network_params.pac_url, actual_config.pac_url());
    EXPECT_TRUE(network_params.proxy_rules.Matches(
        actual_config.proxy_rules()));

    // Removing recommended proxy pref should have no effect on network proxy.
    pref_service_.RemoveRecommendedPref(prefs::kProxy);
    EXPECT_EQ(MK_AVAIL(VALID), SyncGetLatestProxyConfig(&actual_config));
    EXPECT_EQ(network_params.auto_detect, actual_config.auto_detect());
    EXPECT_EQ(network_params.pac_url, actual_config.pac_url());
    EXPECT_TRUE(network_params.proxy_rules.Matches(
        actual_config.proxy_rules()));
  }
}

}  // namespace

}  // namespace chromeos