// Copyright (c) 2006-2008 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 <vector>

#include "base/message_loop.h"
#include "chrome/browser/renderer_security_policy.h"
#include "chrome/browser/resource_dispatcher_host.h"
#include "chrome/common/render_messages.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_job.h"
#include "net/url_request/url_request_test_job.h"
#include "testing/gtest/include/gtest/gtest.h"

static int RequestIDForMessage(const IPC::Message& msg) {
  int request_id = -1;
  switch (msg.type()) {
    case ViewMsg_Resource_UploadProgress::ID:
    case ViewMsg_Resource_ReceivedResponse::ID:
    case ViewMsg_Resource_ReceivedRedirect::ID:
    case ViewMsg_Resource_DataReceived::ID:
    case ViewMsg_Resource_RequestComplete::ID:
      request_id = IPC::MessageIterator(msg).NextInt();
      break;
  }
  return request_id;
}

static ViewHostMsg_Resource_Request CreateResourceRequest(const char* method,
                                                          const GURL& url) {
  ViewHostMsg_Resource_Request request;
  request.method = std::string(method);
  request.url = url;
  request.policy_url = url;  // bypass third-party cookie blocking
  // init the rest to default values to prevent getting UMR.
  request.load_flags = 0;
  request.origin_pid = 0;
  request.resource_type = ResourceType::SUB_RESOURCE;
  request.mixed_content = false;
  return request;
}

// We may want to move this to a shared space if it is useful for something else
class ResourceIPCAccumulator {
 public:
  void AddMessage(const IPC::Message& msg) {
    messages_.push_back(msg);
  }

  // This groups the messages by their request ID. The groups will be in order
  // that the first message for each request ID was received, and the messages
  // within the groups will be in the order that they appeared.
  typedef std::vector< std::vector<IPC::Message> > ClassifiedMessages;
  void GetClassifiedMessages(ClassifiedMessages* msgs);

  std::vector<IPC::Message> messages_;
};

// This is very inefficient as a result of repeatedly extracting the ID, use
// only for tests!
void ResourceIPCAccumulator::GetClassifiedMessages(ClassifiedMessages* msgs) {
  while (!messages_.empty()) {
    std::vector<IPC::Message> cur_requests;
    cur_requests.push_back(messages_[0]);
    int cur_id = RequestIDForMessage(messages_[0]);

    // find all other messages with this ID
    for (int i = 1; i < static_cast<int>(messages_.size()); i++) {
      int id = RequestIDForMessage(messages_[i]);
      if (id == cur_id) {
        cur_requests.push_back(messages_[i]);
        messages_.erase(messages_.begin() + i);
        i --;
      }
    }
    messages_.erase(messages_.begin());
    msgs->push_back(cur_requests);
  }
}

class ResourceDispatcherHostTest : public testing::Test,
                                   public ResourceDispatcherHost::Receiver {
 public:
  ResourceDispatcherHostTest() : host_(NULL) {
  }
  // ResourceDispatcherHost::Delegate implementation
  virtual bool Send(IPC::Message* msg) {
    accum_.AddMessage(*msg);
    delete msg;
    return true;
  }

 protected:
  // testing::Test
  virtual void SetUp() {
    RendererSecurityPolicy::GetInstance()->Add(0);
    URLRequest::RegisterProtocolFactory("test", &URLRequestTestJob::Factory);
    EnsureTestSchemeIsAllowed();
  }
  virtual void TearDown() {
    URLRequest::RegisterProtocolFactory("test", NULL);
    RendererSecurityPolicy::GetInstance()->Remove(0);

    // Flush the message loop to make Purify happy.
    message_loop_.RunAllPending();
  }

  void MakeTestRequest(int request_id, const GURL& url);
  void MakeCancelRequest(int request_id);

  void EnsureTestSchemeIsAllowed() {
    static bool have_white_listed_test_scheme = false;

    if (!have_white_listed_test_scheme) {
      RendererSecurityPolicy::GetInstance()->RegisterWebSafeScheme("test");
      have_white_listed_test_scheme = true;
    }
  }

  MessageLoopForIO message_loop_;
  ResourceDispatcherHost host_;
  ResourceIPCAccumulator accum_;
};

