/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "GMPService.h"
#include "GMPTestMonitor.h"
#include "gmp-api/gmp-video-host.h"
#include "gtest/gtest.h"
#include "mozilla/Services.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIObserverService.h"
#include "GMPVideoDecoderProxy.h"
#include "GMPServiceParent.h"
#include "GMPService.h"
#include "GMPUtils.h"
#include "mozilla/StaticPtr.h"
#include "MediaPrefs.h"

#define GMP_DIR_NAME NS_LITERAL_STRING("gmp-fakeopenh264")
#define GMP_OLD_VERSION NS_LITERAL_STRING("1.0")
#define GMP_NEW_VERSION NS_LITERAL_STRING("1.1")

#define GMP_DELETED_TOPIC "gmp-directory-deleted"

#define EXPECT_OK(X) EXPECT_TRUE(NS_SUCCEEDED(X))

using namespace mozilla;
using namespace mozilla::gmp;

class GMPRemoveTest : public nsIObserver
                    , public GMPVideoDecoderCallbackProxy
{
public:
  GMPRemoveTest();

  NS_DECL_THREADSAFE_ISUPPORTS

  // Called when a GMP plugin directory has been successfully deleted.
  // |aData| will contain the directory path.
  NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
                     const char16_t* aData) override;

  // Create a new GMP plugin directory that we can trash and add it to the GMP
  // service. Remove the original plugin directory. Original plugin directory
  // gets re-added at destruction.
  void Setup();

  bool CreateVideoDecoder(nsCString aNodeId = EmptyCString());
  void CloseVideoDecoder();

  void DeletePluginDirectory(bool aCanDefer);

  // Decode a dummy frame.
  GMPErr Decode();

  // Wait until TestMonitor has been signaled.
  void Wait();

  // Did we get a Terminated() callback from the plugin?
  bool IsTerminated();

  // From GMPVideoDecoderCallbackProxy
  // Set mDecodeResult; unblock TestMonitor.
  virtual void Decoded(GMPVideoi420Frame* aDecodedFrame) override;
  virtual void Error(GMPErr aError) override;

  // From GMPVideoDecoderCallbackProxy
  // We expect this to be called when a plugin has been forcibly closed.
  virtual void Terminated() override;

  // Ignored GMPVideoDecoderCallbackProxy members
  virtual void ReceivedDecodedReferenceFrame(const uint64_t aPictureId) override {}
  virtual void ReceivedDecodedFrame(const uint64_t aPictureId) override {}
  virtual void InputDataExhausted() override {}
  virtual void DrainComplete() override {}
  virtual void ResetComplete() override {}

private:
  virtual ~GMPRemoveTest();

  void gmp_Decode();
  void gmp_GetVideoDecoder(nsCString aNodeId,
                           GMPVideoDecoderProxy** aOutDecoder,
                           GMPVideoHost** aOutHost);
  void GeneratePlugin();

  GMPTestMonitor mTestMonitor;
  nsCOMPtr<nsIThread> mGMPThread;

  bool mIsTerminated;

  // Path to the cloned GMP we have created.
  nsString mTmpPath;
  nsCOMPtr<nsIFile> mTmpDir;

  // Path to the original GMP. Store so that we can re-add it after we're done
  // testing.
  nsString mOriginalPath;

  GMPVideoDecoderProxy* mDecoder;
  GMPVideoHost* mHost;
  GMPErr mDecodeResult;
};

/*
 * Simple test that the plugin is deleted when forcibly removed and deleted.
 */
TEST(GeckoMediaPlugins, RemoveAndDeleteForcedSimple)
{
  RefPtr<GMPRemoveTest> test(new GMPRemoveTest());

  test->Setup();
  test->DeletePluginDirectory(false /* force immediate */);
  test->Wait();
}

/*
 * Simple test that the plugin is deleted when deferred deletion is allowed.
 */
TEST(GeckoMediaPlugins, RemoveAndDeleteDeferredSimple)
{
  RefPtr<GMPRemoveTest> test(new GMPRemoveTest());

  test->Setup();
  test->DeletePluginDirectory(true /* can defer */);
  test->Wait();
}

