/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * 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 <utility>

#include "AnimationFrameBuffer.h"
#include "Common.h"
#include "gtest/gtest.h"

using namespace mozilla;
using namespace mozilla::image;

static already_AddRefed<imgFrame> CreateEmptyFrame(
    const gfx::IntSize& aSize = gfx::IntSize(1, 1),
    const gfx::IntRect& aFrameRect = gfx::IntRect(0, 0, 1, 1),
    bool aCanRecycle = true) {
  RefPtr<imgFrame> frame = new imgFrame();
  AnimationParams animParams{aFrameRect, FrameTimeout::Forever(),
                             /* aFrameNum */ 1, BlendMethod::OVER,
                             DisposalMethod::NOT_SPECIFIED};
  nsresult rv =
      frame->InitForDecoder(aSize, mozilla::gfx::SurfaceFormat::OS_RGBA, false,
                            Some(animParams), aCanRecycle);
  EXPECT_NS_SUCCEEDED(rv);
  RawAccessFrameRef frameRef = frame->RawAccessRef();
  // Normally the blend animation filter would set the dirty rect, but since
  // we aren't producing an actual animation here, we need to fake it.
  frame->SetDirtyRect(aFrameRect);
  frame->Finish();
  return frame.forget();
}

static bool ReinitForRecycle(RawAccessFrameRef& aFrame) {
  if (!aFrame) {
    return false;
  }

  AnimationParams animParams{aFrame->GetRect(), FrameTimeout::Forever(),
                             /* aFrameNum */ 1, BlendMethod::OVER,
                             DisposalMethod::NOT_SPECIFIED};
  return NS_SUCCEEDED(aFrame->InitForDecoderRecycle(animParams));
}

static void PrepareForDiscardingQueue(AnimationFrameRetainedBuffer& aQueue) {
  ASSERT_EQ(size_t(0), aQueue.Size());
  ASSERT_LT(size_t(1), aQueue.Batch());

  AnimationFrameBuffer::InsertStatus status = aQueue.Insert(CreateEmptyFrame());
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);

  while (true) {
    status = aQueue.Insert(CreateEmptyFrame());
    bool restartDecoder = aQueue.AdvanceTo(aQueue.Size() - 1);
    EXPECT_FALSE(restartDecoder);

    if (status == AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE) {
      break;
    }
    EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);
  }

  EXPECT_EQ(aQueue.Threshold(), aQueue.Size());
}

static void VerifyDiscardingQueueContents(
    AnimationFrameDiscardingQueue& aQueue) {
  auto frames = aQueue.Display();
  for (auto i : frames) {
    EXPECT_TRUE(i != nullptr);
  }
}

static void VerifyInsertInternal(AnimationFrameBuffer& aQueue,
                                 imgFrame* aFrame) {
  // Determine the frame index where we just inserted the frame.
  size_t frameIndex;
  if (aQueue.MayDiscard()) {
    const AnimationFrameDiscardingQueue& queue =
        *static_cast<AnimationFrameDiscardingQueue*>(&aQueue);
    frameIndex = queue.PendingInsert() == 0 ? queue.Size() - 1
                                            : queue.PendingInsert() - 1;
  } else {
    ASSERT_FALSE(aQueue.SizeKnown());
    frameIndex = aQueue.Size() - 1;
  }

  // Make sure we can get the frame from that index.
  RefPtr<imgFrame> frame = aQueue.Get(frameIndex, false);
  EXPECT_EQ(aFrame, frame.get());
}

static void VerifyAdvance(AnimationFrameBuffer& aQueue, size_t aExpectedFrame,
                          bool aExpectedRestartDecoder) {
  RefPtr<imgFrame> oldFrame;
  size_t totalRecycled;
  if (aQueue.IsRecycling()) {
    AnimationFrameRecyclingQueue& queue =
        *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
    oldFrame = queue.Get(queue.Displayed(), false);
    totalRecycled = queue.Recycle().size();
  }

  bool restartDecoder = aQueue.AdvanceTo(aExpectedFrame);
  EXPECT_EQ(aExpectedRestartDecoder, restartDecoder);

  if (aQueue.IsRecycling()) {
    const AnimationFrameRecyclingQueue& queue =
        *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
    EXPECT_FALSE(queue.Recycle().back().mDirtyRect.IsEmpty());
    EXPECT_TRUE(
        queue.Recycle().back().mDirtyRect.Contains(oldFrame->GetDirtyRect()));
    EXPECT_EQ(totalRecycled + 1, queue.Recycle().size());
    EXPECT_EQ(oldFrame.get(), queue.Recycle().back().mFrame.get());
  }
}

