// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/metrics/desktop_session_duration/desktop_session_duration_tracker.h"

#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_command_line.h"
#include "base/test/task_environment.h"
#include "base/threading/platform_thread.h"
#include "chrome/common/chrome_switches.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"

namespace metrics {
namespace {

const base::TimeDelta kZeroTime = base::Seconds(0);
const base::TimeDelta kInactivityTimeoutForTesting = base::Seconds(1);

// Mock class for |DesktopSessionDurationTracker| for testing.
class MockDesktopSessionDurationTracker : public DesktopSessionDurationTracker {
 public:
  MockDesktopSessionDurationTracker() = default;

  MockDesktopSessionDurationTracker(const MockDesktopSessionDurationTracker&) =
      delete;
  MockDesktopSessionDurationTracker& operator=(
      const MockDesktopSessionDurationTracker&) = delete;

  bool is_timeout() const { return time_out_; }

  using DesktopSessionDurationTracker::OnAudioStart;
  using DesktopSessionDurationTracker::OnAudioEnd;

 protected:
  void OnTimerFired() override {
    DesktopSessionDurationTracker::OnTimerFired();
    time_out_ = true;
  }

 private:
  bool time_out_ = false;
};

// Mock class for |DesktopSessionDurationTracker::Observer| for testing.
class MockDesktopSessionObserver
    : public metrics::DesktopSessionDurationTracker::Observer {
 public:
  MockDesktopSessionObserver() = default;

  MockDesktopSessionObserver(const MockDesktopSessionObserver&) = delete;
  MockDesktopSessionObserver& operator=(const MockDesktopSessionObserver&) =
      delete;

  int session_started_count() const { return session_started_count_; }
  int session_ended_count() const { return session_ended_count_; }

 protected:
  // metrics::DesktopSessionDurationTracker::Observer:
  void OnSessionStarted(base::TimeTicks session_start) override {
    session_started_count_ = true;
  }
  void OnSessionEnded(base::TimeDelta session_length,
                      base::TimeTicks session_end) override {
    session_ended_count_ = true;
  }

 private:
  int session_started_count_ = false;
  int session_ended_count_ = false;
};

class DesktopSessionDurationTrackerTest : public testing::Test {
 public:
  DesktopSessionDurationTrackerTest() = default;

  DesktopSessionDurationTrackerTest(const DesktopSessionDurationTrackerTest&) =
      delete;
  DesktopSessionDurationTrackerTest& operator=(
      const DesktopSessionDurationTrackerTest&) = delete;

  void SetUp() override {
    metrics::DesktopSessionDurationTracker::Initialize();
    instance_.SetInactivityTimeoutForTesting(kInactivityTimeoutForTesting);
  }

  void TearDown() override {
    metrics::DesktopSessionDurationTracker::CleanupForTesting();
  }

  void ExpectTotalDuration(base::TimeDelta duration) {
    histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
    base::Bucket bucket =
        histogram_tester_.GetAllSamples("Session.TotalDuration")[0];
    int max_expected_value = duration.InMilliseconds();
    EXPECT_LE(bucket.min, max_expected_value);
  }

  base::HistogramTester histogram_tester_;
  MockDesktopSessionDurationTracker instance_;
  MockDesktopSessionObserver observer_;

 private:
  base::test::SingleThreadTaskEnvironment task_environment_;
};

}  // namespace

TEST_F(DesktopSessionDurationTrackerTest, TestVisibility) {
  // The browser becomes visible but it shouldn't start the session.
  instance_.OnVisibilityChanged(true, kZeroTime);
  EXPECT_FALSE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);
  histogram_tester_.ExpectTotalCount(
      "PUMA.RegionalCapabilities.Session.TotalDuration.Recorded", 0);

  instance_.OnUserEvent(std::nullopt);
  EXPECT_TRUE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);
  histogram_tester_.ExpectTotalCount(
      "PUMA.RegionalCapabilities.Session.TotalDuration.Recorded", 0);

  // Even if there is a recent user event visibility change should end the
  // session.
  instance_.OnUserEvent(std::nullopt);
  instance_.OnUserEvent(std::nullopt);
  instance_.OnVisibilityChanged(false, kZeroTime);
  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
  histogram_tester_.ExpectTotalCount(
      "PUMA.RegionalCapabilities.Session.TotalDuration.Recorded", 1);

  // For the second time only visibility change should start the session.
  instance_.OnVisibilityChanged(true, kZeroTime);
  EXPECT_TRUE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
  histogram_tester_.ExpectTotalCount(
      "PUMA.RegionalCapabilities.Session.TotalDuration.Recorded", 1);
  instance_.OnVisibilityChanged(false, kZeroTime);
  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 2);
  histogram_tester_.ExpectTotalCount(
      "PUMA.RegionalCapabilities.Session.TotalDuration.Recorded", 2);
}