/*
 * Test that the plugin is unavailable immediately after a forced
 * RemoveAndDelete, and that the plugin is deleted afterwards.
 */
TEST(GeckoMediaPlugins, RemoveAndDeleteForcedInUse)
{
  RefPtr<GMPRemoveTest> test(new GMPRemoveTest());

  test->Setup();
  EXPECT_TRUE(test->CreateVideoDecoder(NS_LITERAL_CSTRING("thisOrigin")));

  // Test that we can decode a frame.
  GMPErr err = test->Decode();
  EXPECT_EQ(err, GMPNoErr);

  test->DeletePluginDirectory(false /* force immediate */);
  test->Wait();

  // Test that the VideoDecoder is no longer available.
  EXPECT_FALSE(test->CreateVideoDecoder(NS_LITERAL_CSTRING("thisOrigin")));

  // Test that we were notified of the plugin's destruction.
  EXPECT_TRUE(test->IsTerminated());
}

/*
 * Test that the plugin is still usable after a deferred RemoveAndDelete, and
 * that the plugin is deleted afterwards.
 */
TEST(GeckoMediaPlugins, RemoveAndDeleteDeferredInUse)
{
  RefPtr<GMPRemoveTest> test(new GMPRemoveTest());

  test->Setup();
  EXPECT_TRUE(test->CreateVideoDecoder(NS_LITERAL_CSTRING("thisOrigin")));

  // Make sure decoding works before we do anything.
  GMPErr err = test->Decode();
  EXPECT_EQ(err, GMPNoErr);

  test->DeletePluginDirectory(true /* can defer */);

  // Test that decoding still works.
  err = test->Decode();
  EXPECT_EQ(err, GMPNoErr);

  // Test that this origin is still able to fetch the video decoder.
  EXPECT_TRUE(test->CreateVideoDecoder(NS_LITERAL_CSTRING("thisOrigin")));

  test->CloseVideoDecoder();
  test->Wait();
}

static StaticRefPtr<GeckoMediaPluginService> gService;
static StaticRefPtr<GeckoMediaPluginServiceParent> gServiceParent;

static GeckoMediaPluginService*
GetService()
{
  if (!gService) {
    RefPtr<GeckoMediaPluginService> service =
      GeckoMediaPluginService::GetGeckoMediaPluginService();
    gService = service;
  }

  return gService.get();
}

static GeckoMediaPluginServiceParent*
GetServiceParent()
{
  if (!gServiceParent) {
    RefPtr<GeckoMediaPluginServiceParent> parent =
      GeckoMediaPluginServiceParent::GetSingleton();
    gServiceParent = parent;
  }

  return gServiceParent.get();
}

NS_IMPL_ISUPPORTS(GMPRemoveTest, nsIObserver)

GMPRemoveTest::GMPRemoveTest()
  : mIsTerminated(false)
  , mDecoder(nullptr)
  , mHost(nullptr)
{
}

GMPRemoveTest::~GMPRemoveTest()
{
  bool exists;
  EXPECT_TRUE(NS_SUCCEEDED(mTmpDir->Exists(&exists)) && !exists);

  EXPECT_OK(GetServiceParent()->AddPluginDirectory(mOriginalPath));
}

void
GMPRemoveTest::Setup()
{
  // Initialize media preferences.
  MediaPrefs::GetSingleton();
  GeneratePlugin();
  GetService()->GetThread(getter_AddRefs(mGMPThread));

  // Spin the event loop until the GMP service has had a chance to complete
  // adding GMPs from MOZ_GMP_PATH. Otherwise, the RemovePluginDirectory()
  // below may complete before we're finished adding GMPs from MOZ_GMP_PATH,
  // and we'll end up not removing the GMP, and the test will fail.
  RefPtr<AbstractThread> thread(GetServiceParent()->GetAbstractGMPThread());
  EXPECT_TRUE(thread);
  GMPTestMonitor* mon = &mTestMonitor;
  GetServiceParent()->EnsureInitialized()->Then(thread, __func__,
    [mon]() { mon->SetFinished(); },
    [mon]() { mon->SetFinished(); }
  );
  mTestMonitor.AwaitFinished();

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  obs->AddObserver(this, GMP_DELETED_TOPIC, false /* strong ref */);
  EXPECT_OK(GetServiceParent()->RemovePluginDirectory(mOriginalPath));

  GetServiceParent()->AsyncAddPluginDirectory(mTmpPath)->Then(thread, __func__,
    [mon]() { mon->SetFinished(); },
    [mon]() { mon->SetFinished(); }
  );
  mTestMonitor.AwaitFinished();
}