static void VerifyInsertAndAdvance(
    AnimationFrameBuffer& aQueue, size_t aExpectedFrame,
    AnimationFrameBuffer::InsertStatus aExpectedStatus) {
  // Insert the decoded frame.
  RefPtr<imgFrame> frame = CreateEmptyFrame();
  AnimationFrameBuffer::InsertStatus status =
      aQueue.Insert(RefPtr<imgFrame>(frame));
  EXPECT_EQ(aExpectedStatus, status);
  EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame));
  VerifyInsertInternal(aQueue, frame);

  // Advance the display frame.
  bool expectedRestartDecoder =
      aExpectedStatus == AnimationFrameBuffer::InsertStatus::YIELD;
  VerifyAdvance(aQueue, aExpectedFrame, expectedRestartDecoder);
}

static void VerifyMarkComplete(
    AnimationFrameBuffer& aQueue, bool aExpectedContinue,
    const gfx::IntRect& aRefreshArea = gfx::IntRect(0, 0, 1, 1)) {
  if (aQueue.IsRecycling() && !aQueue.SizeKnown()) {
    const AnimationFrameRecyclingQueue& queue =
        *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
    EXPECT_EQ(queue.FirstFrame()->GetRect(), queue.FirstFrameRefreshArea());
  }

  bool keepDecoding = aQueue.MarkComplete(aRefreshArea);
  EXPECT_EQ(aExpectedContinue, keepDecoding);

  if (aQueue.IsRecycling()) {
    const AnimationFrameRecyclingQueue& queue =
        *static_cast<AnimationFrameRecyclingQueue*>(&aQueue);
    EXPECT_EQ(aRefreshArea, queue.FirstFrameRefreshArea());
  }
}

static void VerifyInsert(AnimationFrameBuffer& aQueue,
                         AnimationFrameBuffer::InsertStatus aExpectedStatus) {
  RefPtr<imgFrame> frame = CreateEmptyFrame();
  AnimationFrameBuffer::InsertStatus status =
      aQueue.Insert(RefPtr<imgFrame>(frame));
  EXPECT_EQ(aExpectedStatus, status);
  EXPECT_TRUE(aQueue.IsLastInsertedFrame(frame));
  VerifyInsertInternal(aQueue, frame);
}

static void VerifyReset(AnimationFrameBuffer& aQueue, bool aExpectedContinue,
                        const imgFrame* aFirstFrame) {
  bool keepDecoding = aQueue.Reset();
  EXPECT_EQ(aExpectedContinue, keepDecoding);
  EXPECT_EQ(aQueue.Batch() * 2, aQueue.PendingDecode());
  EXPECT_EQ(aFirstFrame, aQueue.Get(0, true));

  if (!aQueue.MayDiscard()) {
    const AnimationFrameRetainedBuffer& queue =
        *static_cast<AnimationFrameRetainedBuffer*>(&aQueue);
    EXPECT_EQ(aFirstFrame, queue.Frames()[0].get());
    EXPECT_EQ(aFirstFrame, aQueue.Get(0, false));
  } else {
    const AnimationFrameDiscardingQueue& queue =
        *static_cast<AnimationFrameDiscardingQueue*>(&aQueue);
    EXPECT_EQ(size_t(0), queue.PendingInsert());
    EXPECT_EQ(size_t(0), queue.Display().size());
    EXPECT_EQ(aFirstFrame, queue.FirstFrame());
    EXPECT_EQ(nullptr, aQueue.Get(0, false));
  }
}

class ImageAnimationFrameBuffer : public ::testing::Test {
 public:
  ImageAnimationFrameBuffer() {}

 private:
  AutoInitializeImageLib mInit;
};

TEST_F(ImageAnimationFrameBuffer, RetainedInitialState) {
  const size_t kThreshold = 800;
  const size_t kBatch = 100;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);

  EXPECT_EQ(kThreshold, buffer.Threshold());
  EXPECT_EQ(kBatch, buffer.Batch());
  EXPECT_EQ(size_t(0), buffer.Displayed());
  EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
  EXPECT_EQ(size_t(0), buffer.PendingAdvance());
  EXPECT_FALSE(buffer.MayDiscard());
  EXPECT_FALSE(buffer.SizeKnown());
  EXPECT_EQ(size_t(0), buffer.Size());
}

TEST_F(ImageAnimationFrameBuffer, ThresholdTooSmall) {
  const size_t kThreshold = 0;
  const size_t kBatch = 10;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);

  EXPECT_EQ(kBatch * 2 + 1, buffer.Threshold());
  EXPECT_EQ(kBatch, buffer.Batch());
  EXPECT_EQ(kBatch * 2, buffer.PendingDecode());
  EXPECT_EQ(size_t(0), buffer.PendingAdvance());
}