TEST_F(DesktopSessionDurationTrackerTest, TestUserEvent) {
  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  // User event doesn't go through if nothing is visible.
  instance_.OnUserEvent(std::nullopt);
  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnUserEvent(std::nullopt);
  EXPECT_TRUE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  // Wait until the session expires.
  while (!instance_.is_timeout()) {
    base::RunLoop().RunUntilIdle();
  }

  EXPECT_FALSE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
}

TEST_F(DesktopSessionDurationTrackerTest, TestAudioEvent) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnAudioStart();
  EXPECT_TRUE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_TRUE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  instance_.OnVisibilityChanged(false, kZeroTime);
  EXPECT_TRUE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_TRUE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  instance_.OnAudioEnd();
  EXPECT_TRUE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  // Wait until the session expires.
  while (!instance_.is_timeout()) {
    base::RunLoop().RunUntilIdle();
  }

  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_FALSE(instance_.is_audio_playing());
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
}

TEST_F(DesktopSessionDurationTrackerTest, TestInputTimeoutDiscount) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  base::TimeTicks before_session_start = base::TimeTicks::Now();
  instance_.OnUserEvent(std::nullopt);  // This should start the session
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  // Wait until the session expires.
  while (!instance_.is_timeout()) {
    base::RunLoop().RunUntilIdle();
  }
  base::TimeTicks after_session_end = base::TimeTicks::Now();

  ExpectTotalDuration(after_session_end - before_session_start -
                      kInactivityTimeoutForTesting);
}

TEST_F(DesktopSessionDurationTrackerTest, TestVisibilityTimeoutDiscount) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  base::TimeTicks before_session_start = base::TimeTicks::Now();
  instance_.OnUserEvent(std::nullopt);  // This should start the session
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  // Sleep a little while.
  base::TimeDelta kDelay = base::Seconds(2);
  while (true) {
    base::TimeDelta elapsed = base::TimeTicks::Now() - before_session_start;
    if (elapsed >= kDelay)
      break;
    base::PlatformThread::Sleep(kDelay);
  }

  // End the session via visibility change.
  instance_.OnVisibilityChanged(false, kDelay);
  base::TimeTicks after_session_end = base::TimeTicks::Now();

  ExpectTotalDuration(after_session_end - before_session_start - kDelay);
}

TEST_F(DesktopSessionDurationTrackerTest, TestObserver) {
  instance_.AddObserver(&observer_);

  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_EQ(observer_.session_started_count(), 0);
  EXPECT_EQ(observer_.session_ended_count(), 0);
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnUserEvent(std::nullopt);
  EXPECT_TRUE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_EQ(observer_.session_ended_count(), 0);
  EXPECT_EQ(observer_.session_started_count(), 1);
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  // Wait until the session expires.
  while (!instance_.is_timeout()) {
    base::RunLoop().RunUntilIdle();
  }

  EXPECT_FALSE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_EQ(observer_.session_started_count(), 1);
  EXPECT_EQ(observer_.session_ended_count(), 1);
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
}

TEST_F(DesktopSessionDurationTrackerTest, TestNoDoubleEndSession) {
  instance_.AddObserver(&observer_);

  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_EQ(observer_.session_started_count(), 0);
  EXPECT_EQ(observer_.session_ended_count(), 0);
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnUserEvent(std::nullopt);
  EXPECT_TRUE(instance_.in_session());
  EXPECT_TRUE(instance_.is_visible());
  EXPECT_EQ(observer_.session_ended_count(), 0);
  EXPECT_EQ(observer_.session_started_count(), 1);
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 0);

  // Simulate Chrome being hidden. This should end the session.
  instance_.OnVisibilityChanged(false, kZeroTime);
  EXPECT_FALSE(instance_.in_session());
  EXPECT_FALSE(instance_.is_visible());
  EXPECT_EQ(observer_.session_ended_count(), 1);
  EXPECT_EQ(observer_.session_started_count(), 1);
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);

  // When the timeout expires, observers shouldn't be notified again and the
  // histogram shouldn't be recorded again.
  base::PlatformThread::Sleep(kInactivityTimeoutForTesting * 2);
  EXPECT_EQ(observer_.session_ended_count(), 1);
  EXPECT_EQ(observer_.session_started_count(), 1);
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
}