// Spin up the message loop to kick off the request.
static void KickOffRequest() {
  MessageLoop::current()->RunAllPending();
}

void ResourceDispatcherHostTest::MakeTestRequest(int request_id,
                                                 const GURL& url) {
  ViewHostMsg_Resource_Request request = CreateResourceRequest("GET", url);

  host_.BeginRequest(this, GetCurrentProcess(), 0, MSG_ROUTING_NONE,
                     request_id, request, NULL, NULL);
  KickOffRequest();
}

void ResourceDispatcherHostTest::MakeCancelRequest(int request_id) {
  host_.CancelRequest(0, request_id, false);
}

void CheckSuccessfulRequest(const std::vector<IPC::Message>& messages,
                            const std::string& reference_data) {
  // A successful request will have received 4 messages:
  //     ReceivedResponse    (indicates headers received)
  //     DataReceived        (data)
  //    XXX DataReceived        (0 bytes remaining from a read)
  //     RequestComplete     (request is done)
  //
  // This function verifies that we received 4 messages and that they
  // are appropriate.
  ASSERT_EQ(messages.size(), 3);

  // The first messages should be received response
  ASSERT_EQ(ViewMsg_Resource_ReceivedResponse::ID, messages[0].type());

  // followed by the data, currently we only do the data in one chunk, but
  // should probably test multiple chunks later
  ASSERT_EQ(ViewMsg_Resource_DataReceived::ID, messages[1].type());

  void* iter = NULL;
  int request_id;
  ASSERT_TRUE(IPC::ReadParam(&messages[1], &iter, &request_id));
  SharedMemoryHandle shm_handle;
  ASSERT_TRUE(IPC::ReadParam(&messages[1], &iter, &shm_handle));
  int data_len;
  ASSERT_TRUE(IPC::ReadParam(&messages[1], &iter, &data_len));

  ASSERT_EQ(reference_data.size(), data_len);
  SharedMemory shared_mem(shm_handle, true);  // read only
  shared_mem.Map(data_len);
  const char* data = static_cast<char*>(shared_mem.memory());
  ASSERT_EQ(0, memcmp(reference_data.c_str(), data, data_len));

  // followed by a 0-byte read
  //ASSERT_EQ(ViewMsg_Resource_DataReceived::ID, messages[2].type());

  // the last message should be all data received
  ASSERT_EQ(ViewMsg_Resource_RequestComplete::ID, messages[2].type());
}

// Tests whether many messages get dispatched properly.
TEST_F(ResourceDispatcherHostTest, TestMany) {
  MakeTestRequest(1, URLRequestTestJob::test_url_1());
  MakeTestRequest(2, URLRequestTestJob::test_url_2());
  MakeTestRequest(3, URLRequestTestJob::test_url_3());

  // flush all the pending requests
  while (URLRequestTestJob::ProcessOnePendingMessage());

  // sorts out all the messages we saw by request
  ResourceIPCAccumulator::ClassifiedMessages msgs;
  accum_.GetClassifiedMessages(&msgs);

  // there are three requests, so we should have gotten them classified as such
  ASSERT_EQ(3, msgs.size());

  CheckSuccessfulRequest(msgs[0], URLRequestTestJob::test_data_1());
  CheckSuccessfulRequest(msgs[1], URLRequestTestJob::test_data_2());
  CheckSuccessfulRequest(msgs[2], URLRequestTestJob::test_data_3());
}

