// Copyright (c) 2011 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 <algorithm>

#include "base/test/test_timeouts.h"
#include "media/base/mock_callback.h"
#include "media/base/mock_filter_host.h"
#include "media/base/mock_filters.h"
#include "net/base/net_errors.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebURLError.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebURLResponse.h"
#include "webkit/glue/media/buffered_data_source.h"
#include "webkit/mocks/mock_webframe.h"

using ::testing::_;
using ::testing::Assign;
using ::testing::AtLeast;
using ::testing::DeleteArg;
using ::testing::DoAll;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::InvokeWithoutArgs;
using ::testing::NotNull;
using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::SetArgumentPointee;
using ::testing::StrictMock;
using ::testing::NiceMock;
using ::testing::WithArgs;

namespace webkit_glue {

static const char* kHttpUrl = "http://test";
static const char* kFileUrl = "file://test";
static const int kDataSize = 1024;

enum NetworkState {
  NONE,
  LOADED,
  LOADING
};

// A mock BufferedDataSource to inject mock BufferedResourceLoader through
// CreateResourceLoader() method.
class MockBufferedDataSource : public BufferedDataSource {
 public:
  MockBufferedDataSource(MessageLoop* message_loop, WebFrame* frame)
      : BufferedDataSource(message_loop, frame) {
  }

  virtual base::TimeDelta GetTimeoutMilliseconds() {
    return base::TimeDelta::FromMilliseconds(
                            TestTimeouts::tiny_timeout_ms());
  }

  MOCK_METHOD2(CreateResourceLoader,
               BufferedResourceLoader*(int64 first_position,
                                       int64 last_position));

 private:
  DISALLOW_COPY_AND_ASSIGN(MockBufferedDataSource);
};

class MockBufferedResourceLoader : public BufferedResourceLoader {
 public:
  MockBufferedResourceLoader() : BufferedResourceLoader(GURL(), 0, 0) {
  }

  MOCK_METHOD3(Start, void(net::CompletionCallback* read_callback,
                           NetworkEventCallback* network_callback,
                           WebFrame* frame));
  MOCK_METHOD0(Stop, void());
  MOCK_METHOD4(Read, void(int64 position, int read_size, uint8* buffer,
                          net::CompletionCallback* callback));
  MOCK_METHOD0(content_length, int64());
  MOCK_METHOD0(instance_size, int64());
  MOCK_METHOD0(range_supported, bool());
  MOCK_METHOD0(network_activity, bool());
  MOCK_METHOD0(url, const GURL&());
  MOCK_METHOD0(GetBufferedFirstBytePosition, int64());
  MOCK_METHOD0(GetBufferedLastBytePosition, int64());

 protected:
  ~MockBufferedResourceLoader() {}

  DISALLOW_COPY_AND_ASSIGN(MockBufferedResourceLoader);
};

class BufferedDataSourceTest : public testing::Test {
 public:
  BufferedDataSourceTest() {
    message_loop_ = MessageLoop::current();

    // Prepare test data.
    for (size_t i = 0; i < sizeof(data_); ++i) {
      data_[i] = i;
    }
  }

  virtual ~BufferedDataSourceTest() {
  }

  void ExpectCreateAndStartResourceLoader(int start_error) {
    EXPECT_CALL(*data_source_, CreateResourceLoader(_, _))
        .WillOnce(Return(loader_.get()));

    EXPECT_CALL(*loader_, Start(NotNull(), NotNull(), NotNull()))
        .WillOnce(
            DoAll(Assign(&error_, start_error),
                  Invoke(this,
                         &BufferedDataSourceTest::InvokeStartCallback)));
  }

