// Copyright (c) 2011 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/media_internals.h"

#include <stddef.h>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/json/json_reader.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "media/base/audio_parameters.h"
#include "media/base/channel_layout.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"

namespace {
const int kTestComponentID = 0;
const char kTestDeviceID[] = "test-device-id";

// This class encapsulates a MediaInternals reference. It also has some useful
// methods to receive a callback, deserialize its associated data and expect
// integer/string values.
class MediaInternalsTestBase {
 public:
  MediaInternalsTestBase()
    : media_internals_(content::MediaInternals::GetInstance()) {
  }
  virtual ~MediaInternalsTestBase() {}

 protected:
  // Extracts and deserializes the JSON update data; merges into |update_data_|.
  void UpdateCallbackImpl(const base::string16& update) {
    // Each update string looks like "<JavaScript Function Name>({<JSON>});"
    // or for video capabilities: "<JavaScript Function Name>([{<JSON>}]);".
    // In the second case we will be able to extract the dictionary if it is the
    // only member of the list.
    // To use the JSON reader we need to strip out the JS function name and ().
    std::string utf8_update = base::UTF16ToUTF8(update);
    const std::string::size_type first_brace = utf8_update.find('{');
    const std::string::size_type last_brace = utf8_update.rfind('}');
    std::unique_ptr<base::Value> output_value = base::JSONReader::Read(
        utf8_update.substr(first_brace, last_brace - first_brace + 1));
    CHECK(output_value);

    base::DictionaryValue* output_dict = NULL;
    CHECK(output_value->GetAsDictionary(&output_dict));
    update_data_.MergeDictionary(output_dict);
  }

  void ExpectInt(const std::string& key, int expected_value) const {
    int actual_value = 0;
    ASSERT_TRUE(update_data_.GetInteger(key, &actual_value));
    EXPECT_EQ(expected_value, actual_value);
  }

  void ExpectString(const std::string& key,
                    const std::string& expected_value) const {
    std::string actual_value;
    ASSERT_TRUE(update_data_.GetString(key, &actual_value));
    EXPECT_EQ(expected_value, actual_value);
  }

  void ExpectStatus(const std::string& expected_value) const {
    ExpectString("status", expected_value);
  }

  void ExpectListOfStrings(const std::string& key,
                           const base::ListValue& expected_list) const {
    const base::ListValue* actual_list;
    ASSERT_TRUE(update_data_.GetList(key, &actual_list));
    const size_t expected_size = expected_list.GetSize();
    const size_t actual_size = actual_list->GetSize();
    ASSERT_EQ(expected_size, actual_size);
    for (size_t i = 0; i < expected_size; ++i) {
      std::string expected_value, actual_value;
      ASSERT_TRUE(expected_list.GetString(i, &expected_value));
      ASSERT_TRUE(actual_list->GetString(i, &actual_value));
      EXPECT_EQ(expected_value, actual_value);
    }
  }

  const content::TestBrowserThreadBundle thread_bundle_;
  base::DictionaryValue update_data_;
  content::MediaInternals* const media_internals_;
};

}  // namespace

