// Copyright 2015 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 "net/proxy/proxy_service_mojo.h"

#include <algorithm>
#include <string>
#include <utility>

#include "base/callback_helpers.h"
#include "base/memory/scoped_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "net/base/load_flags.h"
#include "net/base/network_delegate_impl.h"
#include "net/base/test_completion_callback.h"
#include "net/dns/mock_host_resolver.h"
#include "net/log/net_log.h"
#include "net/log/test_net_log.h"
#include "net/log/test_net_log_entry.h"
#include "net/proxy/dhcp_proxy_script_fetcher.h"
#include "net/proxy/in_process_mojo_proxy_resolver_factory.h"
#include "net/proxy/mock_proxy_script_fetcher.h"
#include "net/proxy/mojo_proxy_resolver_factory.h"
#include "net/proxy/proxy_config_service_fixed.h"
#include "net/proxy/proxy_service.h"
#include "net/test/event_waiter.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace net {

namespace {

const char kPacUrl[] = "http://example.com/proxy.pac";
const char kSimplePacScript[] =
    "function FindProxyForURL(url, host) {\n"
    "  return 'PROXY foo:1234';\n"
    "}";
const char kDnsResolvePacScript[] =
    "function FindProxyForURL(url, host) {\n"
    "  if (dnsResolveEx('example.com') != '1.2.3.4')\n"
    "    return 'DIRECT';\n"
    "  return 'QUIC bar:4321';\n"
    "}";
const char kThrowingPacScript[] =
    "function FindProxyForURL(url, host) {\n"
    "  alert('alert: ' + host);\n"
    "  throw new Error('error: ' + url);\n"
    "}";
const char kThrowingOnLoadPacScript[] =
    "function FindProxyForURL(url, host) {}\n"
    "alert('alert: foo');\n"
    "throw new Error('error: http://foo');";

class TestNetworkDelegate : public NetworkDelegateImpl {
 public:
  enum Event {
    PAC_SCRIPT_ERROR,
  };

  EventWaiter<Event>& event_waiter() { return event_waiter_; }

  void OnPACScriptError(int line_number, const base::string16& error) override;

 private:
  EventWaiter<Event> event_waiter_;
};

void TestNetworkDelegate::OnPACScriptError(int line_number,
                                           const base::string16& error) {
  event_waiter_.NotifyEvent(PAC_SCRIPT_ERROR);
  EXPECT_EQ(3, line_number);
  EXPECT_TRUE(base::UTF16ToUTF8(error).find("error: http://foo") !=
              std::string::npos);
}

void CheckCapturedNetLogEntries(const TestNetLogEntry::List& entries) {
  ASSERT_GT(entries.size(), 2u);
  size_t i = 0;
  // ProxyService records its own NetLog entries, so skip forward until the
  // expected event type.
  while (i < entries.size() &&
         entries[i].type != NetLog::TYPE_PAC_JAVASCRIPT_ALERT) {
    i++;
  }
  ASSERT_LT(i, entries.size());
  std::string message;
  ASSERT_TRUE(entries[i].GetStringValue("message", &message));
  EXPECT_EQ("alert: foo", message);
  ASSERT_FALSE(entries[i].params->HasKey("line_number"));

  while (i < entries.size() &&
         entries[i].type != NetLog::TYPE_PAC_JAVASCRIPT_ERROR) {
    i++;
  }
  message.clear();
  ASSERT_TRUE(entries[i].GetStringValue("message", &message));
  EXPECT_THAT(message, testing::HasSubstr("error: http://foo"));
  int line_number = 0;
  ASSERT_TRUE(entries[i].GetIntegerValue("line_number", &line_number));
  EXPECT_EQ(3, line_number);
}

class LoggingMockHostResolver : public MockHostResolver {
 public:
  int Resolve(const RequestInfo& info,
              RequestPriority priority,
              AddressList* addresses,
              const CompletionCallback& callback,
              RequestHandle* out_req,
              const BoundNetLog& net_log) override {
    net_log.AddEvent(NetLog::TYPE_HOST_RESOLVER_IMPL_JOB);
    return MockHostResolver::Resolve(info, priority, addresses, callback,
                                     out_req, net_log);
  }
};

}  // namespace

class ProxyServiceMojoTest : public testing::Test,
                             public MojoProxyResolverFactory {
 protected:
  void SetUp() override {
    mock_host_resolver_.rules()->AddRule("example.com", "1.2.3.4");

    fetcher_ = new MockProxyScriptFetcher;
    proxy_service_ = CreateProxyServiceUsingMojoFactory(
        this, make_scoped_ptr(new ProxyConfigServiceFixed(
                  ProxyConfig::CreateFromCustomPacURL(GURL(kPacUrl)))),
        fetcher_, make_scoped_ptr(new DoNothingDhcpProxyScriptFetcher()),
        &mock_host_resolver_, &net_log_, &network_delegate_);
  }

  scoped_ptr<base::ScopedClosureRunner> CreateResolver(
      const mojo::String& pac_script,
      mojo::InterfaceRequest<interfaces::ProxyResolver> req,
      interfaces::ProxyResolverFactoryRequestClientPtr client) override {
    InProcessMojoProxyResolverFactory::GetInstance()->CreateResolver(
        pac_script, std::move(req), std::move(client));
    return make_scoped_ptr(
        new base::ScopedClosureRunner(on_delete_closure_.closure()));
  }

  TestNetworkDelegate network_delegate_;
  LoggingMockHostResolver mock_host_resolver_;
  MockProxyScriptFetcher* fetcher_;  // Owned by |proxy_service_|.
  TestNetLog net_log_;
  TestClosure on_delete_closure_;
  scoped_ptr<ProxyService> proxy_service_;
};