  void InitializeDataSource(const char* url, int error,
                            bool partial_response, int64 instance_size,
                            NetworkState networkState) {
    // Saves the url first.
    gurl_ = GURL(url);

    frame_.reset(new NiceMock<MockWebFrame>());

    data_source_ = new MockBufferedDataSource(MessageLoop::current(),
                                              frame_.get());
    data_source_->set_host(&host_);

    scoped_refptr<NiceMock<MockBufferedResourceLoader> > first_loader(
        new NiceMock<MockBufferedResourceLoader>());

    // Creates the mock loader to be injected.
    loader_ = first_loader;

    bool initialized_ok = (error == net::OK);
    bool loaded = networkState == LOADED;
    {
      InSequence s;
      ExpectCreateAndStartResourceLoader(error);

      // In the case of an invalid partial response we expect a second loader
      // to be created.
      if (partial_response && (error == net::ERR_INVALID_RESPONSE)) {
        // Verify that the initial loader is stopped.
        EXPECT_CALL(*loader_, url())
            .WillRepeatedly(ReturnRef(gurl_));
        EXPECT_CALL(*loader_, Stop());

        // Replace loader_ with a new instance.
        loader_ = new NiceMock<MockBufferedResourceLoader>();

        // Create and start. Make sure Start() is called on the new loader.
        ExpectCreateAndStartResourceLoader(net::OK);

        // Update initialization variable since we know the second loader will
        // return OK.
        initialized_ok = true;
      }
    }

    // Attach a static function that deletes the memory referred by the
    // "callback" parameter.
    ON_CALL(*loader_, Read(_, _, _ , _))
        .WillByDefault(DeleteArg<3>());

    ON_CALL(*loader_, instance_size())
        .WillByDefault(Return(instance_size));

    // range_supported() return true if we expect to get a partial response.
    ON_CALL(*loader_, range_supported())
        .WillByDefault(Return(partial_response));

    ON_CALL(*loader_, url())
        .WillByDefault(ReturnRef(gurl_));
    media::PipelineStatus expected_init_status = media::PIPELINE_OK;
    if (initialized_ok) {
      // Expected loaded or not.
      EXPECT_CALL(host_, SetLoaded(loaded));

      // TODO(hclam): The condition for streaming needs to be adjusted.
      if (instance_size != -1 && (loaded || partial_response)) {
        EXPECT_CALL(host_, SetTotalBytes(instance_size));
        if (loaded)
          EXPECT_CALL(host_, SetBufferedBytes(instance_size));
        else
          EXPECT_CALL(host_, SetBufferedBytes(0));
      } else {
        EXPECT_CALL(host_, SetStreaming(true));
      }
    } else {
      expected_init_status = media::PIPELINE_ERROR_NETWORK;
      EXPECT_CALL(*loader_, Stop());
    }

    // Actual initialization of the data source.
    data_source_->Initialize(url,
        media::NewExpectedStatusCallback(expected_init_status));
    message_loop_->RunAllPending();

    if (initialized_ok) {
      // Verify the size of the data source.
      int64 size;
      if (instance_size != -1 && (loaded || partial_response)) {
        EXPECT_TRUE(data_source_->GetSize(&size));
        EXPECT_EQ(instance_size, size);
      } else {
        EXPECT_TRUE(data_source_->IsStreaming());
      }
    }
  }

  void StopDataSource() {
    if (loader_) {
      InSequence s;
      EXPECT_CALL(*loader_, Stop());
    }

    data_source_->Stop(media::NewExpectedCallback());
    message_loop_->RunAllPending();
  }

  void InvokeStartCallback(
      net::CompletionCallback* callback,
      BufferedResourceLoader::NetworkEventCallback* network_callback,
      WebFrame* frame) {
    callback->RunWithParams(Tuple1<int>(error_));
    delete callback;
    // TODO(hclam): Save this callback.
    delete network_callback;
  }

  void InvokeReadCallback(int64 position, int size, uint8* buffer,
                          net::CompletionCallback* callback) {
    if (error_ > 0)
      memcpy(buffer, data_ + static_cast<int>(position), error_);
    callback->RunWithParams(Tuple1<int>(error_));
    delete callback;
  }

  void ReadDataSourceHit(int64 position, int size, int read_size) {
    EXPECT_TRUE(loader_);

    InSequence s;
    // Expect the read is delegated to the resource loader.
    EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull()))
        .WillOnce(DoAll(Assign(&error_, read_size),
                        Invoke(this,
                               &BufferedDataSourceTest::InvokeReadCallback)));