TEST_F(ImageAnimationFrameBuffer, BatchTooSmall) {
  const size_t kThreshold = 10;
  const size_t kBatch = 0;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);

  EXPECT_EQ(kThreshold, buffer.Threshold());
  EXPECT_EQ(size_t(1), buffer.Batch());
  EXPECT_EQ(size_t(2), buffer.PendingDecode());
  EXPECT_EQ(size_t(0), buffer.PendingAdvance());
}

TEST_F(ImageAnimationFrameBuffer, BatchTooBig) {
  const size_t kThreshold = 50;
  const size_t kBatch = SIZE_MAX;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);

  // The rounding is important here (e.g. SIZE_MAX/4 * 2 != SIZE_MAX/2).
  EXPECT_EQ(SIZE_MAX / 4, buffer.Batch());
  EXPECT_EQ(buffer.Batch() * 2 + 1, buffer.Threshold());
  EXPECT_EQ(buffer.Batch() * 2, buffer.PendingDecode());
  EXPECT_EQ(size_t(0), buffer.PendingAdvance());
}

TEST_F(ImageAnimationFrameBuffer, FinishUnderBatchAndThreshold) {
  const size_t kThreshold = 30;
  const size_t kBatch = 10;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
  const auto& frames = buffer.Frames();

  EXPECT_EQ(kBatch * 2, buffer.PendingDecode());

  RefPtr<imgFrame> firstFrame;
  for (size_t i = 0; i < 5; ++i) {
    RefPtr<imgFrame> frame = CreateEmptyFrame();
    auto status = buffer.Insert(RefPtr<imgFrame>(frame));
    EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
    EXPECT_FALSE(buffer.SizeKnown());
    EXPECT_EQ(buffer.Size(), i + 1);

    if (i == 4) {
      EXPECT_EQ(size_t(15), buffer.PendingDecode());
      bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
      EXPECT_FALSE(keepDecoding);
      EXPECT_TRUE(buffer.SizeKnown());
      EXPECT_EQ(size_t(0), buffer.PendingDecode());
      EXPECT_FALSE(buffer.HasRedecodeError());
    }

    EXPECT_FALSE(buffer.MayDiscard());

    imgFrame* gotFrame = buffer.Get(i, false);
    EXPECT_EQ(frame.get(), gotFrame);
    ASSERT_EQ(i + 1, frames.Length());
    EXPECT_EQ(frame.get(), frames[i].get());

    if (i == 0) {
      firstFrame = std::move(frame);
      EXPECT_EQ(size_t(0), buffer.Displayed());
    } else {
      EXPECT_EQ(i - 1, buffer.Displayed());
      bool restartDecoder = buffer.AdvanceTo(i);
      EXPECT_FALSE(restartDecoder);
      EXPECT_EQ(i, buffer.Displayed());
    }

    gotFrame = buffer.Get(0, false);
    EXPECT_EQ(firstFrame.get(), gotFrame);
  }

  // Loop again over the animation and make sure it is still all there.
  for (size_t i = 0; i < frames.Length(); ++i) {
    EXPECT_TRUE(buffer.Get(i, false) != nullptr);

    bool restartDecoder = buffer.AdvanceTo(i);
    EXPECT_FALSE(restartDecoder);
  }
}

