// 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 <cmath>  // For std::modf.
#include <map>
#include <string>

#include "base/command_line.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "content/browser/network/network_quality_observer_impl.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/base/network_change_notifier.h"
#include "net/base/network_change_notifier_factory.h"
#include "net/dns/mock_host_resolver.h"
#include "net/log/test_net_log.h"
#include "net/nqe/effective_connection_type.h"
#include "services/network/test/test_network_quality_tracker.h"

namespace {

// Returns the total count of samples in |histogram|.
int GetTotalSampleCount(base::HistogramTester* tester,
                        const std::string& histogram) {
  int count = 0;
  std::vector<base::Bucket> buckets = tester->GetAllSamples(histogram);
  for (const auto& bucket : buckets)
    count += bucket.count;
  return count;
}

void VerifyRtt(base::TimeDelta expected_rtt, int32_t got_rtt_milliseconds) {
  EXPECT_EQ(0, got_rtt_milliseconds % 50)
      << " got_rtt_milliseconds=" << got_rtt_milliseconds;

  if (expected_rtt > base::Milliseconds(3000))
    expected_rtt = base::Milliseconds(3000);

  // The difference between the actual and the estimate value should be within
  // 10%. Add 50 (bucket size used in Blink) to account for the cases when the
  // sample may spill over to the next bucket due to the added noise of 10%.
  // For example, if sample is 300 msec, after adding noise, it may become 330,
  // and after rounding off, it would spill over to the next bucket of 350 msec.
  EXPECT_GE((expected_rtt.InMilliseconds() * 0.1) + 50,
            std::abs(expected_rtt.InMilliseconds() - got_rtt_milliseconds))
      << " expected_rtt=" << expected_rtt
      << " got_rtt_milliseconds=" << got_rtt_milliseconds;
}

void VerifyDownlinkKbps(double expected_kbps, double got_kbps) {
  // First verify that |got_kbps| is a multiple of 50.
  int quotient = static_cast<int>(got_kbps / 50);
  // |mod| is the remainder left after dividing |got_kbps| by 50 while
  // restricting the quotient to integer.  For example, if |got_kbps| is
  // 1050, then mod will be 0. If |got_kbps| is 1030, mod will be 30.
  double mod = got_kbps - 50 * quotient;
  EXPECT_LE(0.0, mod);
  EXPECT_GT(50.0, mod);
  // It is possible that |mod| is not exactly 0 because of floating point
  // computations. e.g., |got_kbps| may be 99.999999, in which case |mod|
  // will be 49.999999.
  EXPECT_TRUE(mod < (1e-5) || (50 - mod) < 1e-5) << " got_kbps=" << got_kbps;

  if (expected_kbps > 10000)
    expected_kbps = 10000;

  // The difference between the actual and the estimate value should be within
  // 10%. Add 50 (bucket size used in Blink) to account for the cases when the
  // sample may spill over to the next bucket due to the added noise of 10%.
  // For example, if sample is 300 kbps, after adding noise, it may become 330,
  // and after rounding off, it would spill over to the next bucket of 350 kbps.
  EXPECT_GE((expected_kbps * 0.1) + 50, std::abs(expected_kbps - got_kbps))
      << " expected_kbps=" << expected_kbps << " got_kbps=" << got_kbps;
}

class MockNetworkChangeNotifierWifi : public net::NetworkChangeNotifier {
 public:
  void GetCurrentMaxBandwidthAndConnectionType(
      double* max_bandwidth_mbps,
      ConnectionType* connection_type) const override {
    *connection_type = NetworkChangeNotifier::CONNECTION_WIFI;
    *max_bandwidth_mbps =
        net::NetworkChangeNotifier::GetMaxBandwidthMbpsForConnectionSubtype(
            net::NetworkChangeNotifier::SUBTYPE_WIFI_N);
  }
  ConnectionType GetCurrentConnectionType() const override {
    return NetworkChangeNotifier::CONNECTION_WIFI;
  }
};

}  // namespace

namespace content {

class NetInfoBrowserTest : public content::ContentBrowserTest {
 public:
  NetInfoBrowserTest()
      : test_network_quality_tracker_(
            std::make_unique<network::TestNetworkQualityTracker>()) {}

  network::NetworkQualityTracker* GetNetworkQualityTracker() const {
    return test_network_quality_tracker_.get();
  }

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    // TODO(jkarlin): Once NetInfo is enabled on all platforms remove this
    // switch.
    command_line->AppendSwitch(switches::kEnableNetworkInformationDownlinkMax);