    // The read has succeeded, so read callback will be called.
    EXPECT_CALL(*this, ReadCallback(read_size));

    data_source_->Read(
        position, size, buffer_,
        NewCallback(this, &BufferedDataSourceTest::ReadCallback));
    message_loop_->RunAllPending();

    // Make sure data is correct.
    EXPECT_EQ(0,
              memcmp(buffer_, data_ + static_cast<int>(position), read_size));
  }

  void ReadDataSourceHang(int64 position, int size) {
    EXPECT_TRUE(loader_);

    // Expect a call to read, but the call never returns.
    EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull()));
    data_source_->Read(
        position, size, buffer_,
        NewCallback(this, &BufferedDataSourceTest::ReadCallback));
    message_loop_->RunAllPending();

    // Now expect the read to return after aborting the data source.
    EXPECT_CALL(*this, ReadCallback(_));
    EXPECT_CALL(*loader_, Stop());
    data_source_->Abort();
    message_loop_->RunAllPending();

    // The loader has now been stopped. Set this to null so that when the
    // DataSource is stopped, it does not expect a call to stop the loader.
    loader_ = NULL;
  }

  void ReadDataSourceMiss(int64 position, int size, int start_error) {
    EXPECT_TRUE(loader_);

    // 1. Reply with a cache miss for the read.
    {
      InSequence s;
      EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull()))
          .WillOnce(DoAll(Assign(&error_, net::ERR_CACHE_MISS),
                          Invoke(this,
                                 &BufferedDataSourceTest::InvokeReadCallback)));
      EXPECT_CALL(*loader_, Stop());
    }

    // 2. Then the current loader will be stop and destroyed.
    NiceMock<MockBufferedResourceLoader> *new_loader =
        new NiceMock<MockBufferedResourceLoader>();
    EXPECT_CALL(*data_source_, CreateResourceLoader(position, -1))
        .WillOnce(Return(new_loader));

    // 3. Then the new loader will be started.
    EXPECT_CALL(*new_loader, Start(NotNull(), NotNull(), NotNull()))
        .WillOnce(DoAll(Assign(&error_, start_error),
                        Invoke(this,
                               &BufferedDataSourceTest::InvokeStartCallback)));

    if (start_error == net::OK) {
      EXPECT_CALL(*new_loader, range_supported())
          .WillRepeatedly(Return(loader_->range_supported()));

      // 4a. Then again a read request is made to the new loader.
      EXPECT_CALL(*new_loader, Read(position, size, NotNull(), NotNull()))
          .WillOnce(DoAll(Assign(&error_, size),
                          Invoke(this,
                                 &BufferedDataSourceTest::InvokeReadCallback)));

      EXPECT_CALL(*this, ReadCallback(size));
    } else {
      // 4b. The read callback is called with an error because Start() on the
      // new loader returned an error.
      EXPECT_CALL(*this, ReadCallback(media::DataSource::kReadError));
    }

    data_source_->Read(
        position, size, buffer_,
        NewCallback(this, &BufferedDataSourceTest::ReadCallback));
    message_loop_->RunAllPending();

    // Make sure data is correct.
    if (start_error == net::OK)
      EXPECT_EQ(0, memcmp(buffer_, data_ + static_cast<int>(position), size));

    loader_ = new_loader;
  }

  void ReadDataSourceFailed(int64 position, int size, int error) {
    EXPECT_TRUE(loader_);

    // 1. Expect the read is delegated to the resource loader.
    EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull()))
        .WillOnce(DoAll(Assign(&error_, error),
                        Invoke(this,
                               &BufferedDataSourceTest::InvokeReadCallback)));

    // 2. Host will then receive an error.
    EXPECT_CALL(*loader_, Stop());

    // 3. The read has failed, so read callback will be called.
    EXPECT_CALL(*this, ReadCallback(media::DataSource::kReadError));

    data_source_->Read(
        position, size, buffer_,
        NewCallback(this, &BufferedDataSourceTest::ReadCallback));

    message_loop_->RunAllPending();
  }

  void ReadDataSourceTimesOut(int64 position, int size) {
    // 1. Drop the request and let it times out.
    {
      InSequence s;
      EXPECT_CALL(*loader_, Read(position, size, NotNull(), NotNull()))
          .WillOnce(DeleteArg<3>());
      EXPECT_CALL(*loader_, Stop());
    }

    // 2. Then the current loader will be stop and destroyed.
    NiceMock<MockBufferedResourceLoader> *new_loader =
        new NiceMock<MockBufferedResourceLoader>();
    EXPECT_CALL(*data_source_, CreateResourceLoader(position, -1))
        .WillOnce(Return(new_loader));

    // 3. Then the new loader will be started and respond to queries about
    //    whether this is a partial response using the value of the previous
    //    loader.
    EXPECT_CALL(*new_loader, Start(NotNull(), NotNull(), NotNull()))
        .WillOnce(DoAll(Assign(&error_, net::OK),
                        Invoke(this,
                               &BufferedDataSourceTest::InvokeStartCallback)));
    EXPECT_CALL(*new_loader, range_supported())
        .WillRepeatedly(Return(loader_->range_supported()));

    // 4. Then again a read request is made to the new loader.
    EXPECT_CALL(*new_loader, Read(position, size, NotNull(), NotNull()))
        .WillOnce(DoAll(Assign(&error_, size),
                        Invoke(this,
                               &BufferedDataSourceTest::InvokeReadCallback),
                        InvokeWithoutArgs(message_loop_,
                                          &MessageLoop::Quit)));

    EXPECT_CALL(*this, ReadCallback(size));

    data_source_->Read(
        position, size, buffer_,
        NewCallback(this, &BufferedDataSourceTest::ReadCallback));

    // This blocks the current thread until the watch task is executed and
    // triggers a read callback to quit this message loop.
    message_loop_->Run();

    // Make sure data is correct.
    EXPECT_EQ(0, memcmp(buffer_, data_ + static_cast<int>(position), size));

    loader_ = new_loader;
  }

  MOCK_METHOD1(ReadCallback, void(size_t size));

  scoped_refptr<NiceMock<MockBufferedResourceLoader> > loader_;
  scoped_refptr<MockBufferedDataSource> data_source_;
  scoped_ptr<NiceMock<MockWebFrame> > frame_;

  StrictMock<media::MockFilterHost> host_;
  GURL gurl_;
  MessageLoop* message_loop_;

  int error_;
  uint8 buffer_[1024];
  uint8 data_[1024];

 private:
  DISALLOW_COPY_AND_ASSIGN(BufferedDataSourceTest);
};