// Tests whether messages get canceled properly. We issue three requests,
// cancel one of them, and make sure that each sent the proper notifications.
TEST_F(ResourceDispatcherHostTest, Cancel) {
  ResourceDispatcherHost host(NULL);

  MakeTestRequest(1, URLRequestTestJob::test_url_1());
  MakeTestRequest(2, URLRequestTestJob::test_url_2());
  MakeTestRequest(3, URLRequestTestJob::test_url_3());
  MakeCancelRequest(2);

  // flush all the pending requests
  while (URLRequestTestJob::ProcessOnePendingMessage());

  ResourceIPCAccumulator::ClassifiedMessages msgs;
  accum_.GetClassifiedMessages(&msgs);

  // there are three requests, so we should have gotten them classified as such
  ASSERT_EQ(3, msgs.size());

  CheckSuccessfulRequest(msgs[0], URLRequestTestJob::test_data_1());
  CheckSuccessfulRequest(msgs[2], URLRequestTestJob::test_data_3());

  // Check that request 2 got canceled before it finished reading, which gives
  // us 1 ReceivedResponse message.
  ASSERT_EQ(1, msgs[1].size());
  ASSERT_EQ(ViewMsg_Resource_ReceivedResponse::ID, msgs[1][0].type());

  // TODO(mbelshe):
  // Now that the async IO path is in place, the IO always completes on the
  // initial call; so the cancel doesn't arrive until after we finished.
  // This basically means the test doesn't work.
#if 0
  int request_id;
  URLRequestStatus status;

  // The message should be all data received with an error.
  ASSERT_EQ(ViewMsg_Resource_RequestComplete::ID, msgs[1][2].type());

  void* iter = NULL;
  ASSERT_TRUE(IPC::ReadParam(&msgs[1][2], &iter, &request_id));
  ASSERT_TRUE(IPC::ReadParam(&msgs[1][2], &iter, &status));

  EXPECT_EQ(URLRequestStatus::CANCELED, status.status());
#endif
}

// Tests CancelRequestsForProcess
TEST_F(ResourceDispatcherHostTest, TestProcessCancel) {
  // the host delegate acts as a second one so we can have some requests
  // pending and some canceled
  class TestReceiver : public ResourceDispatcherHost::Receiver {
   public:
    TestReceiver() : has_canceled_(false), received_after_canceled_(0) {
    }
    virtual bool Send(IPC::Message* msg) {
      // no messages should be received when the process has been canceled
      if (has_canceled_)
        received_after_canceled_ ++;
      delete msg;
      return true;
    }
    bool has_canceled_;
    int received_after_canceled_;
  };
  TestReceiver test_receiver;

  // request 1 goes to the test delegate
  ViewHostMsg_Resource_Request request =
      CreateResourceRequest("GET", URLRequestTestJob::test_url_1());

  host_.BeginRequest(&test_receiver, GetCurrentProcess(), 0, MSG_ROUTING_NONE,
                     1, request, NULL, NULL);
  KickOffRequest();

  // request 2 goes to us
  MakeTestRequest(2, URLRequestTestJob::test_url_2());

  // request 3 goes to the test delegate
  request.url = URLRequestTestJob::test_url_3();
  host_.BeginRequest(&test_receiver, GetCurrentProcess(), 0, MSG_ROUTING_NONE,
                     3, request, NULL, NULL);
  KickOffRequest();

  // TODO: mbelshe
  // Now that the async IO path is in place, the IO always completes on the
  // initial call; so the requests have already completed.  This basically
  // breaks the whole test.
  //EXPECT_EQ(3, host_.pending_requests());

  // process each request for one level so one callback is called
  for (int i = 0; i < 3; i++)
    EXPECT_TRUE(URLRequestTestJob::ProcessOnePendingMessage());

  // cancel the requests to the test process
  host_.CancelRequestsForProcess(0);
  test_receiver.has_canceled_ = true;

  // flush all the pending requests
  while (URLRequestTestJob::ProcessOnePendingMessage());

  EXPECT_EQ(0, host_.pending_requests());

  // the test delegate should not have gotten any messages after being canceled
  ASSERT_EQ(0, test_receiver.received_after_canceled_);

  // we should have gotten exactly one result
  ResourceIPCAccumulator::ClassifiedMessages msgs;
  accum_.GetClassifiedMessages(&msgs);
  ASSERT_EQ(1, msgs.size());
  CheckSuccessfulRequest(msgs[0], URLRequestTestJob::test_data_2());
}