    // TODO(jkarlin): Remove this once downlinkMax is no longer
    // experimental.
    command_line->AppendSwitch(
        switches::kEnableExperimentalWebPlatformFeatures);
  }

  void SetUp() override {
    net::NetworkChangeNotifier::SetTestNotificationsOnly(true);

    content::ContentBrowserTest::SetUp();
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    base::RunLoop().RunUntilIdle();
  }

  static void SetConnectionType(
      net::NetworkChangeNotifier::ConnectionType type,
      net::NetworkChangeNotifier::ConnectionSubtype subtype) {
    net::NetworkChangeNotifier::NotifyObserversOfMaxBandwidthChangeForTests(
        net::NetworkChangeNotifier::GetMaxBandwidthMbpsForConnectionSubtype(
            subtype),
        type);
    base::RunLoop().RunUntilIdle();
  }

  std::string RunScriptExtractString(const std::string& script) {
    return EvalJs(shell(), script).ExtractString();
  }

  bool RunScriptExtractBool(const std::string& script) {
    return EvalJs(shell(), script).ExtractBool();
  }

  double RunScriptExtractDouble(const std::string& script) {
    return EvalJs(shell(), script).ExtractDouble();
  }

  int RunScriptExtractInt(const std::string& script) {
    return EvalJs(shell(), script).ExtractInt();
  }

 private:
  std::unique_ptr<network::TestNetworkQualityTracker>
      test_network_quality_tracker_;
};

// Make sure the type is correct when the page is first opened.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, VerifyNetworkStateInitialized) {
  // Mock out the NCN.
  net::NetworkChangeNotifier::DisableForTest disable_for_test;
  MockNetworkChangeNotifierWifi mock_notifier;

  EXPECT_TRUE(NavigateToURL(shell(), content::GetTestUrl("", "net_info.html")));
  EXPECT_TRUE(RunScriptExtractBool("getOnLine()"));
  EXPECT_EQ("wifi", RunScriptExtractString("getType()"));
  EXPECT_EQ(net::NetworkChangeNotifier::GetMaxBandwidthMbpsForConnectionSubtype(
                net::NetworkChangeNotifier::SUBTYPE_WIFI_N),
            RunScriptExtractDouble("getDownlinkMax()"));
}

// Make sure that type changes in the browser make their way to
// navigator.connection.type.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, NetworkChangePlumbsToNavigator) {
  EXPECT_TRUE(NavigateToURL(shell(), content::GetTestUrl("", "net_info.html")));
  SetConnectionType(net::NetworkChangeNotifier::CONNECTION_WIFI,
                    net::NetworkChangeNotifier::SUBTYPE_WIFI_N);
  EXPECT_EQ("wifi", RunScriptExtractString("getType()"));
  EXPECT_EQ(net::NetworkChangeNotifier::GetMaxBandwidthMbpsForConnectionSubtype(
                net::NetworkChangeNotifier::SUBTYPE_WIFI_N),
            RunScriptExtractDouble("getDownlinkMax()"));

  SetConnectionType(net::NetworkChangeNotifier::CONNECTION_ETHERNET,
                    net::NetworkChangeNotifier::SUBTYPE_GIGABIT_ETHERNET);
  EXPECT_EQ("ethernet", RunScriptExtractString("getType()"));
  EXPECT_EQ(net::NetworkChangeNotifier::GetMaxBandwidthMbpsForConnectionSubtype(
                net::NetworkChangeNotifier::SUBTYPE_GIGABIT_ETHERNET),
            RunScriptExtractDouble("getDownlinkMax()"));
}

// Make sure that type changes in the browser make their way to
// navigator.isOnline.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, IsOnline) {
  EXPECT_TRUE(NavigateToURL(shell(), content::GetTestUrl("", "net_info.html")));
  SetConnectionType(net::NetworkChangeNotifier::CONNECTION_ETHERNET,
                    net::NetworkChangeNotifier::SUBTYPE_GIGABIT_ETHERNET);
  EXPECT_TRUE(RunScriptExtractBool("getOnLine()"));
  SetConnectionType(net::NetworkChangeNotifier::CONNECTION_NONE,
                    net::NetworkChangeNotifier::SUBTYPE_NONE);
  EXPECT_FALSE(RunScriptExtractBool("getOnLine()"));
  SetConnectionType(net::NetworkChangeNotifier::CONNECTION_WIFI,
                    net::NetworkChangeNotifier::SUBTYPE_WIFI_N);
  EXPECT_TRUE(RunScriptExtractBool("getOnLine()"));
}