namespace content {

class MediaInternalsVideoCaptureDeviceTest : public testing::Test,
                                             public MediaInternalsTestBase {
 public:
  MediaInternalsVideoCaptureDeviceTest()
      : update_cb_(base::Bind(
            &MediaInternalsVideoCaptureDeviceTest::UpdateCallbackImpl,
            base::Unretained(this))) {
    media_internals_->AddUpdateCallback(update_cb_);
  }

  ~MediaInternalsVideoCaptureDeviceTest() override {
    media_internals_->RemoveUpdateCallback(update_cb_);
  }

 protected:
  MediaInternals::UpdateCallback update_cb_;
};

// TODO(chfremer): Consider removing this. This test seem be
// a duplicate implementation of the functionality under test.
// https://crbug.com/630694
#if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_LINUX) || \
    defined(OS_ANDROID)
TEST_F(MediaInternalsVideoCaptureDeviceTest,
       AllCaptureApiTypesHaveProperStringRepresentation) {
  using VideoCaptureApi = media::VideoCaptureApi;
  std::map<VideoCaptureApi, std::string> api_to_string_map;
  api_to_string_map[VideoCaptureApi::LINUX_V4L2_SINGLE_PLANE] = "V4L2 SPLANE";
  api_to_string_map[VideoCaptureApi::WIN_MEDIA_FOUNDATION] = "Media Foundation";
  api_to_string_map[VideoCaptureApi::WIN_DIRECT_SHOW] = "Direct Show";
  api_to_string_map[VideoCaptureApi::MACOSX_AVFOUNDATION] = "AV Foundation";
  api_to_string_map[VideoCaptureApi::MACOSX_DECKLINK] = "DeckLink";
  api_to_string_map[VideoCaptureApi::ANDROID_API1] = "Camera API1";
  api_to_string_map[VideoCaptureApi::ANDROID_API2_LEGACY] =
      "Camera API2 Legacy";
  api_to_string_map[VideoCaptureApi::ANDROID_API2_FULL] = "Camera API2 Full";
  api_to_string_map[VideoCaptureApi::ANDROID_API2_LIMITED] =
      "Camera API2 Limited";
  api_to_string_map[VideoCaptureApi::ANDROID_TANGO] = "Tango API";
  EXPECT_EQ(static_cast<size_t>(VideoCaptureApi::UNKNOWN),
            api_to_string_map.size());
  for (const auto& map_entry : api_to_string_map) {
    media::VideoCaptureDeviceDescriptor descriptor;
    descriptor.capture_api = map_entry.first;
    EXPECT_EQ(map_entry.second, descriptor.GetCaptureApiTypeString());
  }
}
#endif

TEST_F(MediaInternalsVideoCaptureDeviceTest,
       VideoCaptureFormatStringIsInExpectedFormat) {
  // Since media internals will send video capture capabilities to JavaScript in
  // an expected format and there are no public methods for accessing the
  // resolutions, frame rates or pixel formats, this test checks that the format
  // has not changed. If the test fails because of the changed format, it should
  // be updated at the same time as the media internals JS files.
  const float kFrameRate = 30.0f;
  const gfx::Size kFrameSize(1280, 720);
  const media::VideoPixelFormat kPixelFormat =
      media::PIXEL_FORMAT_I420;
  const media::VideoPixelStorage kPixelStorage = media::PIXEL_STORAGE_CPU;
  const media::VideoCaptureFormat capture_format(kFrameSize, kFrameRate,
                                                 kPixelFormat, kPixelStorage);
  const std::string expected_string = base::StringPrintf(
      "(%s)@%.3ffps, pixel format: %s, storage: %s",
      kFrameSize.ToString().c_str(), kFrameRate,
      media::VideoPixelFormatToString(kPixelFormat).c_str(),
      media::VideoCaptureFormat::PixelStorageToString(kPixelStorage).c_str());
  EXPECT_EQ(expected_string,
            media::VideoCaptureFormat::ToString(capture_format));
}

TEST_F(MediaInternalsVideoCaptureDeviceTest,
       NotifyVideoCaptureDeviceCapabilitiesEnumerated) {
  const int kWidth = 1280;
  const int kHeight = 720;
  const float kFrameRate = 30.0f;
  const media::VideoPixelFormat kPixelFormat =
      media::PIXEL_FORMAT_I420;
  const media::VideoCaptureFormat format_hd({kWidth, kHeight},
      kFrameRate, kPixelFormat);
  media::VideoCaptureFormats formats{};
  formats.push_back(format_hd);
  media::VideoCaptureDeviceDescriptor descriptor;
  descriptor.device_id = "dummy";
  descriptor.display_name = "dummy";
#if defined(OS_MACOSX)
  descriptor.capture_api = media::VideoCaptureApi::MACOSX_AVFOUNDATION;
#elif defined(OS_WIN)
  descriptor.capture_api = media::VideoCaptureApi::WIN_DIRECT_SHOW;
#elif defined(OS_LINUX)
  descriptor.device_id = "/dev/dummy";
  descriptor.capture_api = media::VideoCaptureApi::LINUX_V4L2_SINGLE_PLANE;
#elif defined(OS_ANDROID)
  descriptor.capture_api = media::VideoCaptureApi::ANDROID_API2_LEGACY;
#endif
  std::vector<std::tuple<media::VideoCaptureDeviceDescriptor,
                         media::VideoCaptureFormats>>
      descriptors_and_formats{};
  descriptors_and_formats.push_back(std::make_tuple(descriptor, formats));

  // When updating video capture capabilities, the update will serialize
  // a JSON array of objects to string. So here, the |UpdateCallbackImpl| will
  // deserialize the first object in the array. This means we have to have
  // exactly one device_info in the |descriptors_and_formats|.
  media_internals_->UpdateVideoCaptureDeviceCapabilities(
      descriptors_and_formats);

#if defined(OS_LINUX)
  ExpectString("id", "/dev/dummy");
#else
  ExpectString("id", "dummy");
#endif
  ExpectString("name", "dummy");
  base::ListValue expected_list;
  expected_list.AppendString(media::VideoCaptureFormat::ToString(format_hd));
  ExpectListOfStrings("formats", expected_list);
#if defined(OS_LINUX)
  ExpectString("captureApi", "V4L2 SPLANE");
#elif defined(OS_WIN)
  ExpectString("captureApi", "Direct Show");
#elif defined(OS_MACOSX)
  ExpectString("captureApi", "AV Foundation");
#elif defined(OS_ANDROID)
  ExpectString("captureApi", "Camera API2 Legacy");
#endif
}

class MediaInternalsAudioLogTest
    : public MediaInternalsTestBase,
      public testing::TestWithParam<media::AudioLogFactory::AudioComponent> {
 public:
  MediaInternalsAudioLogTest()
      : update_cb_(base::Bind(&MediaInternalsAudioLogTest::UpdateCallbackImpl,
                              base::Unretained(this))),
        test_params_(MakeAudioParams()),
        test_component_(GetParam()),
        audio_log_(media_internals_->CreateAudioLog(test_component_)) {
    media_internals_->AddUpdateCallback(update_cb_);
  }

  virtual ~MediaInternalsAudioLogTest() {
    media_internals_->RemoveUpdateCallback(update_cb_);
  }

 protected:
  MediaInternals::UpdateCallback update_cb_;
  const media::AudioParameters test_params_;
  const media::AudioLogFactory::AudioComponent test_component_;
  std::unique_ptr<media::AudioLog> audio_log_;

 private:
  static media::AudioParameters MakeAudioParams() {
    media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LINEAR,
                                  media::CHANNEL_LAYOUT_MONO, 48000, 16, 128);
    params.set_effects(media::AudioParameters::ECHO_CANCELLER |
                       media::AudioParameters::DUCKING);
    return params;
  }
};

