// 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 "content/browser/media/audio_stream_monitor.h"

#include <map>
#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/invalidate_type.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "media/base/audio_power_monitor.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::InvokeWithoutArgs;

namespace content {

namespace {

const GlobalRenderFrameHostId kRenderFrameHostId = {1, 2};
const GlobalRenderFrameHostId kRenderFrameHostIdSameProcess = {1, 3};
const GlobalRenderFrameHostId kRenderFrameHostIdOtherProcess = {4, 5};
const int kStreamId = 6;
const int kAnotherStreamId = 7;

// Used to confirm audio indicator state changes occur at the correct times.
class MockWebContentsDelegate : public WebContentsDelegate {
 public:
  MOCK_METHOD2(NavigationStateChanged,
               void(WebContents* source, InvalidateTypes changed_flags));
};

}  // namespace

class AudioStreamMonitorTest : public RenderViewHostTestHarness {
 public:
  AudioStreamMonitorTest()
      : RenderViewHostTestHarness(
            base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
    // Start time at non-zero.
    task_environment()->FastForwardBy(base::Seconds(1000000));
  }

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

  void SetUp() override {
    RenderViewHostTestHarness::SetUp();

    WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(
        RenderViewHostTestHarness::web_contents());
    web_contents->SetDelegate(&mock_web_contents_delegate_);

    monitor_ = web_contents->audio_stream_monitor();
  }

  void TearDown() override {
    monitor_ = nullptr;
    RenderViewHostTestHarness::TearDown();
  }

  void FastForwardBy(const base::TimeDelta& delta) {
    task_environment()->FastForwardBy(delta);
  }

  void ExpectIsMonitoring(GlobalRenderFrameHostId render_frame_host_id,
                          int stream_id,
                          bool is_polling) {
    const AudioStreamMonitor::StreamID key = {render_frame_host_id, stream_id};
    EXPECT_EQ(is_polling,
              monitor_->streams_.find(key) != monitor_->streams_.end());
  }

  void ExpectTabWasRecentlyAudible(
      bool was_audible,
      const base::TimeTicks& last_became_silent_time) {
    EXPECT_EQ(was_audible, monitor_->indicator_is_on_);
    EXPECT_EQ(last_became_silent_time, monitor_->last_became_silent_time_);
    EXPECT_EQ(monitor_->off_timer_.IsRunning(),
              monitor_->indicator_is_on_ && !monitor_->IsCurrentlyAudible() &&
                  base::TimeTicks::Now() <
                      monitor_->last_became_silent_time_ + holding_period());
  }

  void ExpectIsCurrentlyAudible() const {
    EXPECT_TRUE(monitor_->IsCurrentlyAudible());
  }

  void ExpectNotCurrentlyAudible() const {
    EXPECT_FALSE(monitor_->IsCurrentlyAudible());
  }

  void ExpectRecentlyAudibleChangeNotification(bool new_recently_audible) {
    EXPECT_CALL(
        mock_web_contents_delegate_,
        NavigationStateChanged(RenderViewHostTestHarness::web_contents(),
                               INVALIDATE_TYPE_AUDIO))
        .WillOnce(InvokeWithoutArgs(
            this, new_recently_audible
                      ? &AudioStreamMonitorTest::ExpectWasRecentlyAudible
                      : &AudioStreamMonitorTest::ExpectNotRecentlyAudible))
        .RetiresOnSaturation();
  }

  void ExpectCurrentlyAudibleChangeNotification(bool new_audible) {
    EXPECT_CALL(
        mock_web_contents_delegate_,
        NavigationStateChanged(RenderViewHostTestHarness::web_contents(),
                               INVALIDATE_TYPE_AUDIO))
        .WillOnce(InvokeWithoutArgs(
            this, new_audible
                      ? &AudioStreamMonitorTest::ExpectIsCurrentlyAudible
                      : &AudioStreamMonitorTest::ExpectNotCurrentlyAudible))
        .RetiresOnSaturation();
  }

  // A small time step useful for testing the passage of time.
  static base::TimeDelta one_time_step() { return base::Seconds(1) / 15; }

  static base::TimeDelta holding_period() {
    return base::Milliseconds(
        std::to_underlying(AudioStreamMonitor::kHoldOnMilliseconds));
  }

  void StartMonitoring(GlobalRenderFrameHostId render_frame_host_id,
                       int stream_id) {
    monitor_->StartMonitoringStreamOnUIThread(
        AudioStreamMonitor::StreamID{render_frame_host_id, stream_id});
  }