// Creating a new render view shouldn't reinitialize Blink's
// NetworkStateNotifier. See https://crbug.com/535081.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, TwoRenderViewsInOneProcess) {
  SetConnectionType(net::NetworkChangeNotifier::CONNECTION_ETHERNET,
                    net::NetworkChangeNotifier::SUBTYPE_GIGABIT_ETHERNET);
  EXPECT_TRUE(NavigateToURL(shell(), content::GetTestUrl("", "net_info.html")));
  EXPECT_TRUE(RunScriptExtractBool("getOnLine()"));

  SetConnectionType(net::NetworkChangeNotifier::CONNECTION_NONE,
                    net::NetworkChangeNotifier::SUBTYPE_NONE);
  EXPECT_FALSE(RunScriptExtractBool("getOnLine()"));

  // Open the same page in a new window on the same process.
  EXPECT_TRUE(ExecJs(shell(), "window.open(\"net_info.html\")"));

  // The network state should not have reinitialized to what it was when opening
  // the first window (online).
  EXPECT_FALSE(RunScriptExtractBool("getOnLine()"));
}

// Verify that when the network quality notifications are not sent, the
// Javascript API returns a valid estimate that is multiple of 50 msec and 50
// kbps.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest,
                       NetworkQualityEstimatorNotInitialized) {
  base::HistogramTester histogram_tester;
  NetworkQualityObserverImpl impl(GetNetworkQualityTracker());

  EXPECT_TRUE(embedded_test_server()->Start());
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));

  // When NQE is not initialized, the javascript calls should return default
  // values.
  EXPECT_EQ(0, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(10000, RunScriptExtractDouble("getDownlink()") * 1000);
}

// Make sure the changes in the effective connection type are notified to the
// render thread.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest,
                       EffectiveConnectionTypeChangeNotified) {
  base::HistogramTester histogram_tester;
  NetworkQualityObserverImpl impl(GetNetworkQualityTracker());

  base::TimeDelta http_rtt(base::Milliseconds(1000));
  int32_t downstream_throughput_kbps = 300;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);

  EXPECT_TRUE(embedded_test_server()->Start());
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));

  FetchHistogramsFromChildProcesses();

  int samples =
      GetTotalSampleCount(&histogram_tester, "NQE.RenderThreadNotified");
  EXPECT_LT(0, samples);

  // Change effective connection type so that the renderer process is notified.
  // Changing the effective connection type from 2G to 3G is guaranteed to
  // generate the notification to the renderers, irrespective of the current
  // effective connection type.
  GetNetworkQualityTracker()->ReportEffectiveConnectionTypeForTesting(
      net::EFFECTIVE_CONNECTION_TYPE_2G);
  EXPECT_EQ("2g", RunScriptExtractString("getEffectiveType()"));

  GetNetworkQualityTracker()->ReportEffectiveConnectionTypeForTesting(
      net::EFFECTIVE_CONNECTION_TYPE_3G);
  EXPECT_EQ("3g", RunScriptExtractString("getEffectiveType()"));

  FetchHistogramsFromChildProcesses();
  EXPECT_GT(GetTotalSampleCount(&histogram_tester, "NQE.RenderThreadNotified"),
            samples);
}

// Make sure the changes in the network quality are notified to the render
// thread, and the changed network quality is accessible via Javascript API.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, NetworkQualityChangeNotified) {
  base::HistogramTester histogram_tester;
  NetworkQualityObserverImpl impl(GetNetworkQualityTracker());

  base::TimeDelta http_rtt(base::Milliseconds(1000));
  int32_t downstream_throughput_kbps = 300;

  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);

  EXPECT_TRUE(embedded_test_server()->Start());
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));

  FetchHistogramsFromChildProcesses();
  EXPECT_FALSE(
      histogram_tester.GetAllSamples("NQE.RenderThreadNotified").empty());

  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);

  // Verify that the network quality change is accessible via Javascript API.
  http_rtt = base::Seconds(10);
  downstream_throughput_kbps = 3000;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);
  base::RunLoop().RunUntilIdle();
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);
}

// Make sure the changes in the network quality are rounded to the nearest
// 50 milliseconds or 50 kbps.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, NetworkQualityChangeRounded) {
  base::HistogramTester histogram_tester;
  NetworkQualityObserverImpl impl(GetNetworkQualityTracker());

  // Verify that the network quality is rounded properly.
  base::TimeDelta http_rtt(base::Milliseconds(103));
  int32_t downstream_throughput_kbps = 8303;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);

  EXPECT_TRUE(embedded_test_server()->Start());
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);

  http_rtt = base::Milliseconds(1103);
  downstream_throughput_kbps = 1307;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);
  base::RunLoop().RunUntilIdle();
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);

  http_rtt = base::Milliseconds(2112);
  downstream_throughput_kbps = 2112;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);
  base::RunLoop().RunUntilIdle();
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);
}

