// Copyright 2014 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 "media/midi/midi_manager.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <vector>

#include "base/bind.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/scoped_vector.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/system_monitor/system_monitor.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace midi {

namespace {

using mojom::PortState;
using mojom::Result;

class FakeMidiManager : public MidiManager {
 public:
  FakeMidiManager()
      : start_initialization_is_called_(false), finalize_is_called_(false) {}
  ~FakeMidiManager() override {}

  // MidiManager implementation.
  void StartInitialization() override {
    start_initialization_is_called_ = true;
  }

  void Finalize() override { finalize_is_called_ = true; }

  void DispatchSendMidiData(MidiManagerClient* client,
                            uint32_t port_index,
                            const std::vector<uint8_t>& data,
                            double timestamp) override {}

  // Utility functions for testing.
  void CallCompleteInitialization(Result result) {
    CompleteInitialization(result);
  }

  size_t GetClientCount() const {
    return clients_size_for_testing();
  }

  size_t GetPendingClientCount() const {
    return pending_clients_size_for_testing();
  }

  bool start_initialization_is_called_;
  bool finalize_is_called_;

 private:
  DISALLOW_COPY_AND_ASSIGN(FakeMidiManager);
};

class FakeMidiManagerClient : public MidiManagerClient {
 public:
  FakeMidiManagerClient()
      : result_(Result::NOT_SUPPORTED), wait_for_result_(true) {}
  ~FakeMidiManagerClient() override {}

  // MidiManagerClient implementation.
  void AddInputPort(const MidiPortInfo& info) override {}
  void AddOutputPort(const MidiPortInfo& info) override {}
  void SetInputPortState(uint32_t port_index, PortState state) override {}
  void SetOutputPortState(uint32_t port_index, PortState state) override {}

  void CompleteStartSession(Result result) override {
    EXPECT_TRUE(wait_for_result_);
    result_ = result;
    wait_for_result_ = false;
  }

  void ReceiveMidiData(uint32_t port_index,
                       const uint8_t* data,
                       size_t size,
                       double timestamp) override {}
  void AccumulateMidiBytesSent(size_t size) override {}
  void Detach() override {}

  Result result() const { return result_; }

  Result WaitForResult() {
    while (wait_for_result_) {
      base::RunLoop run_loop;
      run_loop.RunUntilIdle();
    }
    return result();
  }

 private:
  Result result_;
  bool wait_for_result_;

  DISALLOW_COPY_AND_ASSIGN(FakeMidiManagerClient);
};

class MidiManagerTest : public ::testing::Test {
 public:
  MidiManagerTest()
      : manager_(new FakeMidiManager),
        message_loop_(new base::MessageLoop) {}
  ~MidiManagerTest() override {
    manager_->Shutdown();
    base::RunLoop run_loop;
    run_loop.RunUntilIdle();
    EXPECT_EQ(manager_->start_initialization_is_called_,
              manager_->finalize_is_called_);
  }

 protected:
  void StartTheFirstSession(FakeMidiManagerClient* client) {
    EXPECT_FALSE(manager_->start_initialization_is_called_);
    EXPECT_EQ(0U, manager_->GetClientCount());
    EXPECT_EQ(0U, manager_->GetPendingClientCount());
    manager_->StartSession(client);
    EXPECT_EQ(0U, manager_->GetClientCount());
    EXPECT_EQ(1U, manager_->GetPendingClientCount());
    EXPECT_TRUE(manager_->start_initialization_is_called_);
    EXPECT_EQ(0U, manager_->GetClientCount());
    EXPECT_EQ(1U, manager_->GetPendingClientCount());
    EXPECT_TRUE(manager_->start_initialization_is_called_);
  }

  void StartTheNthSession(FakeMidiManagerClient* client, size_t nth) {
    EXPECT_EQ(nth != 1, manager_->start_initialization_is_called_);
    EXPECT_EQ(0U, manager_->GetClientCount());
    EXPECT_EQ(nth - 1, manager_->GetPendingClientCount());

    // StartInitialization() should not be called for the second and later
    // sessions.
    manager_->start_initialization_is_called_ = false;
    manager_->StartSession(client);
    EXPECT_EQ(nth == 1, manager_->start_initialization_is_called_);
    manager_->start_initialization_is_called_ = true;
  }

  void EndSession(FakeMidiManagerClient* client, size_t before, size_t after) {
    EXPECT_EQ(before, manager_->GetClientCount());
    manager_->EndSession(client);
    EXPECT_EQ(after, manager_->GetClientCount());
  }

  void CompleteInitialization(Result result) {
    manager_->CallCompleteInitialization(result);
  }

  void RunLoopUntilIdle() {
    base::RunLoop run_loop;
    run_loop.RunUntilIdle();
  }

 protected:
  std::unique_ptr<FakeMidiManager> manager_;

 private:
  std::unique_ptr<base::MessageLoop> message_loop_;

