// 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 "base/basictypes.h"
#include "base/run_loop.h"
#include "content/browser/service_worker/embedded_worker_registry.h"
#include "content/browser/service_worker/embedded_worker_test_helper.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_registration.h"
#include "content/browser/service_worker/service_worker_test_utils.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "testing/gtest/include/gtest/gtest.h"

// IPC messages for testing ---------------------------------------------------

#define IPC_MESSAGE_IMPL
#include "ipc/ipc_message_macros.h"

#define IPC_MESSAGE_START TestMsgStart

IPC_MESSAGE_CONTROL0(TestMsg_Message);
IPC_MESSAGE_ROUTED1(TestMsg_MessageFromWorker, int);

// ---------------------------------------------------------------------------

namespace content {

namespace {

static const int kRenderProcessId = 1;

class MessageReceiver : public EmbeddedWorkerTestHelper {
 public:
  MessageReceiver()
      : EmbeddedWorkerTestHelper(kRenderProcessId),
        current_embedded_worker_id_(0) {}
  virtual ~MessageReceiver() {}

  virtual bool OnMessageToWorker(int thread_id,
                                 int embedded_worker_id,
                                 const IPC::Message& message) OVERRIDE {
    if (EmbeddedWorkerTestHelper::OnMessageToWorker(
            thread_id, embedded_worker_id, message)) {
      return true;
    }
    current_embedded_worker_id_ = embedded_worker_id;
    bool handled = true;
    IPC_BEGIN_MESSAGE_MAP(MessageReceiver, message)
      IPC_MESSAGE_HANDLER(TestMsg_Message, OnMessage)
      IPC_MESSAGE_UNHANDLED(handled = false)
    IPC_END_MESSAGE_MAP()
    return handled;
  }

  void SimulateSendValueToBrowser(int embedded_worker_id, int value) {
    SimulateSend(new TestMsg_MessageFromWorker(embedded_worker_id, value));
  }

 private:
  void OnMessage() {
    // Do nothing.
  }

  int current_embedded_worker_id_;
  DISALLOW_COPY_AND_ASSIGN(MessageReceiver);
};

void VerifyCalled(bool* called) {
  *called = true;
}

void ObserveStatusChanges(ServiceWorkerVersion* version,
                          std::vector<ServiceWorkerVersion::Status>* statuses) {
  statuses->push_back(version->status());
  version->RegisterStatusChangeCallback(
      base::Bind(&ObserveStatusChanges, base::Unretained(version), statuses));
}

// A specialized listener class to receive test messages from a worker.
class MessageReceiverFromWorker : public EmbeddedWorkerInstance::Listener {
 public:
  explicit MessageReceiverFromWorker(EmbeddedWorkerInstance* instance)
      : instance_(instance) {
    instance_->AddListener(this);
  }
  virtual ~MessageReceiverFromWorker() {
    instance_->RemoveListener(this);
  }

  virtual void OnStarted() OVERRIDE { NOTREACHED(); }
  virtual void OnStopped() OVERRIDE { NOTREACHED(); }
  virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE {
    bool handled = true;
    IPC_BEGIN_MESSAGE_MAP(MessageReceiverFromWorker, message)
      IPC_MESSAGE_HANDLER(TestMsg_MessageFromWorker, OnMessageFromWorker)
      IPC_MESSAGE_UNHANDLED(handled = false)
    IPC_END_MESSAGE_MAP()
    return handled;
  }

  void OnMessageFromWorker(int value) { received_values_.push_back(value); }
  const std::vector<int>& received_values() const { return received_values_; }

 private:
  EmbeddedWorkerInstance* instance_;
  std::vector<int> received_values_;
  DISALLOW_COPY_AND_ASSIGN(MessageReceiverFromWorker);
};

}  // namespace

class ServiceWorkerVersionTest : public testing::Test {
 protected:
  ServiceWorkerVersionTest()
      : thread_bundle_(TestBrowserThreadBundle::IO_MAINLOOP) {}

