// Copyright 2018 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 "content/browser/media/audio_loopback_stream_broker.h"

#include <memory>
#include <utility>

#include "base/sync_socket.h"
#include "base/test/mock_callback.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "media/mojo/interfaces/audio_input_stream.mojom.h"
#include "mojo/public/cpp/system/platform_handle.h"
#include "services/audio/public/cpp/fake_stream_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::Test;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::StrictMock;
using ::testing::InSequence;

namespace content {

namespace {

const int kRenderProcessId = 123;
const int kRenderFrameId = 234;
const uint32_t kShMemCount = 10;

media::AudioParameters TestParams() {
  return media::AudioParameters::UnavailableDeviceParams();
}

class MockSource : public AudioStreamBrokerFactory::LoopbackSource {
 public:
  explicit MockSource(const base::UnguessableToken& group_id)
      : group_id_(group_id) {}
  ~MockSource() override {}

  // AudioStreamBrokerFactory::LoopbackSource mocking.
  base::UnguessableToken GetGroupID() override { return group_id_; }
  MOCK_METHOD0(OnStartCapturing, void(void));
  MOCK_METHOD0(OnStopCapturing, void(void));

 private:
  base::UnguessableToken group_id_;
  DISALLOW_COPY_AND_ASSIGN(MockSource);
};

using MockDeleterCallback = StrictMock<
    base::MockCallback<base::OnceCallback<void(AudioStreamBroker*)>>>;

class MockRendererAudioInputStreamFactoryClient
    : public mojom::RendererAudioInputStreamFactoryClient {
 public:
  MockRendererAudioInputStreamFactoryClient() : binding_(this) {}
  ~MockRendererAudioInputStreamFactoryClient() override {}

  mojom::RendererAudioInputStreamFactoryClientPtr MakePtr() {
    mojom::RendererAudioInputStreamFactoryClientPtr ret;
    binding_.Bind(mojo::MakeRequest(&ret));
    return ret;
  }

  MOCK_METHOD0(OnStreamCreated, void());

  void StreamCreated(
      media::mojom::AudioInputStreamPtr input_stream,
      media::mojom::AudioInputStreamClientRequest client_request,
      media::mojom::ReadOnlyAudioDataPipePtr data_pipe,
      bool initially_muted,
      const base::Optional<base::UnguessableToken>& stream_id) override {
    // Loopback streams have no stream ids.
    EXPECT_FALSE(stream_id.has_value());
    input_stream_ = std::move(input_stream);
    client_request_ = std::move(client_request);
    OnStreamCreated();
  }

  void CloseBinding() { binding_.Close(); }

 private:
  mojo::Binding<mojom::RendererAudioInputStreamFactoryClient> binding_;
  media::mojom::AudioInputStreamPtr input_stream_;
  media::mojom::AudioInputStreamClientRequest client_request_;
};

class MockStreamFactory : public audio::FakeStreamFactory {
 public:
  MockStreamFactory() {}
  ~MockStreamFactory() final {}

  // State of an expected stream creation. |device_id| and |params| are set
  // ahead of time and verified during request. The other fields are filled in
  // when the request is received.
  struct StreamRequestData {
    StreamRequestData(const base::UnguessableToken& group_id,
                      const media::AudioParameters& params)
        : params(params), group_id(group_id) {}

    bool requested = false;
    media::mojom::AudioInputStreamRequest stream_request;
    media::mojom::AudioInputStreamClientPtr client;
    media::mojom::AudioInputStreamObserverPtr observer;
    const media::AudioParameters params;
    uint32_t shared_memory_count;
    base::UnguessableToken group_id;
    mojo::ScopedSharedBufferHandle key_press_count_buffer;
    CreateLoopbackStreamCallback created_callback;
    audio::mojom::LocalMuterAssociatedRequest muter_request;
  };

  void ExpectStreamCreation(StreamRequestData* ex) {
    stream_request_data_ = ex;
  }

  MOCK_METHOD1(IsMuting, void(const base::UnguessableToken&));

 private:
  void CreateLoopbackStream(
      media::mojom::AudioInputStreamRequest stream_request,
      media::mojom::AudioInputStreamClientPtr client,
      media::mojom::AudioInputStreamObserverPtr observer,
      const media::AudioParameters& params,
      uint32_t shared_memory_count,
      const base::UnguessableToken& group_id,
      CreateLoopbackStreamCallback created_callback) final {
    // No way to cleanly exit the test here in case of failure, so use CHECK.
    CHECK(stream_request_data_);
    EXPECT_EQ(stream_request_data_->group_id, group_id);
    EXPECT_TRUE(stream_request_data_->params.Equals(params));
    stream_request_data_->requested = true;
    stream_request_data_->stream_request = std::move(stream_request);
    stream_request_data_->client = std::move(client);
    stream_request_data_->observer = std::move(observer);
    stream_request_data_->shared_memory_count = shared_memory_count;
    stream_request_data_->created_callback = std::move(created_callback);
  }