bool
GMPRemoveTest::CreateVideoDecoder(nsCString aNodeId)
{
  GMPVideoHost* host;
  GMPVideoDecoderProxy* decoder = nullptr;

  mGMPThread->Dispatch(
    NewNonOwningRunnableMethod<nsCString, GMPVideoDecoderProxy**, GMPVideoHost**>(
      this, &GMPRemoveTest::gmp_GetVideoDecoder, aNodeId, &decoder, &host),
    NS_DISPATCH_NORMAL);

  mTestMonitor.AwaitFinished();

  if (!decoder) {
    return false;
  }

  GMPVideoCodec codec;
  memset(&codec, 0, sizeof(codec));
  codec.mGMPApiVersion = 33;

  nsTArray<uint8_t> empty;
  mGMPThread->Dispatch(
    NewNonOwningRunnableMethod<const GMPVideoCodec&, const nsTArray<uint8_t>&, GMPVideoDecoderCallbackProxy*, int32_t>(
      decoder, &GMPVideoDecoderProxy::InitDecode,
      codec, empty, this, 1 /* core count */),
    NS_DISPATCH_SYNC);

  if (mDecoder) {
    CloseVideoDecoder();
  }

  mDecoder = decoder;
  mHost = host;

  return true;
}

void
GMPRemoveTest::gmp_GetVideoDecoder(nsCString aNodeId,
                                   GMPVideoDecoderProxy** aOutDecoder,
                                   GMPVideoHost** aOutHost)
{
  nsTArray<nsCString> tags;
  tags.AppendElement(NS_LITERAL_CSTRING("h264"));
  tags.AppendElement(NS_LITERAL_CSTRING("fake"));

  class Callback : public GetGMPVideoDecoderCallback
  {
  public:
    Callback(GMPTestMonitor* aMonitor, GMPVideoDecoderProxy** aDecoder, GMPVideoHost** aHost)
      : mMonitor(aMonitor), mDecoder(aDecoder), mHost(aHost) { }
    virtual void Done(GMPVideoDecoderProxy* aDecoder, GMPVideoHost* aHost) override {
      *mDecoder = aDecoder;
      *mHost = aHost;
      mMonitor->SetFinished();
    }
  private:
    GMPTestMonitor* mMonitor;
    GMPVideoDecoderProxy** mDecoder;
    GMPVideoHost** mHost;
  };

  UniquePtr<GetGMPVideoDecoderCallback>
    cb(new Callback(&mTestMonitor, aOutDecoder, aOutHost));

  if (NS_FAILED(GetService()->GetGMPVideoDecoder(&tags, aNodeId, Move(cb)))) {
    mTestMonitor.SetFinished();
  }
}

void
GMPRemoveTest::CloseVideoDecoder()
{
  mGMPThread->Dispatch(
    NewNonOwningRunnableMethod(mDecoder, &GMPVideoDecoderProxy::Close),
    NS_DISPATCH_SYNC);

  mDecoder = nullptr;
  mHost = nullptr;
}

void
GMPRemoveTest::DeletePluginDirectory(bool aCanDefer)
{
  GetServiceParent()->RemoveAndDeletePluginDirectory(mTmpPath, aCanDefer);
}

GMPErr
GMPRemoveTest::Decode()
{
  mGMPThread->Dispatch(
    NewNonOwningRunnableMethod(this, &GMPRemoveTest::gmp_Decode),
    NS_DISPATCH_NORMAL);

  mTestMonitor.AwaitFinished();
  return mDecodeResult;
}