  void StopMonitoring(GlobalRenderFrameHostId render_frame_host_id,
                      int stream_id) {
    monitor_->StopMonitoringStreamOnUIThread(
        AudioStreamMonitor::StreamID{render_frame_host_id, stream_id});
  }

  void UpdateAudibleState(GlobalRenderFrameHostId render_frame_host_id,
                          int stream_id,
                          bool is_audible) {
    monitor_->UpdateStreamAudibleStateOnUIThread(
        AudioStreamMonitor::StreamID{render_frame_host_id, stream_id},
        is_audible);
  }

  WebContents* web_contents() { return monitor_->web_contents_; }

 protected:
  raw_ptr<AudioStreamMonitor> monitor_;

 private:
  void ExpectWasRecentlyAudible() const {
    EXPECT_TRUE(monitor_->WasRecentlyAudible());
  }

  void ExpectNotRecentlyAudible() const {
    EXPECT_FALSE(monitor_->WasRecentlyAudible());
  }

  MockWebContentsDelegate mock_web_contents_delegate_;
};

TEST_F(AudioStreamMonitorTest, MonitorsWhenProvidedAStream) {
  EXPECT_FALSE(monitor_->WasRecentlyAudible());
  ExpectNotCurrentlyAudible();
  ExpectIsMonitoring(kRenderFrameHostId, kStreamId, false);

  StartMonitoring(kRenderFrameHostId, kStreamId);
  EXPECT_FALSE(monitor_->WasRecentlyAudible());
  ExpectNotCurrentlyAudible();
  ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);

  StopMonitoring(kRenderFrameHostId, kStreamId);
  EXPECT_FALSE(monitor_->WasRecentlyAudible());
  ExpectNotCurrentlyAudible();
  ExpectIsMonitoring(kRenderFrameHostId, kStreamId, false);
}

// Tests that AudioStreamMonitor keeps the indicator on for the holding period
// even if there is silence during the holding period.
// See comments in audio_stream_monitor.h for expected behavior.
TEST_F(AudioStreamMonitorTest, IndicatorIsOnUntilHoldingPeriodHasPassed) {
  StartMonitoring(kRenderFrameHostId, kStreamId);

  // Expect WebContents will get one call form AudioStreamMonitor to toggle the
  // indicator upon the very first notification that the stream has become
  // audible.
  ExpectRecentlyAudibleChangeNotification(true);

  // Loop, each time testing a slightly longer period of silence.  The recently
  // audible state should not change while the currently audible one should.
  int num_silence_steps = 1;
  base::TimeTicks last_became_silent_time;
  do {
    ExpectCurrentlyAudibleChangeNotification(true);

    UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
    ExpectTabWasRecentlyAudible(true, last_became_silent_time);
    FastForwardBy(one_time_step());

    ExpectCurrentlyAudibleChangeNotification(false);

    // Notify that the stream has become silent and advance time repeatedly,
    // ensuring that the indicator is being held on during the holding period.
    UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
    last_became_silent_time = base::TimeTicks::Now();
    ExpectTabWasRecentlyAudible(true, last_became_silent_time);
    for (int i = 0; i < num_silence_steps; ++i) {
      // If the next time step will cause the holding period to expire, then a
      // notification will be sent.
      if (base::TimeTicks::Now() + one_time_step() >=
          last_became_silent_time + holding_period()) {
        ExpectRecentlyAudibleChangeNotification(false);
      }

      ExpectTabWasRecentlyAudible(true, last_became_silent_time);
      FastForwardBy(one_time_step());
    }

    ++num_silence_steps;
  } while (base::TimeTicks::Now() < last_became_silent_time + holding_period());

  // At this point, the time has just advanced to beyond the holding period and
  // the tab indicator has been turned off. Also, make sure it stays off for
  // several cycles thereafter.
  for (int i = 0; i < 10; ++i) {
    ExpectTabWasRecentlyAudible(false, last_became_silent_time);
    FastForwardBy(one_time_step());
  }
}