// Make sure the network quality are rounded down when it exceeds the upper
// limit.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, NetworkQualityChangeUpperLimit) {
  base::HistogramTester histogram_tester;
  NetworkQualityObserverImpl impl(GetNetworkQualityTracker());

  base::TimeDelta http_rtt(base::Milliseconds(12003));
  int32_t downstream_throughput_kbps = 30300;

  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);

  EXPECT_TRUE(embedded_test_server()->Start());
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);
}

// Make sure the noise added to the network quality varies with the hostname.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, NetworkQualityRandomized) {
  base::HistogramTester histogram_tester;
  NetworkQualityObserverImpl impl(GetNetworkQualityTracker());

  base::TimeDelta http_rtt(base::Milliseconds(2000));
  int32_t downstream_throughput_kbps = 3000;

  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);

  EXPECT_TRUE(embedded_test_server()->Start());

  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);

  const int32_t rtt_noise_milliseconds = RunScriptExtractInt("getRtt()") - 2000;
  const int32_t downlink_noise_kbps =
      RunScriptExtractDouble("getDownlink()") * 1000 - 3000;

  // When the hostname is not changed, the noise should not change.
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);
  EXPECT_EQ(rtt_noise_milliseconds, RunScriptExtractInt("getRtt()") - 2000);
  EXPECT_EQ(downlink_noise_kbps,
            RunScriptExtractDouble("getDownlink()") * 1000 - 3000);

  // Verify that changing the hostname changes the noise. It is possible that
  // the hash of a different host also maps to the same bucket among 20 buckets.
  // Try 10 different hosts. This reduces the probability of failure of this
  // test to (1/20)^10 = 9,7 * 10^-14.
  for (size_t i = 0; i < 10; ++i) {
    // The noise added is a function of the hostname. Varying the hostname
    // should vary the noise.
    std::string fake_hostname = "example" + base::NumberToString(i) + ".com";
    EXPECT_TRUE(NavigateToURL(shell(), embedded_test_server()->GetURL(
                                           fake_hostname, "/net_info.html")));
    VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
    VerifyDownlinkKbps(downstream_throughput_kbps,
                       RunScriptExtractDouble("getDownlink()") * 1000);

    int32_t new_rtt_noise_milliseconds = RunScriptExtractInt("getRtt()") - 2000;
    int32_t new_downlink_noise_kbps =
        RunScriptExtractDouble("getDownlink()") * 1000 - 3000;

    if (rtt_noise_milliseconds != new_rtt_noise_milliseconds &&
        downlink_noise_kbps != new_downlink_noise_kbps) {
      return;
    }
  }
  NOTREACHED() << "Noise not added to the network quality estimates";
}

// Make sure the minor changes (<10%) in the network quality are not notified.
IN_PROC_BROWSER_TEST_F(NetInfoBrowserTest, NetworkQualityChangeNotNotified) {
  base::HistogramTester histogram_tester;
  NetworkQualityObserverImpl impl(GetNetworkQualityTracker());

  // Verify that the network quality is rounded properly.
  base::TimeDelta http_rtt(base::Milliseconds(1123));
  int32_t downstream_throughput_kbps = 1303;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);

  EXPECT_TRUE(embedded_test_server()->Start());
  EXPECT_TRUE(
      NavigateToURL(shell(), embedded_test_server()->GetURL("/net_info.html")));
  VerifyRtt(http_rtt, RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(downstream_throughput_kbps,
                     RunScriptExtractDouble("getDownlink()") * 1000);

  // All the 3 metrics change by less than 10%. So, the observers are not
  // notified.
  http_rtt = base::Milliseconds(1223);
  downstream_throughput_kbps = 1403;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);
  base::RunLoop().RunUntilIdle();
  VerifyRtt(base::Milliseconds(1100), RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(1300, RunScriptExtractDouble("getDownlink()") * 1000);

  // HTTP RTT has changed by more than 10% from the last notified value of
  // |network_quality_1|. The observers should be notified.
  http_rtt = base::Milliseconds(2223);
  downstream_throughput_kbps = 1403;
  GetNetworkQualityTracker()->ReportRTTsAndThroughputForTesting(
      http_rtt, downstream_throughput_kbps);
  base::RunLoop().RunUntilIdle();
  VerifyRtt(base::Milliseconds(2200), RunScriptExtractInt("getRtt()"));
  VerifyDownlinkKbps(1400, RunScriptExtractDouble("getDownlink()") * 1000);
}

}  // namespace content