#if BUILDFLAG(IS_WIN)
TEST_F(DesktopSessionDurationTrackerTest, TestNoAutoLaunchHistogram) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnUserEvent(ui::EventType::kMouseMoved);

  instance_.OnVisibilityChanged(false, kZeroTime);

  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
  histogram_tester_.ExpectTotalCount(
      "Session.TotalDuration.IgnoreNonInteractiveTimeForOSLaunchedSessions", 1);
}

class DesktopSessionDurationTrackerInteractiveSessionTest
    : public DesktopSessionDurationTrackerTest {
  void SetUp() override {
    DesktopSessionDurationTrackerTest::SetUp();
    scoped_command_line_.GetProcessCommandLine()->AppendSwitch(
        switches::kStartupForegroundLaunch);
  }

 private:
  base::test::ScopedCommandLine scoped_command_line_;
};

TEST_F(DesktopSessionDurationTrackerInteractiveSessionTest,
       TestInteractiveEventStart) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  // Start session with a non-interactive event (e.g. mouse move).
  instance_.OnUserEvent(ui::EventType::kMouseMoved);
  EXPECT_TRUE(instance_.in_session());
  EXPECT_TRUE(instance_.waiting_for_first_interactive_session());

  // Send an interactive event (e.g. mouse click).
  instance_.OnUserEvent(ui::EventType::kMousePressed);
  EXPECT_FALSE(instance_.waiting_for_first_interactive_session());

  // End session
  instance_.OnVisibilityChanged(false, kZeroTime);

  // Verify that the histogram is recorded
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
  histogram_tester_.ExpectTotalCount(
      "Session.TotalDuration.IgnoreNonInteractiveTimeForOSLaunchedSessions", 1);
}

TEST_F(DesktopSessionDurationTrackerInteractiveSessionTest,
       TestNoInteractiveEventNoHistogram) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnUserEvent(ui::EventType::kMouseMoved);

  instance_.OnVisibilityChanged(false, kZeroTime);

  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
  histogram_tester_.ExpectTotalCount(
      "Session.TotalDuration.IgnoreNonInteractiveTimeForOSLaunchedSessions", 0);
}

TEST_F(DesktopSessionDurationTrackerInteractiveSessionTest,
       TestMultipleVisibilityChangesNoInteractiveEvent) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnUserEvent(ui::EventType::kMouseMoved);
  instance_.OnVisibilityChanged(false, kZeroTime);

  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnUserEvent(ui::EventType::kMouseMoved);
  instance_.OnVisibilityChanged(false, kZeroTime);

  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 2);
  histogram_tester_.ExpectTotalCount(
      "Session.TotalDuration.IgnoreNonInteractiveTimeForOSLaunchedSessions", 0);
}

TEST_F(DesktopSessionDurationTrackerInteractiveSessionTest,
       TestSecondSessionDoesNotWaitForInteractiveEvent) {
  instance_.OnVisibilityChanged(true, kZeroTime);
  // Start first session with an interactive event.
  instance_.OnUserEvent(ui::EventType::kMousePressed);
  instance_.OnVisibilityChanged(false, kZeroTime);

  // Verify first session recorded.
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 1);
  histogram_tester_.ExpectTotalCount(
      "Session.TotalDuration.IgnoreNonInteractiveTimeForOSLaunchedSessions", 1);

  // Second session should start with only a visibility change.
  instance_.OnVisibilityChanged(true, kZeroTime);
  instance_.OnVisibilityChanged(false, kZeroTime);

  // Verify second session also recorded to both histograms.
  histogram_tester_.ExpectTotalCount("Session.TotalDuration", 2);
  histogram_tester_.ExpectTotalCount(
      "Session.TotalDuration.IgnoreNonInteractiveTimeForOSLaunchedSessions", 2);
}
#endif  // BUILDFLAG(IS_WIN)

}  // namespace metrics