TEST_F(ImageAnimationFrameBuffer, FinishMultipleBatchesUnderThreshold) {
  const size_t kThreshold = 30;
  const size_t kBatch = 2;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, 0);
  const auto& frames = buffer.Frames();

  EXPECT_EQ(kBatch * 2, buffer.PendingDecode());

  // Add frames until it tells us to stop.
  AnimationFrameBuffer::InsertStatus status;
  do {
    status = buffer.Insert(CreateEmptyFrame());
    EXPECT_FALSE(buffer.SizeKnown());
    EXPECT_FALSE(buffer.MayDiscard());
  } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE);

  EXPECT_EQ(size_t(0), buffer.PendingDecode());
  EXPECT_EQ(size_t(4), frames.Length());
  EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::YIELD);

  // Progress through the animation until it lets us decode again.
  bool restartDecoder = false;
  size_t i = 0;
  do {
    EXPECT_TRUE(buffer.Get(i, false) != nullptr);
    if (i > 0) {
      restartDecoder = buffer.AdvanceTo(i);
    }
    ++i;
  } while (!restartDecoder);

  EXPECT_EQ(size_t(2), buffer.PendingDecode());
  EXPECT_EQ(size_t(2), buffer.Displayed());

  // Add the last frame.
  status = buffer.Insert(CreateEmptyFrame());
  EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
  bool keepDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
  EXPECT_FALSE(keepDecoding);
  EXPECT_TRUE(buffer.SizeKnown());
  EXPECT_EQ(size_t(0), buffer.PendingDecode());
  EXPECT_EQ(size_t(5), frames.Length());
  EXPECT_FALSE(buffer.HasRedecodeError());

  // Finish progressing through the animation.
  for (; i < frames.Length(); ++i) {
    EXPECT_TRUE(buffer.Get(i, false) != nullptr);
    restartDecoder = buffer.AdvanceTo(i);
    EXPECT_FALSE(restartDecoder);
  }

  // Loop again over the animation and make sure it is still all there.
  for (i = 0; i < frames.Length(); ++i) {
    EXPECT_TRUE(buffer.Get(i, false) != nullptr);
    restartDecoder = buffer.AdvanceTo(i);
    EXPECT_FALSE(restartDecoder);
  }

  // Loop to the third frame and then reset the animation.
  for (i = 0; i < 3; ++i) {
    EXPECT_TRUE(buffer.Get(i, false) != nullptr);
    restartDecoder = buffer.AdvanceTo(i);
    EXPECT_FALSE(restartDecoder);
  }

  // Since we are below the threshold, we can reset the get index only.
  // Nothing else should have changed.
  restartDecoder = buffer.Reset();
  EXPECT_FALSE(restartDecoder);
  for (i = 0; i < 5; ++i) {
    EXPECT_TRUE(buffer.Get(i, false) != nullptr);
  }
  EXPECT_EQ(size_t(0), buffer.PendingDecode());
  EXPECT_EQ(size_t(0), buffer.PendingAdvance());
  EXPECT_EQ(size_t(0), buffer.Displayed());
}

TEST_F(ImageAnimationFrameBuffer, StartAfterBeginning) {
  const size_t kThreshold = 30;
  const size_t kBatch = 2;
  const size_t kStartFrame = 7;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame);

  EXPECT_EQ(kStartFrame, buffer.PendingAdvance());

  // Add frames until it tells us to stop. It should be later than before,
  // because it auto-advances until its displayed frame is kStartFrame.
  AnimationFrameBuffer::InsertStatus status;
  size_t i = 0;
  do {
    status = buffer.Insert(CreateEmptyFrame());
    EXPECT_FALSE(buffer.SizeKnown());
    EXPECT_FALSE(buffer.MayDiscard());

    if (i <= kStartFrame) {
      EXPECT_EQ(i, buffer.Displayed());
      EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance());
    } else {
      EXPECT_EQ(kStartFrame, buffer.Displayed());
      EXPECT_EQ(size_t(0), buffer.PendingAdvance());
    }

    i++;
  } while (status == AnimationFrameBuffer::InsertStatus::CONTINUE);

  EXPECT_EQ(size_t(0), buffer.PendingDecode());
  EXPECT_EQ(size_t(0), buffer.PendingAdvance());
  EXPECT_EQ(size_t(10), buffer.Size());
}

TEST_F(ImageAnimationFrameBuffer, StartAfterBeginningAndReset) {
  const size_t kThreshold = 30;
  const size_t kBatch = 2;
  const size_t kStartFrame = 7;
  AnimationFrameRetainedBuffer buffer(kThreshold, kBatch, kStartFrame);

  EXPECT_EQ(kStartFrame, buffer.PendingAdvance());

  // Add frames until it tells us to stop. It should be later than before,
  // because it auto-advances until its displayed frame is kStartFrame.
  for (size_t i = 0; i < 5; ++i) {
    AnimationFrameBuffer::InsertStatus status =
        buffer.Insert(CreateEmptyFrame());
    EXPECT_EQ(status, AnimationFrameBuffer::InsertStatus::CONTINUE);
    EXPECT_FALSE(buffer.SizeKnown());
    EXPECT_FALSE(buffer.MayDiscard());
    EXPECT_EQ(i, buffer.Displayed());
    EXPECT_EQ(kStartFrame - i, buffer.PendingAdvance());
  }

  // When we reset the animation, it goes back to the beginning. That means
  // we can forget about what we were told to advance to at the start. While
  // we have plenty of frames in our buffer, we still need one more because
  // in the real scenario, the decoder thread is still running and it is easier
  // to let it insert its last frame than to coordinate quitting earlier.
  buffer.Reset();
  EXPECT_EQ(size_t(0), buffer.Displayed());
  EXPECT_EQ(size_t(1), buffer.PendingDecode());
  EXPECT_EQ(size_t(0), buffer.PendingAdvance());
  EXPECT_EQ(size_t(5), buffer.Size());
}