void
GMPRemoveTest::gmp_Decode()
{
  // from gmp-fake.cpp
  struct EncodedFrame {
    uint32_t length_;
    uint8_t h264_compat_;
    uint32_t magic_;
    uint32_t width_;
    uint32_t height_;
    uint8_t y_;
    uint8_t u_;
    uint8_t v_;
    uint32_t timestamp_;
  };

  GMPVideoFrame* absFrame;
  GMPErr err = mHost->CreateFrame(kGMPEncodedVideoFrame, &absFrame);
  EXPECT_EQ(err, GMPNoErr);

  GMPUniquePtr<GMPVideoEncodedFrame>
    frame(static_cast<GMPVideoEncodedFrame*>(absFrame));
  err = frame->CreateEmptyFrame(sizeof(EncodedFrame) /* size */);
  EXPECT_EQ(err, GMPNoErr);

  EncodedFrame* frameData = reinterpret_cast<EncodedFrame*>(frame->Buffer());
  frameData->magic_ = 0x4652414d;
  frameData->width_ = frameData->height_ = 16;

  nsTArray<uint8_t> empty;
  nsresult rv = mDecoder->Decode(Move(frame), false /* aMissingFrames */, empty);
  EXPECT_OK(rv);
}

void
GMPRemoveTest::Wait()
{
  mTestMonitor.AwaitFinished();
}

bool
GMPRemoveTest::IsTerminated()
{
  return mIsTerminated;
}

// nsIObserver
NS_IMETHODIMP
GMPRemoveTest::Observe(nsISupports* aSubject, const char* aTopic,
                       const char16_t* aData)
{
  EXPECT_TRUE(!strcmp(GMP_DELETED_TOPIC, aTopic));

  nsString data(aData);
  if (mTmpPath.Equals(data)) {
    mTestMonitor.SetFinished();
    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
    obs->RemoveObserver(this, GMP_DELETED_TOPIC);
  }

  return NS_OK;
}

// GMPVideoDecoderCallbackProxy
void
GMPRemoveTest::Decoded(GMPVideoi420Frame* aDecodedFrame)
{
  aDecodedFrame->Destroy();
  mDecodeResult = GMPNoErr;
  mTestMonitor.SetFinished();
}

// GMPVideoDecoderCallbackProxy
void
GMPRemoveTest::Error(GMPErr aError)
{
  mDecodeResult = aError;
  mTestMonitor.SetFinished();
}

// GMPVideoDecoderCallbackProxy
void
GMPRemoveTest::Terminated()
{
  mIsTerminated = true;
  if (mDecoder) {
    mDecoder->Close();
    mDecoder = nullptr;
  }
}

void
GMPRemoveTest::GeneratePlugin()
{
  nsresult rv;
  nsCOMPtr<nsIFile> gmpDir;
  nsCOMPtr<nsIFile> origDir;
  nsCOMPtr<nsIFile> tmpDir;

  rv = NS_GetSpecialDirectory(NS_GRE_DIR,
                              getter_AddRefs(gmpDir));
  EXPECT_OK(rv);
  rv = gmpDir->Append(GMP_DIR_NAME);
  EXPECT_OK(rv);

  rv = gmpDir->Clone(getter_AddRefs(origDir));
  EXPECT_OK(rv);
  rv = origDir->Append(GMP_OLD_VERSION);
  EXPECT_OK(rv);

  rv = gmpDir->Clone(getter_AddRefs(tmpDir));
  EXPECT_OK(rv);
  rv = tmpDir->Append(GMP_NEW_VERSION);
  EXPECT_OK(rv);
  bool exists = false;
  rv = tmpDir->Exists(&exists);
  EXPECT_OK(rv);
  if (exists) {
    rv = tmpDir->Remove(true);
    EXPECT_OK(rv);
  }
  rv = origDir->CopyTo(gmpDir, GMP_NEW_VERSION);
  EXPECT_OK(rv);

  rv = gmpDir->Clone(getter_AddRefs(tmpDir));
  EXPECT_OK(rv);
  rv = tmpDir->Append(GMP_NEW_VERSION);
  EXPECT_OK(rv);

  EXPECT_OK(origDir->GetPath(mOriginalPath));
  EXPECT_OK(tmpDir->GetPath(mTmpPath));
  mTmpDir = tmpDir;
}