  DISALLOW_COPY_AND_ASSIGN(MidiManagerTest);
};

TEST_F(MidiManagerTest, StartAndEndSession) {
  std::unique_ptr<FakeMidiManagerClient> client;
  client.reset(new FakeMidiManagerClient);

  StartTheFirstSession(client.get());
  CompleteInitialization(Result::OK);
  EXPECT_EQ(Result::OK, client->WaitForResult());
  EndSession(client.get(), 1U, 0U);
}

TEST_F(MidiManagerTest, StartAndEndSessionWithError) {
  std::unique_ptr<FakeMidiManagerClient> client;
  client.reset(new FakeMidiManagerClient);

  StartTheFirstSession(client.get());
  CompleteInitialization(Result::INITIALIZATION_ERROR);
  EXPECT_EQ(Result::INITIALIZATION_ERROR, client->WaitForResult());
  EndSession(client.get(), 0U, 0U);
}

TEST_F(MidiManagerTest, StartMultipleSessions) {
  std::unique_ptr<FakeMidiManagerClient> client1;
  std::unique_ptr<FakeMidiManagerClient> client2;
  std::unique_ptr<FakeMidiManagerClient> client3;
  client1.reset(new FakeMidiManagerClient);
  client2.reset(new FakeMidiManagerClient);
  client3.reset(new FakeMidiManagerClient);

  StartTheFirstSession(client1.get());
  StartTheNthSession(client2.get(), 2);
  StartTheNthSession(client3.get(), 3);
  CompleteInitialization(Result::OK);
  EXPECT_EQ(Result::OK, client1->WaitForResult());
  EXPECT_EQ(Result::OK, client2->WaitForResult());
  EXPECT_EQ(Result::OK, client3->WaitForResult());
  EndSession(client1.get(), 3U, 2U);
  EndSession(client2.get(), 2U, 1U);
  EndSession(client3.get(), 1U, 0U);
}

// TODO(toyoshim): Add a test for a MidiManagerClient that has multiple
// sessions with multiple client_id.

TEST_F(MidiManagerTest, TooManyPendingSessions) {
  // Push as many client requests for starting session as possible.
  ScopedVector<FakeMidiManagerClient> many_existing_clients;
  many_existing_clients.resize(MidiManager::kMaxPendingClientCount);
  for (size_t i = 0; i < MidiManager::kMaxPendingClientCount; ++i) {
    many_existing_clients[i] = new FakeMidiManagerClient;
    StartTheNthSession(many_existing_clients[i], i + 1);
  }
  EXPECT_TRUE(manager_->start_initialization_is_called_);

  // Push the last client that should be rejected for too many pending requests.
  std::unique_ptr<FakeMidiManagerClient> additional_client(
      new FakeMidiManagerClient);
  manager_->start_initialization_is_called_ = false;
  manager_->StartSession(additional_client.get());
  EXPECT_FALSE(manager_->start_initialization_is_called_);
  manager_->start_initialization_is_called_ = true;
  EXPECT_EQ(Result::INITIALIZATION_ERROR, additional_client->result());

  // Other clients still should not receive a result.
  RunLoopUntilIdle();
  for (size_t i = 0; i < many_existing_clients.size(); ++i)
    EXPECT_EQ(Result::NOT_SUPPORTED, many_existing_clients[i]->result());

  // The Result::OK should be distributed to other clients.
  CompleteInitialization(Result::OK);
  for (size_t i = 0; i < many_existing_clients.size(); ++i)
    EXPECT_EQ(Result::OK, many_existing_clients[i]->WaitForResult());

  // Close all successful sessions in FIFO order.
  size_t sessions = many_existing_clients.size();
  for (size_t i = 0; i < many_existing_clients.size(); ++i, --sessions)
    EndSession(many_existing_clients[i], sessions, sessions - 1);
}

TEST_F(MidiManagerTest, AbortSession) {
  // A client starting a session can be destructed while an asynchronous
  // initialization is performed.
  std::unique_ptr<FakeMidiManagerClient> client;
  client.reset(new FakeMidiManagerClient);

  StartTheFirstSession(client.get());
  EndSession(client.get(), 0, 0);
  client.reset();

  // Following function should not call the destructed |client| function.
  CompleteInitialization(Result::OK);
  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

TEST_F(MidiManagerTest, CreateMidiManager) {
  // SystemMonitor is needed on Windows.
  base::SystemMonitor system_monitor;

  std::unique_ptr<FakeMidiManagerClient> client;
  client.reset(new FakeMidiManagerClient);

  std::unique_ptr<MidiManager> manager(MidiManager::Create());
  manager->StartSession(client.get());

  Result result = client->WaitForResult();
  // This #ifdef needs to be identical to the one in media/midi/midi_manager.cc.
  // Do not change the condition for disabling this test.
#if !defined(OS_MACOSX) && !defined(OS_WIN) && \
    !(defined(USE_ALSA) && defined(USE_UDEV)) && !defined(OS_ANDROID)
  EXPECT_EQ(Result::NOT_SUPPORTED, result);
#elif defined(USE_ALSA)
  // Temporary until http://crbug.com/371230 is resolved.
  EXPECT_TRUE(result == Result::OK || result == Result::INITIALIZATION_ERROR);
#else
  EXPECT_EQ(Result::OK, result);
#endif

  manager->Shutdown();
  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

// TODO(toyoshim): Add multi-threaded unit tests to check races around
// StartInitialization(), CompleteInitialization(), and Finalize().

}  // namespace

}  // namespace midi