  virtual void SetUp() OVERRIDE {
    helper_.reset(new MessageReceiver());

    registration_ = new ServiceWorkerRegistration(
        GURL("http://www.example.com/"),
        GURL("http://www.example.com/service_worker.js"),
        1L,
        helper_->context()->AsWeakPtr());
    version_ = new ServiceWorkerVersion(
        registration_, 1L, helper_->context()->AsWeakPtr());

    // Simulate adding one process to the worker.
    int embedded_worker_id = version_->embedded_worker()->embedded_worker_id();
    helper_->SimulateAddProcessToWorker(embedded_worker_id, kRenderProcessId);
    ASSERT_TRUE(version_->HasProcessToRun());
  }

  virtual void TearDown() OVERRIDE {
    version_ = 0;
    registration_ = 0;
    helper_.reset();
  }

  TestBrowserThreadBundle thread_bundle_;
  scoped_ptr<MessageReceiver> helper_;
  scoped_refptr<ServiceWorkerRegistration> registration_;
  scoped_refptr<ServiceWorkerVersion> version_;

 private:
  DISALLOW_COPY_AND_ASSIGN(ServiceWorkerVersionTest);
};

TEST_F(ServiceWorkerVersionTest, ConcurrentStartAndStop) {
  // Call StartWorker() multiple times.
  ServiceWorkerStatusCode status1 = SERVICE_WORKER_ERROR_FAILED;
  ServiceWorkerStatusCode status2 = SERVICE_WORKER_ERROR_FAILED;
  ServiceWorkerStatusCode status3 = SERVICE_WORKER_ERROR_FAILED;
  version_->StartWorker(CreateReceiverOnCurrentThread(&status1));
  version_->StartWorker(CreateReceiverOnCurrentThread(&status2));

  EXPECT_EQ(ServiceWorkerVersion::STARTING, version_->running_status());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(ServiceWorkerVersion::RUNNING, version_->running_status());

  // Call StartWorker() after it's started.
  version_->StartWorker(CreateReceiverOnCurrentThread(&status3));
  base::RunLoop().RunUntilIdle();

  // All should just succeed.
  EXPECT_EQ(SERVICE_WORKER_OK, status1);
  EXPECT_EQ(SERVICE_WORKER_OK, status2);
  EXPECT_EQ(SERVICE_WORKER_OK, status3);

  // Call StopWorker() multiple times.
  status1 = SERVICE_WORKER_ERROR_FAILED;
  status2 = SERVICE_WORKER_ERROR_FAILED;
  status3 = SERVICE_WORKER_ERROR_FAILED;
  version_->StopWorker(CreateReceiverOnCurrentThread(&status1));
  version_->StopWorker(CreateReceiverOnCurrentThread(&status2));

  // Also try calling StartWorker while StopWorker is in queue.
  version_->StartWorker(CreateReceiverOnCurrentThread(&status3));

  EXPECT_EQ(ServiceWorkerVersion::STOPPING, version_->running_status());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(ServiceWorkerVersion::STOPPED, version_->running_status());

  // All StopWorker should just succeed, while StartWorker fails.
  EXPECT_EQ(SERVICE_WORKER_OK, status1);
  EXPECT_EQ(SERVICE_WORKER_OK, status2);
  EXPECT_EQ(SERVICE_WORKER_ERROR_START_WORKER_FAILED, status3);
}

TEST_F(ServiceWorkerVersionTest, SendMessage) {
  EXPECT_EQ(ServiceWorkerVersion::STOPPED, version_->running_status());

  // Send a message without starting the worker.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_FAILED;
  version_->SendMessage(TestMsg_Message(),
                        CreateReceiverOnCurrentThread(&status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, status);

  // The worker should be now started.
  EXPECT_EQ(ServiceWorkerVersion::RUNNING, version_->running_status());

  // Stop the worker, and then send the message immediately.
  ServiceWorkerStatusCode msg_status = SERVICE_WORKER_ERROR_FAILED;
  ServiceWorkerStatusCode stop_status = SERVICE_WORKER_ERROR_FAILED;
  version_->StopWorker(CreateReceiverOnCurrentThread(&stop_status));
  version_->SendMessage(TestMsg_Message(),
                       CreateReceiverOnCurrentThread(&msg_status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, stop_status);

  // SendMessage should return START_WORKER_FAILED error since it tried to
  // start a worker while it was stopping.
  EXPECT_EQ(SERVICE_WORKER_ERROR_START_WORKER_FAILED, msg_status);
}

TEST_F(ServiceWorkerVersionTest, ReSendMessageAfterStop) {
  EXPECT_EQ(ServiceWorkerVersion::STOPPED, version_->running_status());

  // Start the worker.
  ServiceWorkerStatusCode start_status = SERVICE_WORKER_ERROR_FAILED;
  version_->StartWorker(CreateReceiverOnCurrentThread(&start_status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, start_status);
  EXPECT_EQ(ServiceWorkerVersion::RUNNING, version_->running_status());

  // Stop the worker, and then send the message immediately.
  ServiceWorkerStatusCode msg_status = SERVICE_WORKER_ERROR_FAILED;
  ServiceWorkerStatusCode stop_status = SERVICE_WORKER_ERROR_FAILED;
  version_->StopWorker(CreateReceiverOnCurrentThread(&stop_status));
  version_->SendMessage(TestMsg_Message(),
                       CreateReceiverOnCurrentThread(&msg_status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, stop_status);

  // SendMessage should return START_WORKER_FAILED error since it tried to
  // start a worker while it was stopping.
  EXPECT_EQ(SERVICE_WORKER_ERROR_START_WORKER_FAILED, msg_status);

  // Resend the message, which should succeed and restart the worker.
  version_->SendMessage(TestMsg_Message(),
                       CreateReceiverOnCurrentThread(&msg_status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, msg_status);
  EXPECT_EQ(ServiceWorkerVersion::RUNNING, version_->running_status());
}

TEST_F(ServiceWorkerVersionTest, ReceiveMessageFromWorker) {
  MessageReceiverFromWorker receiver(version_->embedded_worker());

  // Simulate sending some dummy values from the worker.
  helper_->SimulateSendValueToBrowser(
      version_->embedded_worker()->embedded_worker_id(), 555);
  helper_->SimulateSendValueToBrowser(
      version_->embedded_worker()->embedded_worker_id(), 777);

  // Verify the receiver received the values.
  ASSERT_EQ(2U, receiver.received_values().size());
  EXPECT_EQ(555, receiver.received_values()[0]);
  EXPECT_EQ(777, receiver.received_values()[1]);
}

TEST_F(ServiceWorkerVersionTest, InstallAndWaitCompletion) {
  version_->SetStatus(ServiceWorkerVersion::INSTALLING);

  // Dispatch an install event.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_FAILED;
  version_->DispatchInstallEvent(-1, CreateReceiverOnCurrentThread(&status));

  // Wait for the completion.
  bool status_change_called = false;
  version_->RegisterStatusChangeCallback(
      base::Bind(&VerifyCalled, &status_change_called));

  base::RunLoop().RunUntilIdle();

  // Version's status must not have changed during installation.
  EXPECT_EQ(SERVICE_WORKER_OK, status);
  EXPECT_FALSE(status_change_called);
  EXPECT_EQ(ServiceWorkerVersion::INSTALLING, version_->status());
}

TEST_F(ServiceWorkerVersionTest, ActivateAndWaitCompletion) {
  version_->SetStatus(ServiceWorkerVersion::ACTIVATING);

  // Dispatch an activate event.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_FAILED;
  version_->DispatchActivateEvent(CreateReceiverOnCurrentThread(&status));

  // Wait for the completion.
  bool status_change_called = false;
  version_->RegisterStatusChangeCallback(
      base::Bind(&VerifyCalled, &status_change_called));

  base::RunLoop().RunUntilIdle();

  // Version's status must not have changed during activation.
  EXPECT_EQ(SERVICE_WORKER_OK, status);
  EXPECT_FALSE(status_change_called);
  EXPECT_EQ(ServiceWorkerVersion::ACTIVATING, version_->status());
}

TEST_F(ServiceWorkerVersionTest, RepeatedlyObserveStatusChanges) {
  EXPECT_EQ(ServiceWorkerVersion::NEW, version_->status());

  // Repeatedly observe status changes (the callback re-registers itself).
  std::vector<ServiceWorkerVersion::Status> statuses;
  version_->RegisterStatusChangeCallback(
      base::Bind(&ObserveStatusChanges, version_, &statuses));

  version_->SetStatus(ServiceWorkerVersion::INSTALLING);
  version_->SetStatus(ServiceWorkerVersion::INSTALLED);
  version_->SetStatus(ServiceWorkerVersion::ACTIVATING);
  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
  version_->SetStatus(ServiceWorkerVersion::REDUNDANT);

  // Verify that we could successfully observe repeated status changes.
  ASSERT_EQ(5U, statuses.size());
  ASSERT_EQ(ServiceWorkerVersion::INSTALLING, statuses[0]);
  ASSERT_EQ(ServiceWorkerVersion::INSTALLED, statuses[1]);
  ASSERT_EQ(ServiceWorkerVersion::ACTIVATING, statuses[2]);
  ASSERT_EQ(ServiceWorkerVersion::ACTIVATED, statuses[3]);
  ASSERT_EQ(ServiceWorkerVersion::REDUNDANT, statuses[4]);
}

TEST_F(ServiceWorkerVersionTest, AddAndRemoveProcesses) {
  // Preparation (to reset the process count to 0).
  ASSERT_TRUE(version_->HasProcessToRun());
  version_->RemoveProcessFromWorker(kRenderProcessId);
  ASSERT_FALSE(version_->HasProcessToRun());

  // Add another process to the worker twice, and then remove process once.
  const int another_process_id = kRenderProcessId + 1;
  version_->AddProcessToWorker(another_process_id);
  version_->AddProcessToWorker(another_process_id);
  version_->RemoveProcessFromWorker(another_process_id);

  // We're ref-counting the process internally, so adding the same process
  // multiple times should be handled correctly.
  ASSERT_TRUE(version_->HasProcessToRun());

  // Removing the process again (so that # of AddProcess == # of RemoveProcess
  // for the process) should remove all process references.
  version_->RemoveProcessFromWorker(another_process_id);
  ASSERT_FALSE(version_->HasProcessToRun());
}

TEST_F(ServiceWorkerVersionTest, ScheduleStopWorker) {
  // Verify the timer is not running when version initializes its status.
  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
  EXPECT_FALSE(version_->stop_worker_timer_.IsRunning());

  // Verify the timer is running when version status changes frome ACTIVATING
  // to ACTIVATED.
  ServiceWorkerStatusCode status = SERVICE_WORKER_ERROR_FAILED;
  version_->StartWorker(CreateReceiverOnCurrentThread(&status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, status);
  version_->SetStatus(ServiceWorkerVersion::ACTIVATING);
  version_->SetStatus(ServiceWorkerVersion::ACTIVATED);
  EXPECT_TRUE(version_->stop_worker_timer_.IsRunning());

  // The timer should be running if the worker is restarted without controllee.
  status = SERVICE_WORKER_ERROR_FAILED;
  version_->StopWorker(CreateReceiverOnCurrentThread(&status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, status);
  status = SERVICE_WORKER_ERROR_FAILED;
  version_->StartWorker(CreateReceiverOnCurrentThread(&status));
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(SERVICE_WORKER_OK, status);
  EXPECT_TRUE(version_->stop_worker_timer_.IsRunning());

  // The timer should not be running if a controllee is added.
  scoped_ptr<ServiceWorkerProviderHost> host(
      new ServiceWorkerProviderHost(33 /* dummy render process id */,
                                    1 /* dummy provider_id */,
                                    helper_->context()->AsWeakPtr(),
                                    NULL));
  version_->AddControllee(host.get());
  EXPECT_FALSE(version_->stop_worker_timer_.IsRunning());

  // The timer should be running if the controllee is removed.
  version_->RemoveControllee(host.get());
  EXPECT_TRUE(version_->stop_worker_timer_.IsRunning());
}

}  // namespace content