// Tests that the AudioStreamMonitor correctly processes updates from two
// different streams in the same tab.
TEST_F(AudioStreamMonitorTest, HandlesMultipleStreamUpdate) {
  StartMonitoring(kRenderFrameHostId, kStreamId);
  StartMonitoring(kRenderFrameHostIdSameProcess, kAnotherStreamId);

  base::TimeTicks last_became_silent_time;
  ExpectTabWasRecentlyAudible(false, last_became_silent_time);
  ExpectNotCurrentlyAudible();

  // The first stream becomes audible and the second stream is silent. The tab
  // becomes audible.
  ExpectRecentlyAudibleChangeNotification(true);
  ExpectCurrentlyAudibleChangeNotification(true);

  UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
  UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, false);
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectIsCurrentlyAudible();

  // Halfway through the holding period, the second stream joins in.  The
  // indicator stays on.
  FastForwardBy(holding_period() / 2);
  UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, true);
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectIsCurrentlyAudible();

  // Now, both streams become silent. The tab becoms silent but the indicator
  // stays on.
  ExpectCurrentlyAudibleChangeNotification(false);
  UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
  UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, false);
  last_became_silent_time = base::TimeTicks::Now();
  ExpectNotCurrentlyAudible();
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);

  // Advance half a holding period and the indicator should still be on.
  FastForwardBy(holding_period() / 2);
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectNotCurrentlyAudible();

  // The first stream becomes audible again during the holding period.
  // The tab becomes audible and the indicator stays on.
  ExpectCurrentlyAudibleChangeNotification(true);
  UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectIsCurrentlyAudible();

  // Advance a holding period. The original holding period has expired but the
  // indicator should stay on because a stream became audible in the meantime.
  FastForwardBy(holding_period() / 2);
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectIsCurrentlyAudible();

  // The first stream becomes silent again. The tab becomes silent and the
  // indicator is still on.
  ExpectCurrentlyAudibleChangeNotification(false);
  UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
  last_became_silent_time = base::TimeTicks::Now();
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectNotCurrentlyAudible();

  // After a holding period passes, the indicator turns off.
  ExpectRecentlyAudibleChangeNotification(false);
  FastForwardBy(holding_period());
  ExpectTabWasRecentlyAudible(false, last_became_silent_time);
  ExpectNotCurrentlyAudible();

  // Now, the second stream becomes audible and the first one remains silent.
  // The tab becomes audible again.
  ExpectRecentlyAudibleChangeNotification(true);
  ExpectCurrentlyAudibleChangeNotification(true);
  UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, true);
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectIsCurrentlyAudible();

  // From here onwards, both streams are silent.  Halfway through the holding
  // period, the tab is no longer audible but stays as recently audible.
  ExpectCurrentlyAudibleChangeNotification(false);
  UpdateAudibleState(kRenderFrameHostIdSameProcess, kAnotherStreamId, false);
  last_became_silent_time = base::TimeTicks::Now();
  FastForwardBy(holding_period() / 2);
  ExpectTabWasRecentlyAudible(true, last_became_silent_time);
  ExpectNotCurrentlyAudible();

  // Just past the holding period, the tab is no longer marked as recently
  // audible.
  ExpectRecentlyAudibleChangeNotification(false);
  FastForwardBy(holding_period() -
                (base::TimeTicks::Now() - last_became_silent_time));
  ExpectTabWasRecentlyAudible(false, last_became_silent_time);
  ExpectNotCurrentlyAudible();

  // The passage of time should not turn the indicator back while both streams
  // are remaining silent.
  for (int i = 0; i < 100; ++i) {
    FastForwardBy(one_time_step());
    ExpectTabWasRecentlyAudible(false, last_became_silent_time);
    ExpectNotCurrentlyAudible();
  }
}

TEST_F(AudioStreamMonitorTest, MultipleRendererProcesses) {
  StartMonitoring(kRenderFrameHostId, kStreamId);
  StartMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
  ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);
  ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);
  StopMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
  ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);
  ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, false);
}

TEST_F(AudioStreamMonitorTest, RenderProcessGone) {
  StartMonitoring(kRenderFrameHostId, kStreamId);
  StartMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
  ExpectIsMonitoring(kRenderFrameHostId, kStreamId, true);
  ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);
  monitor_->RenderProcessGone(kRenderFrameHostId.child_id);
  ExpectIsMonitoring(kRenderFrameHostId, kStreamId, false);
  monitor_->RenderProcessGone(kRenderFrameHostIdOtherProcess.child_id);
  ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, false);
}

TEST_F(AudioStreamMonitorTest, RenderFrameGone) {
  RenderFrameHost* render_frame_host = web_contents()->GetPrimaryMainFrame();
  GlobalRenderFrameHostId render_frame_host_id =
      render_frame_host->GetGlobalId();

  StartMonitoring(render_frame_host_id, kStreamId);
  StartMonitoring(kRenderFrameHostIdOtherProcess, kStreamId);
  ExpectIsMonitoring(render_frame_host_id, kStreamId, true);
  ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);

  monitor_->RenderFrameDeleted(render_frame_host);
  ExpectIsMonitoring(render_frame_host_id, kStreamId, false);
  ExpectIsMonitoring(kRenderFrameHostIdOtherProcess, kStreamId, true);
}