  void BindMuter(audio::mojom::LocalMuterAssociatedRequest request,
                 const base::UnguessableToken& group_id) final {
    stream_request_data_->muter_request = std::move(request);
    IsMuting(group_id);
  }

  StreamRequestData* stream_request_data_;

  DISALLOW_COPY_AND_ASSIGN(MockStreamFactory);
};

const bool kMuteSource = true;

struct TestEnvironment {
  TestEnvironment(const base::UnguessableToken& group_id, bool mute_source) {
    // Muting should not start until CreateStream() is called.
    EXPECT_CALL(stream_factory, IsMuting(_)).Times(0);
    auto mock_source = std::make_unique<NiceMock<MockSource>>(group_id);
    source = mock_source.get();
    broker = std::make_unique<AudioLoopbackStreamBroker>(
        kRenderProcessId, kRenderFrameId, std::move(mock_source), TestParams(),
        kShMemCount, mute_source, deleter.Get(),
        renderer_factory_client.MakePtr());
  }

  void RunUntilIdle() { thread_bundle.RunUntilIdle(); }

  TestBrowserThreadBundle thread_bundle;
  MockDeleterCallback deleter;
  MockSource* source;
  StrictMock<MockRendererAudioInputStreamFactoryClient> renderer_factory_client;
  std::unique_ptr<AudioLoopbackStreamBroker> broker;
  MockStreamFactory stream_factory;
  audio::mojom::StreamFactoryPtr factory_ptr = stream_factory.MakePtr();
};

}  // namespace

TEST(AudioLoopbackStreamBrokerTest, StoresProcessAndFrameId) {
  InSequence seq;
  TestBrowserThreadBundle thread_bundle;
  MockDeleterCallback deleter;
  StrictMock<MockRendererAudioInputStreamFactoryClient> renderer_factory_client;
  auto source = std::make_unique<StrictMock<MockSource>>(
      base::UnguessableToken::Create());
  MockSource* mock_source = source.get();

  EXPECT_CALL(*mock_source, OnStartCapturing());

  AudioLoopbackStreamBroker broker(kRenderProcessId, kRenderFrameId,
                                   std::move(source), TestParams(), kShMemCount,
                                   !kMuteSource, deleter.Get(),
                                   renderer_factory_client.MakePtr());

  EXPECT_EQ(kRenderProcessId, broker.render_process_id());
  EXPECT_EQ(kRenderFrameId, broker.render_frame_id());

  EXPECT_CALL(*mock_source, OnStopCapturing());
}

TEST(AudioLoopbackStreamBrokerTest, StreamCreationSuccess_Propagates) {
  TestEnvironment env(base::UnguessableToken::Create(), !kMuteSource);
  MockStreamFactory::StreamRequestData stream_request_data(
      env.source->GetGroupID(), TestParams());
  env.stream_factory.ExpectStreamCreation(&stream_request_data);

  EXPECT_CALL(env.stream_factory, IsMuting(_)).Times(0);

  env.broker->CreateStream(env.factory_ptr.get());
  env.RunUntilIdle();

  EXPECT_TRUE(stream_request_data.requested);

  // Set up test IPC primitives.
  const size_t shmem_size = 456;
  base::SyncSocket socket1, socket2;
  base::SyncSocket::CreatePair(&socket1, &socket2);
  std::move(stream_request_data.created_callback)
      .Run({base::in_place,
            base::ReadOnlySharedMemoryRegion::Create(shmem_size).region,
            mojo::WrapPlatformFile(socket1.Release())});

  EXPECT_CALL(env.renderer_factory_client, OnStreamCreated());

  env.RunUntilIdle();

  Mock::VerifyAndClear(&env.renderer_factory_client);
  env.broker.reset();
}

TEST(AudioLoopbackStreamBrokerTest, MutedStreamCreation_Mutes) {
  TestEnvironment env(base::UnguessableToken::Create(), kMuteSource);
  MockStreamFactory::StreamRequestData stream_request_data(
      env.source->GetGroupID(), TestParams());
  env.stream_factory.ExpectStreamCreation(&stream_request_data);

  EXPECT_CALL(env.stream_factory, IsMuting(env.source->GetGroupID()));

  env.broker->CreateStream(env.factory_ptr.get());
  env.RunUntilIdle();

  EXPECT_TRUE(stream_request_data.requested);

  // Set up test IPC primitives.
  const size_t shmem_size = 456;
  base::SyncSocket socket1, socket2;
  base::SyncSocket::CreatePair(&socket1, &socket2);
  std::move(stream_request_data.created_callback)
      .Run({base::in_place,
            base::ReadOnlySharedMemoryRegion::Create(shmem_size).region,
            mojo::WrapPlatformFile(socket1.Release())});

  EXPECT_CALL(env.renderer_factory_client, OnStreamCreated());

  env.RunUntilIdle();

  Mock::VerifyAndClear(&env.renderer_factory_client);
  env.broker.reset();
}

TEST(AudioLoopbackStreamBrokerTest, SourceGone_CallsDeleter) {
  TestEnvironment env(base::UnguessableToken::Create(), kMuteSource);
  MockStreamFactory::StreamRequestData stream_request_data(
      env.source->GetGroupID(), TestParams());
  env.stream_factory.ExpectStreamCreation(&stream_request_data);

  EXPECT_CALL(env.stream_factory, IsMuting(env.source->GetGroupID()));

  env.broker->CreateStream(env.factory_ptr.get());
  env.RunUntilIdle();

  EXPECT_TRUE(stream_request_data.requested);

  // Set up test IPC primitives.
  const size_t shmem_size = 456;
  base::SyncSocket socket1, socket2;
  base::SyncSocket::CreatePair(&socket1, &socket2);
  std::move(stream_request_data.created_callback)
      .Run({base::in_place,
            base::ReadOnlySharedMemoryRegion::Create(shmem_size).region,
            mojo::WrapPlatformFile(socket1.Release())});

  EXPECT_CALL(env.renderer_factory_client, OnStreamCreated());

  env.RunUntilIdle();

  Mock::VerifyAndClear(&env.renderer_factory_client);

  EXPECT_CALL(env.deleter, Run(env.broker.release()))
      .WillOnce(testing::DeleteArg<0>());

  env.source->WebContentsDestroyed();

  env.RunUntilIdle();
}

TEST(AudioLoopbackStreamBrokerTest, StreamCreationFailure_CallsDeleter) {
  TestEnvironment env(base::UnguessableToken::Create(), !kMuteSource);
  MockStreamFactory::StreamRequestData stream_request_data(
      env.source->GetGroupID(), TestParams());
  env.stream_factory.ExpectStreamCreation(&stream_request_data);

  EXPECT_CALL(env.stream_factory, IsMuting(_)).Times(0);

  env.broker->CreateStream(env.factory_ptr.get());
  env.RunUntilIdle();

  EXPECT_TRUE(stream_request_data.requested);
  EXPECT_CALL(env.deleter, Run(env.broker.release()))
      .WillOnce(testing::DeleteArg<0>());

  std::move(stream_request_data.created_callback).Run(nullptr);

  env.RunUntilIdle();
}

TEST(AudioLoopbackStreamBrokerTest,
     RendererFactoryClientDisconnect_CallsDeleter) {
  TestEnvironment env(base::UnguessableToken::Create(), !kMuteSource);
  MockStreamFactory::StreamRequestData stream_request_data(
      env.source->GetGroupID(), TestParams());
  env.stream_factory.ExpectStreamCreation(&stream_request_data);

  EXPECT_CALL(env.stream_factory, IsMuting(_)).Times(0);

  env.broker->CreateStream(env.factory_ptr.get());
  env.RunUntilIdle();
  EXPECT_TRUE(stream_request_data.requested);

  EXPECT_CALL(env.deleter, Run(env.broker.release()))
      .WillOnce(testing::DeleteArg<0>());
  env.renderer_factory_client.CloseBinding();
  env.RunUntilIdle();
  Mock::VerifyAndClear(&env.deleter);

  env.stream_factory.CloseBinding();
  env.RunUntilIdle();
}

TEST(AudioLoopbackStreamBrokerTest, ObserverDisconnect_CallsDeleter) {
  TestEnvironment env(base::UnguessableToken::Create(), !kMuteSource);
  MockStreamFactory::StreamRequestData stream_request_data(
      env.source->GetGroupID(), TestParams());
  env.stream_factory.ExpectStreamCreation(&stream_request_data);

  EXPECT_CALL(env.stream_factory, IsMuting(_)).Times(0);

  env.broker->CreateStream(env.factory_ptr.get());
  env.RunUntilIdle();
  EXPECT_TRUE(stream_request_data.requested);

  EXPECT_CALL(env.deleter, Run(env.broker.release()))
      .WillOnce(testing::DeleteArg<0>());
  stream_request_data.observer.reset();
  env.RunUntilIdle();
  Mock::VerifyAndClear(&env.deleter);

  env.stream_factory.CloseBinding();
  env.RunUntilIdle();
}

TEST(AudioLoopbackStreamBrokerTest,
     FactoryDisconnectDuringConstruction_CallsDeleter) {
  TestEnvironment env(base::UnguessableToken::Create(), !kMuteSource);
  env.broker->CreateStream(env.factory_ptr.get());
  env.stream_factory.CloseBinding();

  EXPECT_CALL(env.deleter, Run(env.broker.release()))
      .WillOnce(testing::DeleteArg<0>());

  env.RunUntilIdle();
}

}  // namespace content
