/* -*- 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 "GMPServiceParent.h"
#include "GMPTestMonitor.h"
#include "GMPUtils.h"
#include "GMPVideoDecoderProxy.h"
#include "gmp-api/gmp-video-host.h"
#include "gtest/gtest.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIObserverService.h"

#define GMP_DIR_NAME u"gmp-fakeopenh264"_ns
#define GMP_OLD_VERSION u"1.0"_ns
#define GMP_NEW_VERSION u"1.1"_ns

#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 = ""_ns);
  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.
 */
// Bug 1115253 - disable test in win64 to reduce failure rate
#if !defined(_WIN64)
TEST(GeckoMediaPlugins, RemoveAndDeleteForcedInUse)
{
  RefPtr<GMPRemoveTest> test(new GMPRemoveTest());

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

  // 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("thisOrigin"_ns));

  // 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("thisOrigin"_ns));

  // 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("thisOrigin"_ns));

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

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() {
  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.
  nsCOMPtr<nsISerialEventTarget> thread(GetServiceParent()->GetGMPThread());
  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**>(
          "GMPRemoveTest::gmp_GetVideoDecoder", 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;
  NS_DispatchAndSpinEventLoopUntilComplete(
      "GMPVideoDecoderProxy::InitDecode"_ns, mGMPThread,
      NewNonOwningRunnableMethod<const GMPVideoCodec&, const nsTArray<uint8_t>&,
                                 GMPVideoDecoderCallbackProxy*, int32_t>(
          "GMPVideoDecoderProxy::InitDecode", decoder,
          &GMPVideoDecoderProxy::InitDecode, codec, empty, this,
          1 /* core count */));

  if (mDecoder) {
    CloseVideoDecoder();
  }

  mDecoder = decoder;
  mHost = host;

  return true;
}

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

  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(nullptr, &tags, aNodeId,
                                                 std::move(cb)))) {
    mTestMonitor.SetFinished();
  }
}

void GMPRemoveTest::CloseVideoDecoder() {
  NS_DispatchAndSpinEventLoopUntilComplete(
      "GMPVideoDecoderProxy::Close"_ns, mGMPThread,
      NewNonOwningRunnableMethod("GMPVideoDecoderProxy::Close", mDecoder,
                                 &GMPVideoDecoderProxy::Close));

  mDecoder = nullptr;
  mHost = nullptr;
}

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

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

  mTestMonitor.AwaitFinished();
  return mDecodeResult;
}

void GMPRemoveTest::gmp_Decode() {
// from gmp-fake.cpp
#pragma pack(push, 1)
  struct EncodedFrame {
    struct SPSNalu {
      uint32_t size_;
      uint8_t payload[14];
    } sps_nalu;
    struct PPSNalu {
      uint32_t size_;
      uint8_t payload[4];
    } pps_nalu;
    struct IDRNalu {
      uint32_t size_;
      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_;
    } idr_nalu;
  };
#pragma pack(pop)

  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->sps_nalu.size_ = sizeof(EncodedFrame::SPSNalu) - sizeof(uint32_t);
  frameData->pps_nalu.size_ = sizeof(EncodedFrame::PPSNalu) - sizeof(uint32_t);
  frameData->idr_nalu.size_ = sizeof(EncodedFrame::IDRNalu) - sizeof(uint32_t);
  frameData->idr_nalu.h264_compat_ = 5;
  frameData->idr_nalu.magic_ = 0x004000b8;
  frameData->idr_nalu.width_ = frameData->idr_nalu.height_ = 16;

  nsTArray<uint8_t> empty;
  nsresult rv =
      mDecoder->Decode(std::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;
}