TEST_F(BufferedDataSourceTest, InitializationSuccess) {
  InitializeDataSource(kHttpUrl, net::OK, true, 1024, LOADING);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, InitiailizationFailed) {
  InitializeDataSource(kHttpUrl, net::ERR_FILE_NOT_FOUND, false, 0, NONE);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, MissingContentLength) {
  InitializeDataSource(kHttpUrl, net::OK, true, -1, LOADING);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, RangeRequestNotSupported) {
  InitializeDataSource(kHttpUrl, net::OK, false, 1024, LOADING);
  StopDataSource();
}

// Test the case where we get a 206 response, but no Content-Range header.
TEST_F(BufferedDataSourceTest, MissingContentRange) {
  InitializeDataSource(kHttpUrl, net::ERR_INVALID_RESPONSE, true, 1024,
                       LOADING);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest,
       MissingContentLengthAndRangeRequestNotSupported) {
  InitializeDataSource(kHttpUrl, net::OK, false, -1, LOADING);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, ReadCacheHit) {
  InitializeDataSource(kHttpUrl, net::OK, true, 25, LOADING);

  // Performs read with cache hit.
  ReadDataSourceHit(10, 10, 10);

  // Performs read with cache hit but partially filled.
  ReadDataSourceHit(20, 10, 5);

  StopDataSource();
}

TEST_F(BufferedDataSourceTest, ReadCacheMiss) {
  InitializeDataSource(kHttpUrl, net::OK, true, 1024, LOADING);
  ReadDataSourceMiss(1000, 10, net::OK);
  ReadDataSourceMiss(20, 10, net::OK);
  StopDataSource();
}