TEST_F(ProxyServiceMojoTest, Basic) {
  ProxyInfo info;
  TestCompletionCallback callback;
  EXPECT_EQ(ERR_IO_PENDING,
            proxy_service_->ResolveProxy(
                GURL("http://foo"), std::string(), LOAD_NORMAL, &info,
                callback.callback(), nullptr, nullptr, BoundNetLog()));

  // Proxy script fetcher should have a fetch triggered by the first
  // |ResolveProxy()| request.
  EXPECT_TRUE(fetcher_->has_pending_request());
  EXPECT_EQ(GURL(kPacUrl), fetcher_->pending_request_url());
  fetcher_->NotifyFetchCompletion(OK, kSimplePacScript);

  EXPECT_EQ(OK, callback.WaitForResult());
  EXPECT_EQ("PROXY foo:1234", info.ToPacString());
  EXPECT_EQ(0u, mock_host_resolver_.num_resolve());
  proxy_service_.reset();
  on_delete_closure_.WaitForResult();
}

TEST_F(ProxyServiceMojoTest, DnsResolution) {
  ProxyInfo info;
  TestCompletionCallback callback;
  BoundTestNetLog bound_net_log;
  EXPECT_EQ(ERR_IO_PENDING,
            proxy_service_->ResolveProxy(
                GURL("http://foo"), std::string(), LOAD_NORMAL, &info,
                callback.callback(), nullptr, nullptr, bound_net_log.bound()));

  // Proxy script fetcher should have a fetch triggered by the first
  // |ResolveProxy()| request.
  EXPECT_TRUE(fetcher_->has_pending_request());
  EXPECT_EQ(GURL(kPacUrl), fetcher_->pending_request_url());
  fetcher_->NotifyFetchCompletion(OK, kDnsResolvePacScript);

  EXPECT_EQ(OK, callback.WaitForResult());
  EXPECT_EQ("QUIC bar:4321", info.ToPacString());
  EXPECT_EQ(1u, mock_host_resolver_.num_resolve());
  proxy_service_.reset();
  on_delete_closure_.WaitForResult();

  TestNetLogEntry::List entries;
  bound_net_log.GetEntries(&entries);
  // There should be one entry with type TYPE_HOST_RESOLVER_IMPL_JOB.
  EXPECT_EQ(1, std::count_if(entries.begin(), entries.end(),
                             [](const TestNetLogEntry& entry) {
                               return entry.type ==
                                      NetLog::TYPE_HOST_RESOLVER_IMPL_JOB;
                             }));
}

TEST_F(ProxyServiceMojoTest, Error) {
  ProxyInfo info;
  TestCompletionCallback callback;
  BoundTestNetLog bound_net_log;
  EXPECT_EQ(ERR_IO_PENDING,
            proxy_service_->ResolveProxy(
                GURL("http://foo"), std::string(), LOAD_NORMAL, &info,
                callback.callback(), nullptr, nullptr, bound_net_log.bound()));

  // Proxy script fetcher should have a fetch triggered by the first
  // |ResolveProxy()| request.
  EXPECT_TRUE(fetcher_->has_pending_request());
  EXPECT_EQ(GURL(kPacUrl), fetcher_->pending_request_url());
  fetcher_->NotifyFetchCompletion(OK, kThrowingPacScript);

  network_delegate_.event_waiter().WaitForEvent(
      TestNetworkDelegate::PAC_SCRIPT_ERROR);

  EXPECT_EQ(OK, callback.WaitForResult());
  EXPECT_EQ("DIRECT", info.ToPacString());
  EXPECT_EQ(0u, mock_host_resolver_.num_resolve());

  TestNetLogEntry::List entries;
  bound_net_log.GetEntries(&entries);
  CheckCapturedNetLogEntries(entries);
  entries.clear();
  net_log_.GetEntries(&entries);
  CheckCapturedNetLogEntries(entries);
}

TEST_F(ProxyServiceMojoTest, ErrorOnInitialization) {
  ProxyInfo info;
  TestCompletionCallback callback;
  EXPECT_EQ(ERR_IO_PENDING,
            proxy_service_->ResolveProxy(
                GURL("http://foo"), std::string(), LOAD_NORMAL, &info,
                callback.callback(), nullptr, nullptr, BoundNetLog()));

  // Proxy script fetcher should have a fetch triggered by the first
  // |ResolveProxy()| request.
  EXPECT_TRUE(fetcher_->has_pending_request());
  EXPECT_EQ(GURL(kPacUrl), fetcher_->pending_request_url());
  fetcher_->NotifyFetchCompletion(OK, kThrowingOnLoadPacScript);

  network_delegate_.event_waiter().WaitForEvent(
      TestNetworkDelegate::PAC_SCRIPT_ERROR);

  EXPECT_EQ(OK, callback.WaitForResult());
  EXPECT_EQ("DIRECT", info.ToPacString());
  EXPECT_EQ(0u, mock_host_resolver_.num_resolve());

  TestNetLogEntry::List entries;
  net_log_.GetEntries(&entries);
  CheckCapturedNetLogEntries(entries);
}

}  // namespace net