/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


#include <unistd.h>
#include <memory>
#include <vector>

#include "pagespeed/controller/context_registry.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
#include "pagespeed/kernel/thread/queued_worker_pool.h"
#include "pagespeed/kernel/util/platform.h"

using testing::AtMost;
using testing::Eq;
using testing::Gt;
using testing::Invoke;
using testing::Lt;

namespace net_instaweb {

namespace {

// Trivial mock object that can be put into a ContextRegistry.
class MockContext {
 public:
  MockContext() {
    EXPECT_CALL(*this, TryCancel()).Times(0);
  }

  MOCK_METHOD0(TryCancel, void());
};

class ContextRegistryTest : public testing::Test {
 public:
  ContextRegistryTest()
      : thread_system_(Platform::CreateThreadSystem()),
        worker_(1 /* max_workers */, "context_registry_test",
                thread_system_.get()),
        sequence_(worker_.NewSequence()),
        registry_(thread_system_.get()) {}

  ~ContextRegistryTest() override { worker_.FreeSequence(sequence_); }

  // MakeFunction doesn't know how to deal with protected, grumble.
  void RemoveContexts(std::vector<MockContext>* contexts,
                      WorkerTestBase::SyncPoint* sync) {
    sync->Notify();
    for (MockContext& context : *contexts) {
      registry_.RemoveContext(&context);
      usleep(1);  // yield.
    }
  }

 protected:
  typedef ContextRegistry<MockContext> MockRegistry;

  std::unique_ptr<ThreadSystem> thread_system_;
  QueuedWorkerPool worker_;
  QueuedWorkerPool::Sequence* sequence_;

  MockRegistry registry_;
};

TEST_F(ContextRegistryTest, CancelEmptyDoesntBlock) {
  EXPECT_THAT(registry_.Empty(), Eq(true));
  EXPECT_THAT(registry_.IsShutdown(), Eq(false));
  registry_.CancelAllActiveAndWait();
  // We're verifying that Cancel doesn't wait indefinitely when empty. If that
  // happens the test will hang and require ^C or some other timeout.
  EXPECT_THAT(registry_.Empty(), Eq(true));
  EXPECT_THAT(registry_.IsShutdown(), Eq(true));
}

TEST_F(ContextRegistryTest, CantRegisterAfterShutdown) {
  MockContext ctx;
  registry_.CancelAllActive();
  EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(false));
  EXPECT_THAT(registry_.Empty(), Eq(true));
}

TEST_F(ContextRegistryTest, DoesntCancelOldEntries) {
  MockContext ctx;
  EXPECT_CALL(ctx, TryCancel()).Times(0);

  EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(true));
  EXPECT_THAT(registry_.Empty(), Eq(false));

  registry_.RemoveContext(&ctx);
  EXPECT_THAT(registry_.Empty(), Eq(true));

  // Should do precisely nothing.
  registry_.CancelAllActiveAndWait();
}

TEST_F(ContextRegistryTest, CancelsContainedItems) {
  MockContext ctx;
  // CancelAllActiveAndWait blocks until the registry is empty, so we need to
  // call RemoveContext on another thread. Here we setup "ctx" so that it
  // expects TryCancel to be called once. Once that happens, it will queue a
  // task on sequence_ that removes ctx from the registry.
  EXPECT_CALL(ctx, TryCancel()).WillOnce(Invoke([this, &ctx]() {
    sequence_->Add(
        MakeFunction(&registry_, &MockRegistry::RemoveContext, &ctx));
  }));

  EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(true));
  EXPECT_THAT(registry_.Empty(), Eq(false));

  registry_.CancelAllActiveAndWait();  // Should block.
  EXPECT_THAT(registry_.Empty(), Eq(true));
}

TEST_F(ContextRegistryTest, NoWaitDoesnWait) {
  MockContext ctx;
  EXPECT_CALL(ctx, TryCancel());

  EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(true));
  EXPECT_THAT(registry_.Empty(), Eq(false));

  registry_.CancelAllActive();  // Must not block, even though not empty.
  EXPECT_THAT(registry_.Empty(), Eq(false));

  registry_.RemoveContext(&ctx);
  EXPECT_THAT(registry_.Empty(), Eq(true));
}

TEST_F(ContextRegistryTest, RemovalsWhileCanceling) {
  // We want to verify that nothing bad happens if a context is removed by
  // another thread while CancelAllActiveAndWait() is running. So, we put a
  // bunch of things in the registry, start a thread that removes them and call
  // CancelAllActiveAndWait() in parallel. It would be nice to force
  // synchronization here, but that's tricky because we can't call back into the
  // registry from TryCancel() because the lock is held.
  static const int kNumContexts = 1000;

  int num_canceled = 0;

  std::vector<MockContext> contexts(kNumContexts);
  for (MockContext& context : contexts) {
    EXPECT_CALL(context, TryCancel())
        .Times(AtMost(1))
        // Calls to TryCancel are in series via this thread, so there's no
        // concurrency issue on num_canceled.
        .WillOnce(Invoke([&num_canceled]() { ++num_canceled; }));
    ASSERT_THAT(registry_.TryRegisterContext(&context), Eq(true));
  }

  // Queue the task that unregisters everything from the registry.
  WorkerTestBase::SyncPoint sync(thread_system_.get());
  sequence_->Add(MakeFunction<ContextRegistryTest>(
      this, &ContextRegistryTest::RemoveContexts, &contexts, &sync));

  // Wait for the task to start, then start notifying everything in the
  // registry.
  sync.Wait();
  registry_.CancelAllActiveAndWait();

  EXPECT_THAT(registry_.Empty(), Eq(true));

  // Verify that some contexts were canceled (> 0) and some were removed
  // (< kNumContexts).
  EXPECT_THAT(num_canceled, Gt(0));
  EXPECT_THAT(num_canceled, Lt(kNumContexts));
}

}  // namespace

}  // namespace net_instaweb