// Test the case where the initial response from the server indicates that
// Range requests are supported, but a later request prove otherwise.
TEST_F(BufferedDataSourceTest, ServerLiesAboutRangeSupport) {
  InitializeDataSource(kHttpUrl, net::OK, true, 1024, LOADING);
  ReadDataSourceHit(10, 10, 10);
  ReadDataSourceMiss(1000, 10, net::ERR_INVALID_RESPONSE);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, ReadHang) {
  InitializeDataSource(kHttpUrl, net::OK, true, 25, LOADING);
  ReadDataSourceHang(10, 10);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, ReadFailed) {
  InitializeDataSource(kHttpUrl, net::OK, true, 1024, LOADING);
  ReadDataSourceHit(10, 10, 10);
  ReadDataSourceFailed(10, 10, net::ERR_CONNECTION_RESET);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, ReadTimesOut) {
  InitializeDataSource(kHttpUrl, net::OK, true, 1024, LOADING);
  ReadDataSourceTimesOut(20, 10);
  StopDataSource();
}

TEST_F(BufferedDataSourceTest, FileHasLoadedState) {
  InitializeDataSource(kFileUrl, net::OK, true, 1024, LOADED);
  ReadDataSourceTimesOut(20, 10);
  StopDataSource();
}

// This test makes sure that Stop() does not require a task to run on
// |message_loop_| before it calls its callback. This prevents accidental
// introduction of a pipeline teardown deadlock. The pipeline owner blocks
// the render message loop while waiting for Stop() to complete. Since this
// object runs on the render message loop, Stop() will not complete if it
// requires a task to run on the the message loop that is being blocked.
TEST_F(BufferedDataSourceTest, StopDoesNotUseMessageLoopForCallback) {
  InitializeDataSource(kFileUrl, net::OK, true, 1024, LOADED);

  // Create a callback that lets us verify that it was called before
  // Stop() returns. This is to make sure that the callback does not
  // require |message_loop_| to execute tasks before being called.
  media::MockCallback* stop_callback = media::NewExpectedCallback();
  bool stop_done_called = false;
  ON_CALL(*stop_callback, RunWithParams(_))
      .WillByDefault(Assign(&stop_done_called, true));

  // Stop() the data source like normal.
  data_source_->Stop(stop_callback);

  // Verify that the callback was called inside the Stop() call.
  EXPECT_TRUE(stop_done_called);

  message_loop_->RunAllPending();
}

TEST_F(BufferedDataSourceTest, AbortDuringPendingRead) {
  InitializeDataSource(kFileUrl, net::OK, true, 1024, LOADED);

  // Setup a way to verify that Read() is not called on the loader.
  // We are doing this to make sure that the ReadTask() is still on
  // the message loop queue when Abort() is called.
  bool read_called = false;
  ON_CALL(*loader_, Read(_, _, _ , _))
      .WillByDefault(DoAll(Assign(&read_called, true),
                           DeleteArg<3>()));

  // Initiate a Read() on the data source, but don't allow the
  // message loop to run.
  data_source_->Read(
      0, 10, buffer_,
      NewCallback(static_cast<BufferedDataSourceTest*>(this),
                  &BufferedDataSourceTest::ReadCallback));

  // Call Abort() with the read pending.
  EXPECT_CALL(*this, ReadCallback(-1));
  EXPECT_CALL(*loader_, Stop());
  data_source_->Abort();

  // Verify that Read()'s after the Abort() issue callback with an error.
  EXPECT_CALL(*this, ReadCallback(-1));
  data_source_->Read(
      0, 10, buffer_,
      NewCallback(static_cast<BufferedDataSourceTest*>(this),
                  &BufferedDataSourceTest::ReadCallback));

  // Stop() the data source like normal.
  data_source_->Stop(media::NewExpectedCallback());

  // Allow cleanup task to run.
  message_loop_->RunAllPending();

  // Verify that Read() was not called on the loader.
  EXPECT_FALSE(read_called);
}

}  // namespace webkit_glue