static void TestDiscardingQueueLoop(AnimationFrameDiscardingQueue& aQueue,
                                    const imgFrame* aFirstFrame,
                                    size_t aThreshold, size_t aBatch,
                                    size_t aStartFrame) {
  // We should be advanced right up to the last decoded frame.
  EXPECT_TRUE(aQueue.MayDiscard());
  EXPECT_FALSE(aQueue.SizeKnown());
  EXPECT_EQ(aBatch, aQueue.Batch());
  EXPECT_EQ(aThreshold, aQueue.PendingInsert());
  EXPECT_EQ(aThreshold, aQueue.Size());
  EXPECT_EQ(aFirstFrame, aQueue.FirstFrame());
  EXPECT_EQ(size_t(1), aQueue.Display().size());
  EXPECT_EQ(size_t(3), aQueue.PendingDecode());
  VerifyDiscardingQueueContents(aQueue);

  // Make sure frames get removed as we advance.
  VerifyInsertAndAdvance(aQueue, 5,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
  EXPECT_EQ(size_t(1), aQueue.Display().size());
  VerifyInsertAndAdvance(aQueue, 6,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
  EXPECT_EQ(size_t(1), aQueue.Display().size());

  // We actually will yield if we are recycling instead of continuing because
  // the pending calculation is slightly different. We will actually request one
  // less frame than we have to recycle.
  if (aQueue.IsRecycling()) {
    VerifyInsertAndAdvance(aQueue, 7,
                           AnimationFrameBuffer::InsertStatus::YIELD);
  } else {
    VerifyInsertAndAdvance(aQueue, 7,
                           AnimationFrameBuffer::InsertStatus::CONTINUE);
  }
  EXPECT_EQ(size_t(1), aQueue.Display().size());

  // We should get throttled if we insert too much.
  VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE);
  EXPECT_EQ(size_t(2), aQueue.Display().size());
  EXPECT_EQ(size_t(1), aQueue.PendingDecode());
  VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD);
  EXPECT_EQ(size_t(3), aQueue.Display().size());
  EXPECT_EQ(size_t(0), aQueue.PendingDecode());

  // We should get restarted if we advance.
  VerifyAdvance(aQueue, 8, true);
  EXPECT_EQ(size_t(2), aQueue.PendingDecode());
  VerifyAdvance(aQueue, 9, false);
  EXPECT_EQ(size_t(2), aQueue.PendingDecode());

  // We should continue decoding if we completed, since we are discarding.
  VerifyMarkComplete(aQueue, true);
  EXPECT_EQ(size_t(2), aQueue.PendingDecode());
  EXPECT_EQ(size_t(10), aQueue.Size());
  EXPECT_TRUE(aQueue.SizeKnown());
  EXPECT_FALSE(aQueue.HasRedecodeError());

  // Insert the first frames of the animation.
  VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsert(aQueue, AnimationFrameBuffer::InsertStatus::YIELD);
  EXPECT_EQ(size_t(0), aQueue.PendingDecode());
  EXPECT_EQ(size_t(10), aQueue.Size());

  // Advance back at the beginning. The first frame should only match for
  // display purposes.
  VerifyAdvance(aQueue, 0, true);
  EXPECT_EQ(size_t(2), aQueue.PendingDecode());
  EXPECT_TRUE(aQueue.FirstFrame() != nullptr);
  EXPECT_TRUE(aQueue.Get(0, false) != nullptr);
  EXPECT_NE(aQueue.FirstFrame(), aQueue.Get(0, false));
  EXPECT_EQ(aQueue.FirstFrame(), aQueue.Get(0, true));

  // Reiterate one more time and make it loops back.
  VerifyInsertAndAdvance(aQueue, 1,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(aQueue, 2, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(aQueue, 3,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(aQueue, 4, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(aQueue, 5,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(aQueue, 6, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(aQueue, 7,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(aQueue, 8, AnimationFrameBuffer::InsertStatus::YIELD);

  EXPECT_EQ(size_t(10), aQueue.PendingInsert());
  VerifyMarkComplete(aQueue, true);
  EXPECT_EQ(size_t(0), aQueue.PendingInsert());

  VerifyInsertAndAdvance(aQueue, 9,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(aQueue, 0, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(aQueue, 1,
                         AnimationFrameBuffer::InsertStatus::CONTINUE);
}

TEST_F(ImageAnimationFrameBuffer, DiscardingLoop) {
  const size_t kThreshold = 5;
  const size_t kBatch = 2;
  const size_t kStartFrame = 0;
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
  PrepareForDiscardingQueue(retained);
  const imgFrame* firstFrame = retained.Frames()[0].get();
  AnimationFrameDiscardingQueue buffer(std::move(retained));
  TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
}

TEST_F(ImageAnimationFrameBuffer, RecyclingLoop) {
  const size_t kThreshold = 5;
  const size_t kBatch = 2;
  const size_t kStartFrame = 0;
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
  PrepareForDiscardingQueue(retained);
  const imgFrame* firstFrame = retained.Frames()[0].get();
  AnimationFrameRecyclingQueue buffer(std::move(retained));

  // We should not start with any recycled frames.
  ASSERT_TRUE(buffer.Recycle().empty());

  TestDiscardingQueueLoop(buffer, firstFrame, kThreshold, kBatch, kStartFrame);

  // All the frames we inserted should have been recycleable.
  ASSERT_FALSE(buffer.Recycle().empty());
  while (!buffer.Recycle().empty()) {
    gfx::IntRect expectedRect(0, 0, 1, 1);
    RefPtr<imgFrame> expectedFrame = buffer.Recycle().front().mFrame;
    EXPECT_FALSE(expectedRect.IsEmpty());
    EXPECT_TRUE(expectedFrame.get() != nullptr);

    gfx::IntRect gotRect;
    RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect);
    EXPECT_EQ(expectedFrame.get(), gotFrame.get());
    EXPECT_EQ(expectedRect, gotRect);
    EXPECT_TRUE(ReinitForRecycle(gotFrame));
  }

  // Trying to pull a recycled frame when we have nothing should be safe too.
  gfx::IntRect gotRect;
  RawAccessFrameRef gotFrame = buffer.RecycleFrame(gotRect);
  EXPECT_TRUE(gotFrame.get() == nullptr);
  EXPECT_FALSE(ReinitForRecycle(gotFrame));
}

static void TestDiscardingQueueReset(AnimationFrameDiscardingQueue& aQueue,
                                     const imgFrame* aFirstFrame,
                                     size_t aThreshold, size_t aBatch,
                                     size_t aStartFrame) {
  // We should be advanced right up to the last decoded frame.
  EXPECT_TRUE(aQueue.MayDiscard());
  EXPECT_FALSE(aQueue.SizeKnown());
  EXPECT_EQ(aBatch, aQueue.Batch());
  EXPECT_EQ(aThreshold, aQueue.PendingInsert());
  EXPECT_EQ(aThreshold, aQueue.Size());
  EXPECT_EQ(aFirstFrame, aQueue.FirstFrame());
  EXPECT_EQ(size_t(1), aQueue.Display().size());
  EXPECT_EQ(size_t(4), aQueue.PendingDecode());
  VerifyDiscardingQueueContents(aQueue);

  // Reset should clear everything except the first frame.
  VerifyReset(aQueue, false, aFirstFrame);
}

TEST_F(ImageAnimationFrameBuffer, DiscardingReset) {
  const size_t kThreshold = 8;
  const size_t kBatch = 3;
  const size_t kStartFrame = 0;
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
  PrepareForDiscardingQueue(retained);
  const imgFrame* firstFrame = retained.Frames()[0].get();
  AnimationFrameDiscardingQueue buffer(std::move(retained));
  TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
}

TEST_F(ImageAnimationFrameBuffer, ResetBeforeDiscardingThreshold) {
  const size_t kThreshold = 3;
  const size_t kBatch = 1;
  const size_t kStartFrame = 0;

  // Get the starting buffer to just before the point where we need to switch
  // to a discarding buffer, reset the animation so advancing points at the
  // first frame, and insert the last frame to cross the threshold.
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
  VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(retained, 1,
                         AnimationFrameBuffer::InsertStatus::YIELD);
  bool restartDecoder = retained.Reset();
  EXPECT_FALSE(restartDecoder);
  VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);

  const imgFrame* firstFrame = retained.Frames()[0].get();
  EXPECT_TRUE(firstFrame != nullptr);
  AnimationFrameDiscardingQueue buffer(std::move(retained));
  const imgFrame* displayFirstFrame = buffer.Get(0, true);
  const imgFrame* advanceFirstFrame = buffer.Get(0, false);
  EXPECT_EQ(firstFrame, displayFirstFrame);
  EXPECT_EQ(firstFrame, advanceFirstFrame);
}

TEST_F(ImageAnimationFrameBuffer, DiscardingTooFewFrames) {
  const size_t kThreshold = 3;
  const size_t kBatch = 1;
  const size_t kStartFrame = 0;

  // First get us to a discarding buffer state.
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
  VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(retained, 1,
                         AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);

  // Insert one more frame.
  AnimationFrameDiscardingQueue buffer(std::move(retained));
  VerifyAdvance(buffer, 2, true);
  VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD);

  // Mark it as complete.
  bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
  EXPECT_FALSE(restartDecoder);
  EXPECT_FALSE(buffer.HasRedecodeError());

  // Insert one fewer frame than before.
  VerifyAdvance(buffer, 3, true);
  VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD);

  // When we mark it as complete, it should fail due to too few frames.
  restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
  EXPECT_TRUE(buffer.HasRedecodeError());
  EXPECT_EQ(size_t(0), buffer.PendingDecode());
  EXPECT_EQ(size_t(4), buffer.Size());
}

TEST_F(ImageAnimationFrameBuffer, DiscardingTooManyFrames) {
  const size_t kThreshold = 3;
  const size_t kBatch = 1;
  const size_t kStartFrame = 0;

  // First get us to a discarding buffer state.
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
  VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::CONTINUE);
  VerifyInsertAndAdvance(retained, 1,
                         AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsert(retained, AnimationFrameBuffer::InsertStatus::DISCARD_YIELD);

  // Insert one more frame.
  AnimationFrameDiscardingQueue buffer(std::move(retained));
  VerifyAdvance(buffer, 2, true);
  VerifyInsert(buffer, AnimationFrameBuffer::InsertStatus::YIELD);

  // Mark it as complete.
  bool restartDecoder = buffer.MarkComplete(gfx::IntRect(0, 0, 1, 1));
  EXPECT_FALSE(restartDecoder);
  EXPECT_FALSE(buffer.HasRedecodeError());

  // Advance and insert to get us back to the end on the redecode.
  VerifyAdvance(buffer, 3, true);
  VerifyInsertAndAdvance(buffer, 0, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(buffer, 1, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(buffer, 2, AnimationFrameBuffer::InsertStatus::YIELD);
  VerifyInsertAndAdvance(buffer, 3, AnimationFrameBuffer::InsertStatus::YIELD);

  // Attempt to insert a 5th frame, it should fail.
  RefPtr<imgFrame> frame = CreateEmptyFrame();
  AnimationFrameBuffer::InsertStatus status = buffer.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);
  EXPECT_TRUE(buffer.HasRedecodeError());
  EXPECT_EQ(size_t(0), buffer.PendingDecode());
  EXPECT_EQ(size_t(4), buffer.Size());
}

TEST_F(ImageAnimationFrameBuffer, RecyclingReset) {
  const size_t kThreshold = 8;
  const size_t kBatch = 3;
  const size_t kStartFrame = 0;
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);
  PrepareForDiscardingQueue(retained);
  const imgFrame* firstFrame = retained.Frames()[0].get();
  AnimationFrameRecyclingQueue buffer(std::move(retained));
  TestDiscardingQueueReset(buffer, firstFrame, kThreshold, kBatch, kStartFrame);
}

TEST_F(ImageAnimationFrameBuffer, RecyclingResetBeforeComplete) {
  const size_t kThreshold = 3;
  const size_t kBatch = 1;
  const size_t kStartFrame = 0;
  const gfx::IntSize kImageSize(100, 100);
  const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize);
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);

  // Get the starting buffer to just before the point where we need to switch
  // to a discarding buffer, reset the animation so advancing points at the
  // first frame, and insert the last frame to cross the threshold.
  RefPtr<imgFrame> frame;
  frame = CreateEmptyFrame(kImageSize, kImageRect, false);
  AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);

  frame = CreateEmptyFrame(
      kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)),
      false);
  status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);

  VerifyAdvance(retained, 1, true);

  frame = CreateEmptyFrame(
      kImageSize, gfx::IntRect(gfx::IntPoint(20, 10), gfx::IntSize(1, 1)),
      false);
  status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_YIELD, status);

  AnimationFrameRecyclingQueue buffer(std::move(retained));
  bool restartDecoding = buffer.Reset();
  EXPECT_TRUE(restartDecoding);

  // None of the buffers were recyclable.
  EXPECT_FALSE(buffer.Recycle().empty());
  while (!buffer.Recycle().empty()) {
    gfx::IntRect recycleRect;
    RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect);
    EXPECT_TRUE(frameRef);
    EXPECT_FALSE(ReinitForRecycle(frameRef));
  }

  // Reinsert the first two frames as recyclable and reset again.
  frame = CreateEmptyFrame(kImageSize, kImageRect, true);
  status = buffer.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);

  frame = CreateEmptyFrame(
      kImageSize, gfx::IntRect(gfx::IntPoint(10, 10), gfx::IntSize(1, 1)),
      true);
  status = buffer.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);

  restartDecoding = buffer.Reset();
  EXPECT_TRUE(restartDecoding);

  // Now both buffers should have been saved and the dirty rect replaced with
  // the full image rect since we don't know the first frame refresh area yet.
  EXPECT_EQ(size_t(2), buffer.Recycle().size());
  for (const auto& entry : buffer.Recycle()) {
    EXPECT_EQ(kImageRect, entry.mDirtyRect);
  }
}

