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() {}

  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() {}

  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() {}

  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);

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

  // Even if there is a recent user event visibility change should end the
  // session.
  instance_.OnUserEvent();
  instance_.OnUserEvent();
  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);

  // 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);
  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);
}

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();
  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();
  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();  // 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();  // 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();
  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();
  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);
}

}  // namespace metrics