TEST_P(MediaInternalsAudioLogTest, AudioLogCreateStartStopErrorClose) {
  audio_log_->OnCreated(kTestComponentID, test_params_, kTestDeviceID);
  base::RunLoop().RunUntilIdle();

  ExpectString("channel_layout",
               media::ChannelLayoutToString(test_params_.channel_layout()));
  ExpectInt("sample_rate", test_params_.sample_rate());
  ExpectInt("frames_per_buffer", test_params_.frames_per_buffer());
  ExpectInt("channels", test_params_.channels());
  ExpectString("effects", "ECHO_CANCELLER | DUCKING");
  ExpectString("device_id", kTestDeviceID);
  ExpectInt("component_id", kTestComponentID);
  ExpectInt("component_type", test_component_);
  ExpectStatus("created");

  // Verify OnStarted().
  audio_log_->OnStarted(kTestComponentID);
  base::RunLoop().RunUntilIdle();
  ExpectStatus("started");

  // Verify OnStopped().
  audio_log_->OnStopped(kTestComponentID);
  base::RunLoop().RunUntilIdle();
  ExpectStatus("stopped");

  // Verify OnError().
  const char kErrorKey[] = "error_occurred";
  std::string no_value;
  ASSERT_FALSE(update_data_.GetString(kErrorKey, &no_value));
  audio_log_->OnError(kTestComponentID);
  base::RunLoop().RunUntilIdle();
  ExpectString(kErrorKey, "true");

  // Verify OnClosed().
  audio_log_->OnClosed(kTestComponentID);
  base::RunLoop().RunUntilIdle();
  ExpectStatus("closed");
}

TEST_P(MediaInternalsAudioLogTest, AudioLogCreateClose) {
  audio_log_->OnCreated(kTestComponentID, test_params_, kTestDeviceID);
  base::RunLoop().RunUntilIdle();
  ExpectStatus("created");

  audio_log_->OnClosed(kTestComponentID);
  base::RunLoop().RunUntilIdle();
  ExpectStatus("closed");
}

INSTANTIATE_TEST_CASE_P(
    MediaInternalsAudioLogTest, MediaInternalsAudioLogTest, testing::Values(
        media::AudioLogFactory::AUDIO_INPUT_CONTROLLER,
        media::AudioLogFactory::AUDIO_OUTPUT_CONTROLLER,
        media::AudioLogFactory::AUDIO_OUTPUT_STREAM));

}  // namespace content