TEST_F(ImageAnimationFrameBuffer, RecyclingRect) {
  const size_t kThreshold = 5;
  const size_t kBatch = 2;
  const size_t kStartFrame = 0;
  const gfx::IntSize kImageSize(100, 100);
  const gfx::IntRect kImageRect(gfx::IntPoint(0, 0), kImageSize);
  AnimationFrameRetainedBuffer retained(kThreshold, kBatch, kStartFrame);

  // Let's get to the recycling state while marking all of the frames as not
  // recyclable, just like AnimationFrameBuffer / the decoders would do.
  RefPtr<imgFrame> frame;
  frame = CreateEmptyFrame(kImageSize, kImageRect, false);
  AnimationFrameBuffer::InsertStatus status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);

  frame = CreateEmptyFrame(kImageSize, kImageRect, false);
  status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);

  frame = CreateEmptyFrame(kImageSize, kImageRect, false);
  status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::CONTINUE, status);

  frame = CreateEmptyFrame(kImageSize, kImageRect, false);
  status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);

  VerifyAdvance(retained, 1, false);
  VerifyAdvance(retained, 2, true);
  VerifyAdvance(retained, 3, false);

  frame = CreateEmptyFrame(kImageSize, kImageRect, false);
  status = retained.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::DISCARD_CONTINUE, status);

  AnimationFrameRecyclingQueue buffer(std::move(retained));

  // The first frame is now the candidate for recycling. Since it was marked as
  // not recyclable, we should get nothing.
  VerifyAdvance(buffer, 4, false);

  gfx::IntRect recycleRect;
  EXPECT_FALSE(buffer.Recycle().empty());
  RawAccessFrameRef frameRef = buffer.RecycleFrame(recycleRect);
  EXPECT_TRUE(frameRef);
  EXPECT_FALSE(ReinitForRecycle(frameRef));
  EXPECT_TRUE(buffer.Recycle().empty());

  // Insert a recyclable partial frame. Its dirty rect shouldn't matter since
  // the previous frame was not recyclable.
  frame = CreateEmptyFrame(kImageSize, gfx::IntRect(0, 0, 25, 25));
  status = buffer.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);

  VerifyAdvance(buffer, 5, true);
  EXPECT_FALSE(buffer.Recycle().empty());
  frameRef = buffer.RecycleFrame(recycleRect);
  EXPECT_TRUE(frameRef);
  EXPECT_FALSE(ReinitForRecycle(frameRef));
  EXPECT_TRUE(buffer.Recycle().empty());

  // Insert a recyclable partial frame. Its dirty rect should match the recycle
  // rect since it is the only frame in the buffer.
  frame = CreateEmptyFrame(kImageSize, gfx::IntRect(25, 0, 50, 50));
  status = buffer.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);

  VerifyAdvance(buffer, 6, true);
  EXPECT_FALSE(buffer.Recycle().empty());
  frameRef = buffer.RecycleFrame(recycleRect);
  EXPECT_TRUE(frameRef);
  EXPECT_TRUE(ReinitForRecycle(frameRef));
  EXPECT_EQ(gfx::IntRect(25, 0, 50, 50), recycleRect);
  EXPECT_TRUE(buffer.Recycle().empty());

  // Insert the last frame and mark us as complete. The next recycled frame is
  // producing the first frame again, so we should use the first frame refresh
  // area instead of its dirty rect.
  frame = CreateEmptyFrame(kImageSize, gfx::IntRect(10, 10, 60, 10));
  status = buffer.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);

  bool continueDecoding = buffer.MarkComplete(gfx::IntRect(0, 0, 75, 50));
  EXPECT_FALSE(continueDecoding);

  VerifyAdvance(buffer, 7, true);
  EXPECT_FALSE(buffer.Recycle().empty());
  frameRef = buffer.RecycleFrame(recycleRect);
  EXPECT_TRUE(frameRef);
  EXPECT_TRUE(ReinitForRecycle(frameRef));
  EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect);
  EXPECT_TRUE(buffer.Recycle().empty());

  // Now let's reinsert the first frame. The recycle rect should still be the
  // first frame refresh area instead of the dirty rect of the first frame (e.g.
  // the full frame).
  frame = CreateEmptyFrame(kImageSize, kImageRect, false);
  status = buffer.Insert(std::move(frame));
  EXPECT_EQ(AnimationFrameBuffer::InsertStatus::YIELD, status);

  VerifyAdvance(buffer, 0, true);
  EXPECT_FALSE(buffer.Recycle().empty());
  frameRef = buffer.RecycleFrame(recycleRect);
  EXPECT_TRUE(frameRef);
  EXPECT_TRUE(ReinitForRecycle(frameRef));
  EXPECT_EQ(gfx::IntRect(0, 0, 75, 50), recycleRect);
  EXPECT_TRUE(buffer.Recycle().empty());
}