TEST_F(AudioStreamMonitorTest, OneAudibleClient) {
  ExpectNotCurrentlyAudible();

  auto* render_frame_host_impl =
      static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame());
  GlobalRenderFrameHostId host_id = render_frame_host_impl->GetGlobalId();

  ExpectRecentlyAudibleChangeNotification(true);
  ExpectCurrentlyAudibleChangeNotification(true);
  auto registration = monitor_->RegisterAudibleClient(host_id);
  ExpectIsCurrentlyAudible();

  ExpectCurrentlyAudibleChangeNotification(false);
  registration.reset();
  ExpectNotCurrentlyAudible();
}

TEST_F(AudioStreamMonitorTest, MultipleAudibleClients) {
  ExpectNotCurrentlyAudible();

  auto* render_frame_host_impl =
      static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame());
  GlobalRenderFrameHostId host_id = render_frame_host_impl->GetGlobalId();

  // Add one client and the tab becomes audible.
  ExpectRecentlyAudibleChangeNotification(true);
  ExpectCurrentlyAudibleChangeNotification(true);
  auto registration1 = monitor_->RegisterAudibleClient(host_id);
  ExpectIsCurrentlyAudible();

  // Add another client and the tab remains audible.
  auto registration2 = monitor_->RegisterAudibleClient(host_id);
  ExpectIsCurrentlyAudible();

  // Removes one client and the tab remains audible.
  registration1.reset();
  ExpectIsCurrentlyAudible();

  // Removes another client and the tab is not audible.
  ExpectCurrentlyAudibleChangeNotification(false);
  registration2.reset();
  ExpectNotCurrentlyAudible();
}

TEST_F(AudioStreamMonitorTest, MultipleAudibleClientsMultipleRenderFrames) {
  ExpectNotCurrentlyAudible();
  // We need to navigate once before we can add child frames.
  const char kDefaultTestUrl[] = "https://google.com/";
  content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents(), GURL(kDefaultTestUrl));

  auto* render_frame_host_tester =
      RenderFrameHostTester::For(web_contents()->GetPrimaryMainFrame());

  auto* render_frame_host_impl_1 = static_cast<RenderFrameHostImpl*>(
      render_frame_host_tester->AppendChild("child_1"));
  auto* render_frame_host_impl_2 = static_cast<RenderFrameHostImpl*>(
      render_frame_host_tester->AppendChild("child_2"));

  GlobalRenderFrameHostId host_id1 = render_frame_host_impl_1->GetGlobalId();
  GlobalRenderFrameHostId host_id2 = render_frame_host_impl_2->GetGlobalId();

  // Add one client and the tab becomes audible.
  ExpectRecentlyAudibleChangeNotification(true);
  ExpectCurrentlyAudibleChangeNotification(true);
  auto registration1 = monitor_->RegisterAudibleClient(host_id1);
  ExpectIsCurrentlyAudible();

  // Add another client and the tab remains audible.
  auto registration2 = monitor_->RegisterAudibleClient(host_id2);
  ExpectIsCurrentlyAudible();

  // Removes one client and the tab remains audible.
  registration1.reset();
  ExpectIsCurrentlyAudible();

  // Removes another client and the tab is not audible.
  ExpectCurrentlyAudibleChangeNotification(false);
  registration2.reset();
  ExpectNotCurrentlyAudible();
}

TEST_F(AudioStreamMonitorTest, AudibleClientAndStream) {
  StartMonitoring(kRenderFrameHostId, kStreamId);
  ExpectNotCurrentlyAudible();

  auto* render_frame_host_impl =
      static_cast<RenderFrameHostImpl*>(web_contents()->GetPrimaryMainFrame());
  GlobalRenderFrameHostId host_id = render_frame_host_impl->GetGlobalId();

  // Add one client and the tab becomes audible.
  ExpectRecentlyAudibleChangeNotification(true);
  ExpectCurrentlyAudibleChangeNotification(true);
  auto registration = monitor_->RegisterAudibleClient(host_id);
  ExpectIsCurrentlyAudible();

  // The stream becomes audible and the tab remains audible.
  UpdateAudibleState(kRenderFrameHostId, kStreamId, true);
  ExpectIsCurrentlyAudible();

  // Remove the client and the tab remains audible.
  registration.reset();
  ExpectIsCurrentlyAudible();

  // The stream becomes not audible and the tab is not audible.
  ExpectCurrentlyAudibleChangeNotification(false);
  UpdateAudibleState(kRenderFrameHostId, kStreamId, false);
  ExpectNotCurrentlyAudible();
}

}  // namespace content
