// Copyright (c) 2012 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 <stack>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/message_loop.h"
#include "base/threading/thread.h"
#include "base/synchronization/waitable_event.h"
#include "net/base/net_errors.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "webkit/appcache/appcache.h"
#include "webkit/appcache/appcache_database.h"
#include "webkit/appcache/appcache_entry.h"
#include "webkit/appcache/appcache_group.h"
#include "webkit/appcache/appcache_service.h"
#include "webkit/appcache/appcache_storage_impl.h"
#include "webkit/quota/quota_manager.h"

namespace appcache {

namespace {

const base::Time kZeroTime;
const GURL kManifestUrl("http://blah/manifest");
const GURL kManifestUrl2("http://blah/manifest2");
const GURL kManifestUrl3("http://blah/manifest3");
const GURL kEntryUrl("http://blah/entry");
const GURL kEntryUrl2("http://blah/entry2");
const GURL kFallbackNamespace("http://blah/fallback_namespace/");
const GURL kFallbackNamespace2("http://blah/fallback_namespace/longer");
const GURL kFallbackTestUrl("http://blah/fallback_namespace/longer/test");
const GURL kOnlineNamespace("http://blah/online_namespace");
const GURL kOnlineNamespaceWithinFallback(
    "http://blah/fallback_namespace/online/");
const GURL kInterceptNamespace("http://blah/intercept_namespace/");
const GURL kInterceptNamespace2("http://blah/intercept_namespace/longer/");
const GURL kInterceptTestUrl("http://blah/intercept_namespace/longer/test");
const GURL kOrigin(kManifestUrl.GetOrigin());

const int kManifestEntryIdOffset = 100;
const int kFallbackEntryIdOffset = 1000;

const GURL kDefaultEntryUrl("http://blah/makecacheandgroup_default_entry");
const int kDefaultEntrySize = 10;
const int kDefaultEntryIdOffset = 12345;

const int kMockQuota = 5000;

scoped_ptr<base::Thread> io_thread;
scoped_ptr<base::Thread> db_thread;

}  // namespace

class AppCacheStorageImplTest : public testing::Test {
 public:
  class MockStorageDelegate : public AppCacheStorage::Delegate {
   public:
    explicit MockStorageDelegate(AppCacheStorageImplTest* test)
        : loaded_cache_id_(0), stored_group_success_(false),
          would_exceed_quota_(false), obsoleted_success_(false),
          found_cache_id_(kNoCacheId), test_(test) {
    }

    void OnCacheLoaded(AppCache* cache, int64 cache_id) {
      loaded_cache_ = cache;
      loaded_cache_id_ = cache_id;
      test_->ScheduleNextTask();
    }

    void OnGroupLoaded(AppCacheGroup* group, const GURL& manifest_url) {
      loaded_group_ = group;
      loaded_manifest_url_ = manifest_url;
      loaded_groups_newest_cache_ = group ? group->newest_complete_cache()
                                          : NULL;
      test_->ScheduleNextTask();
    }

    void OnGroupAndNewestCacheStored(
        AppCacheGroup* group, AppCache* newest_cache, bool success,
        bool would_exceed_quota) {
      stored_group_ = group;
      stored_group_success_ = success;
      would_exceed_quota_ = would_exceed_quota;
      test_->ScheduleNextTask();
    }

    void OnGroupMadeObsolete(AppCacheGroup* group, bool success) {
      obsoleted_group_ = group;
      obsoleted_success_ = success;
      test_->ScheduleNextTask();
    }

    void OnMainResponseFound(const GURL& url, const AppCacheEntry& entry,
                             const GURL& namespace_entry_url,
                             const AppCacheEntry& fallback_entry,
                             int64 cache_id, int64 group_id,
                             const GURL& manifest_url) {
      found_url_ = url;
      found_entry_ = entry;
      found_namespace_entry_url_ = namespace_entry_url;
      found_fallback_entry_ = fallback_entry;
      found_cache_id_ = cache_id;
      found_group_id_ = group_id;
      found_manifest_url_ = manifest_url;
      test_->ScheduleNextTask();
    }

    scoped_refptr<AppCache> loaded_cache_;
    int64 loaded_cache_id_;
    scoped_refptr<AppCacheGroup> loaded_group_;
    GURL loaded_manifest_url_;
    scoped_refptr<AppCache> loaded_groups_newest_cache_;
    scoped_refptr<AppCacheGroup> stored_group_;
    bool stored_group_success_;
    bool would_exceed_quota_;
    scoped_refptr<AppCacheGroup> obsoleted_group_;
    bool obsoleted_success_;
    GURL found_url_;
    AppCacheEntry found_entry_;
    GURL found_namespace_entry_url_;
    AppCacheEntry found_fallback_entry_;
    int64 found_cache_id_;
    int64 found_group_id_;
    GURL found_manifest_url_;
    AppCacheStorageImplTest* test_;
  };

  class MockQuotaManager : public quota::QuotaManager {
   public:
    MockQuotaManager()
      : QuotaManager(true /* is_incognito */, FilePath(),
                     io_thread->message_loop_proxy(),
                     db_thread->message_loop_proxy(),
                     NULL),
        async_(false) {}

    virtual void GetUsageAndQuota(
        const GURL& origin, quota::StorageType type,
        const GetUsageAndQuotaCallback& callback) OVERRIDE {
      EXPECT_EQ(kOrigin, origin);
      EXPECT_EQ(quota::kStorageTypeTemporary, type);
      if (async_) {
        MessageLoop::current()->PostTask(
            FROM_HERE, base::Bind(&MockQuotaManager::CallCallback,
                                  base::Unretained(this), callback));
        return;
      }
      CallCallback(callback);
    }

    void CallCallback(const GetUsageAndQuotaCallback& callback) {
      callback.Run(quota::kQuotaStatusOk, 0, kMockQuota);
    }

    bool async_;

   protected:
    virtual ~MockQuotaManager() {}
  };

  class MockQuotaManagerProxy : public quota::QuotaManagerProxy {
   public:
    MockQuotaManagerProxy()
        : QuotaManagerProxy(NULL, NULL),
          notify_storage_accessed_count_(0),
          notify_storage_modified_count_(0),
          last_delta_(0),
          mock_manager_(new MockQuotaManager) {
      manager_ = mock_manager_;
    }

    virtual void NotifyStorageAccessed(quota::QuotaClient::ID client_id,
                                       const GURL& origin,
                                       quota::StorageType type) OVERRIDE {
      EXPECT_EQ(quota::QuotaClient::kAppcache, client_id);
      EXPECT_EQ(quota::kStorageTypeTemporary, type);
      ++notify_storage_accessed_count_;
      last_origin_ = origin;
    }

    virtual void NotifyStorageModified(quota::QuotaClient::ID client_id,
                                       const GURL& origin,
                                       quota::StorageType type,
                                       int64 delta) OVERRIDE {
      EXPECT_EQ(quota::QuotaClient::kAppcache, client_id);
      EXPECT_EQ(quota::kStorageTypeTemporary, type);
      ++notify_storage_modified_count_;
      last_origin_ = origin;
      last_delta_ = delta;
    }

    // Not needed for our tests.
    virtual void RegisterClient(quota::QuotaClient* client) OVERRIDE {}
    virtual void NotifyOriginInUse(const GURL& origin) OVERRIDE {}
    virtual void NotifyOriginNoLongerInUse(const GURL& origin) OVERRIDE {}

    int notify_storage_accessed_count_;
    int notify_storage_modified_count_;
    GURL last_origin_;
    int last_delta_;
    scoped_refptr<MockQuotaManager> mock_manager_;

   protected:
    virtual ~MockQuotaManagerProxy() {}
  };

  template <class Method>
  void RunMethod(Method method) {
    (this->*method)();
  }

  // Helper callback to run a test on our io_thread. The io_thread is spun up
  // once and reused for all tests.
  template <class Method>
  void MethodWrapper(Method method) {
    SetUpTest();

    // Ensure InitTask execution prior to conducting a test.
    FlushDbThreadTasks();

    // We also have to wait for InitTask completion call to be performed
    // on the IO thread prior to running the test. Its guaranteed to be
    // queued by this time.
    MessageLoop::current()->PostTask(
        FROM_HERE, base::Bind(&AppCacheStorageImplTest::RunMethod<Method>,
                              base::Unretained(this), method));
  }

  static void SetUpTestCase() {
    io_thread.reset(new base::Thread("AppCacheTest.IOThread"));
    base::Thread::Options options(MessageLoop::TYPE_IO, 0);
    ASSERT_TRUE(io_thread->StartWithOptions(options));

    db_thread.reset(new base::Thread("AppCacheTest::DBThread"));
    ASSERT_TRUE(db_thread->Start());
  }

  static void TearDownTestCase() {
    io_thread.reset(NULL);
    db_thread.reset(NULL);
  }

  // Test harness --------------------------------------------------

  AppCacheStorageImplTest() {
  }

  template <class Method>
  void RunTestOnIOThread(Method method) {
    test_finished_event_ .reset(new base::WaitableEvent(false, false));
    io_thread->message_loop()->PostTask(
        FROM_HERE, base::Bind(&AppCacheStorageImplTest::MethodWrapper<Method>,
                              base::Unretained(this), method));
    test_finished_event_->Wait();
  }

  void SetUpTest() {
    DCHECK(MessageLoop::current() == io_thread->message_loop());
    service_.reset(new AppCacheService(NULL));
    service_->Initialize(FilePath(), db_thread->message_loop_proxy(), NULL);
    mock_quota_manager_proxy_ = new MockQuotaManagerProxy();
    service_->quota_manager_proxy_ = mock_quota_manager_proxy_;
    delegate_.reset(new MockStorageDelegate(this));
  }

  void TearDownTest() {
    DCHECK(MessageLoop::current() == io_thread->message_loop());
    storage()->CancelDelegateCallbacks(delegate());
    group_ = NULL;
    cache_ = NULL;
    cache2_ = NULL;
    mock_quota_manager_proxy_ = NULL;
    delegate_.reset();
    service_.reset();
    FlushDbThreadTasks();
  }

  void TestFinished() {
    // We unwind the stack prior to finishing up to let stack
    // based objects get deleted.
    DCHECK(MessageLoop::current() == io_thread->message_loop());
    MessageLoop::current()->PostTask(
        FROM_HERE, base::Bind(&AppCacheStorageImplTest::TestFinishedUnwound,
                              base::Unretained(this)));
  }

  void TestFinishedUnwound() {
    TearDownTest();
    test_finished_event_->Signal();
  }

  void PushNextTask(const base::Closure& task) {
    task_stack_.push(task);
  }

  void ScheduleNextTask() {
    DCHECK(MessageLoop::current() == io_thread->message_loop());
    if (task_stack_.empty()) {
      return;
    }
    MessageLoop::current()->PostTask(FROM_HERE, task_stack_.top());
    task_stack_.pop();
  }

  static void SignalEvent(base::WaitableEvent* event) {
    event->Signal();
  }

  void FlushDbThreadTasks() {
    // We pump a task thru the db thread to ensure any tasks previously
    // scheduled on that thread have been performed prior to return.
    base::WaitableEvent event(false, false);
    db_thread->message_loop()->PostTask(
        FROM_HERE, base::Bind(&AppCacheStorageImplTest::SignalEvent, &event));
    event.Wait();
  }

  // LoadCache_Miss ----------------------------------------------------

  void LoadCache_Miss() {
    // Attempt to load a cache that doesn't exist. Should
    // complete asynchronously.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_LoadCache_Miss,
                            base::Unretained(this)));

    storage()->LoadCache(111, delegate());
    EXPECT_NE(111, delegate()->loaded_cache_id_);
  }

  void Verify_LoadCache_Miss() {
    EXPECT_EQ(111, delegate()->loaded_cache_id_);
    EXPECT_FALSE(delegate()->loaded_cache_);
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_accessed_count_);
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_modified_count_);
    TestFinished();
  }

  // LoadCache_NearHit -------------------------------------------------

  void LoadCache_NearHit() {
    // Attempt to load a cache that is currently in use
    // and does not require loading from storage. This
    // load should complete syncly.

    // Setup some preconditions. Make an 'unstored' cache for
    // us to load. The ctor should put it in the working set.
    int64 cache_id = storage()->NewCacheId();
    scoped_refptr<AppCache> cache(new AppCache(service(), cache_id));

    // Conduct the test.
    storage()->LoadCache(cache_id, delegate());
    EXPECT_EQ(cache_id, delegate()->loaded_cache_id_);
    EXPECT_EQ(cache.get(), delegate()->loaded_cache_.get());
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_accessed_count_);
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_modified_count_);
    TestFinished();
  }

  // CreateGroup  --------------------------------------------

  void CreateGroupInEmptyOrigin() {
    // Attempt to load a group that doesn't exist, one should
    // be created for us, but not stored.

    // Since the origin has no groups, the storage class will respond
    // syncly.
    storage()->LoadOrCreateGroup(kManifestUrl, delegate());
    Verify_CreateGroup();
  }

  void CreateGroupInPopulatedOrigin() {
    // Attempt to load a group that doesn't exist, one should
    // be created for us, but not stored.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_CreateGroup,
                            base::Unretained(this)));

    // Since the origin has groups, storage class will have to
    // consult the database and completion will be async.
    storage()->usage_map_[kOrigin] = kDefaultEntrySize;

    storage()->LoadOrCreateGroup(kManifestUrl, delegate());
    EXPECT_FALSE(delegate()->loaded_group_.get());
  }

  void Verify_CreateGroup() {
    EXPECT_EQ(kManifestUrl, delegate()->loaded_manifest_url_);
    EXPECT_TRUE(delegate()->loaded_group_.get());
    EXPECT_TRUE(delegate()->loaded_group_->HasOneRef());
    EXPECT_FALSE(delegate()->loaded_group_->newest_complete_cache());

    // Should not have been stored in the database.
    AppCacheDatabase::GroupRecord record;
    EXPECT_FALSE(database()->FindGroup(
        delegate()->loaded_group_->group_id(), &record));

    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_accessed_count_);
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_modified_count_);

    TestFinished();
  }

  // LoadGroupAndCache_FarHit  --------------------------------------

  void LoadGroupAndCache_FarHit() {
    // Attempt to load a cache that is not currently in use
    // and does require loading from disk. This
    // load should complete asynchronously.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_LoadCache_Far_Hit,
                            base::Unretained(this)));

    // Setup some preconditions. Create a group and newest cache that
    // appear to be "stored" and "not currently in use".
    MakeCacheAndGroup(kManifestUrl, 1, 1, true);
    group_ = NULL;
    cache_ = NULL;

    // Conduct the cache load test, completes async
    storage()->LoadCache(1, delegate());
  }

  void Verify_LoadCache_Far_Hit() {
    EXPECT_TRUE(delegate()->loaded_cache_);
    EXPECT_TRUE(delegate()->loaded_cache_->HasOneRef());
    EXPECT_EQ(1, delegate()->loaded_cache_id_);

    // The group should also have been loaded.
    EXPECT_TRUE(delegate()->loaded_cache_->owning_group());
    EXPECT_TRUE(delegate()->loaded_cache_->owning_group()->HasOneRef());
    EXPECT_EQ(1, delegate()->loaded_cache_->owning_group()->group_id());

    EXPECT_EQ(1, mock_quota_manager_proxy_->notify_storage_accessed_count_);
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_modified_count_);

    // Drop things from the working set.
    delegate()->loaded_cache_ = NULL;
    EXPECT_FALSE(delegate()->loaded_group_);

    // Conduct the group load test, also complete asynchronously.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_LoadGroup_Far_Hit,
                            base::Unretained(this)));

    storage()->LoadOrCreateGroup(kManifestUrl, delegate());
  }

  void Verify_LoadGroup_Far_Hit() {
    EXPECT_TRUE(delegate()->loaded_group_);
    EXPECT_EQ(kManifestUrl, delegate()->loaded_manifest_url_);
    EXPECT_TRUE(delegate()->loaded_group_->newest_complete_cache());
    delegate()->loaded_groups_newest_cache_ = NULL;
    EXPECT_TRUE(delegate()->loaded_group_->HasOneRef());
    EXPECT_EQ(2, mock_quota_manager_proxy_->notify_storage_accessed_count_);
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_modified_count_);
    TestFinished();
  }

  // StoreNewGroup  --------------------------------------

  void StoreNewGroup() {
    // Store a group and its newest cache. Should complete asynchronously.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_StoreNewGroup,
                            base::Unretained(this)));

    // Setup some preconditions. Create a group and newest cache that
    // appear to be "unstored".
    group_ = new AppCacheGroup(
        service(), kManifestUrl, storage()->NewGroupId());
    cache_ = new AppCache(service(), storage()->NewCacheId());
    cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT, 1,
                                              kDefaultEntrySize));
    // Hold a ref to the cache simulate the UpdateJob holding that ref,
    // and hold a ref to the group to simulate the CacheHost holding that ref.

    // Have the quota manager retrun asynchronously for this test.
    mock_quota_manager_proxy_->mock_manager_->async_ = true;

    // Conduct the store test.
    storage()->StoreGroupAndNewestCache(group_, cache_, delegate());
    EXPECT_FALSE(delegate()->stored_group_success_);
  }

  void Verify_StoreNewGroup() {
    EXPECT_TRUE(delegate()->stored_group_success_);
    EXPECT_EQ(group_.get(), delegate()->stored_group_.get());
    EXPECT_EQ(cache_.get(), group_->newest_complete_cache());
    EXPECT_TRUE(cache_->is_complete());

    // Should have been stored in the database.
    AppCacheDatabase::GroupRecord group_record;
    AppCacheDatabase::CacheRecord cache_record;
    EXPECT_TRUE(database()->FindGroup(group_->group_id(), &group_record));
    EXPECT_TRUE(database()->FindCache(cache_->cache_id(), &cache_record));

    // Verify quota bookkeeping
    EXPECT_EQ(kDefaultEntrySize, storage()->usage_map_[kOrigin]);
    EXPECT_EQ(1, mock_quota_manager_proxy_->notify_storage_modified_count_);
    EXPECT_EQ(kOrigin, mock_quota_manager_proxy_->last_origin_);
    EXPECT_EQ(kDefaultEntrySize, mock_quota_manager_proxy_->last_delta_);

    TestFinished();
  }

  // StoreExistingGroup  --------------------------------------

  void StoreExistingGroup() {
    // Store a group and its newest cache. Should complete asynchronously.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_StoreExistingGroup,
                            base::Unretained(this)));

    // Setup some preconditions. Create a group and old complete cache
    // that appear to be "stored"
    MakeCacheAndGroup(kManifestUrl, 1, 1, true);
    EXPECT_EQ(kDefaultEntrySize, storage()->usage_map_[kOrigin]);

    // And a newest unstored complete cache.
    cache2_ = new AppCache(service(), 2);
    cache2_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::MASTER, 1,
                                               kDefaultEntrySize + 100));

    // Conduct the test.
    storage()->StoreGroupAndNewestCache(group_, cache2_, delegate());
    EXPECT_FALSE(delegate()->stored_group_success_);
  }

  void Verify_StoreExistingGroup() {
    EXPECT_TRUE(delegate()->stored_group_success_);
    EXPECT_EQ(group_.get(), delegate()->stored_group_.get());
    EXPECT_EQ(cache2_.get(), group_->newest_complete_cache());
    EXPECT_TRUE(cache2_->is_complete());

    // The new cache should have been stored in the database.
    AppCacheDatabase::GroupRecord group_record;
    AppCacheDatabase::CacheRecord cache_record;
    EXPECT_TRUE(database()->FindGroup(1, &group_record));
    EXPECT_TRUE(database()->FindCache(2, &cache_record));

    // The old cache should have been deleted
    EXPECT_FALSE(database()->FindCache(1, &cache_record));

    // Verify quota bookkeeping
    EXPECT_EQ(kDefaultEntrySize + 100, storage()->usage_map_[kOrigin]);
    EXPECT_EQ(1, mock_quota_manager_proxy_->notify_storage_modified_count_);
    EXPECT_EQ(kOrigin, mock_quota_manager_proxy_->last_origin_);
    EXPECT_EQ(100, mock_quota_manager_proxy_->last_delta_);

    TestFinished();
  }

  // StoreExistingGroupExistingCache  -------------------------------

  void StoreExistingGroupExistingCache() {
    // Store a group with updates to its existing newest complete cache.
    // Setup some preconditions. Create a group and a complete cache that
    // appear to be "stored".

    // Setup some preconditions. Create a group and old complete cache
    // that appear to be "stored"
    MakeCacheAndGroup(kManifestUrl, 1, 1, true);
    EXPECT_EQ(kDefaultEntrySize, storage()->usage_map_[kOrigin]);

    // Change the cache.
    base::Time now = base::Time::Now();
    cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::MASTER, 1, 100));
    cache_->set_update_time(now);

    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_StoreExistingGroupExistingCache,
        base::Unretained(this), now));

    // Conduct the test.
    EXPECT_EQ(cache_, group_->newest_complete_cache());
    storage()->StoreGroupAndNewestCache(group_, cache_, delegate());
    EXPECT_FALSE(delegate()->stored_group_success_);
  }

  void Verify_StoreExistingGroupExistingCache(
      base::Time expected_update_time) {
    EXPECT_TRUE(delegate()->stored_group_success_);
    EXPECT_EQ(cache_, group_->newest_complete_cache());

    AppCacheDatabase::CacheRecord cache_record;
    EXPECT_TRUE(database()->FindCache(1, &cache_record));
    EXPECT_EQ(1, cache_record.cache_id);
    EXPECT_EQ(1, cache_record.group_id);
    EXPECT_FALSE(cache_record.online_wildcard);
    EXPECT_TRUE(expected_update_time == cache_record.update_time);
    EXPECT_EQ(100 + kDefaultEntrySize, cache_record.cache_size);

    std::vector<AppCacheDatabase::EntryRecord> entry_records;
    EXPECT_TRUE(database()->FindEntriesForCache(1, &entry_records));
    EXPECT_EQ(2U, entry_records.size());
    if (entry_records[0].url == kDefaultEntryUrl)
      entry_records.erase(entry_records.begin());
    EXPECT_EQ(1 , entry_records[0].cache_id);
    EXPECT_EQ(kEntryUrl, entry_records[0].url);
    EXPECT_EQ(AppCacheEntry::MASTER, entry_records[0].flags);
    EXPECT_EQ(1, entry_records[0].response_id);
    EXPECT_EQ(100, entry_records[0].response_size);

    // Verify quota bookkeeping
    EXPECT_EQ(100 + kDefaultEntrySize, storage()->usage_map_[kOrigin]);
    EXPECT_EQ(1, mock_quota_manager_proxy_->notify_storage_modified_count_);
    EXPECT_EQ(kOrigin, mock_quota_manager_proxy_->last_origin_);
    EXPECT_EQ(100, mock_quota_manager_proxy_->last_delta_);

    TestFinished();
  }

  // FailStoreGroup  --------------------------------------

  void FailStoreGroup() {
    // Store a group and its newest cache. Should complete asynchronously.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_FailStoreGroup,
                            base::Unretained(this)));

    // Setup some preconditions. Create a group and newest cache that
    // appear to be "unstored" and big enough to exceed the 5M limit.
    const int64 kTooBig = 10 * 1024 * 1024;  // 10M
    group_ = new AppCacheGroup(
        service(), kManifestUrl, storage()->NewGroupId());
    cache_ = new AppCache(service(), storage()->NewCacheId());
    cache_->AddEntry(kManifestUrl,
                     AppCacheEntry(AppCacheEntry::MANIFEST, 1, kTooBig));
    // Hold a ref to the cache simulate the UpdateJob holding that ref,
    // and hold a ref to the group to simulate the CacheHost holding that ref.

    // Conduct the store test.
    storage()->StoreGroupAndNewestCache(group_, cache_, delegate());
    EXPECT_FALSE(delegate()->stored_group_success_);  // Expected to be async.
  }

  void Verify_FailStoreGroup() {
    EXPECT_FALSE(delegate()->stored_group_success_);
    EXPECT_TRUE(delegate()->would_exceed_quota_);

    // Should not have been stored in the database.
    AppCacheDatabase::GroupRecord group_record;
    AppCacheDatabase::CacheRecord cache_record;
    EXPECT_FALSE(database()->FindGroup(group_->group_id(), &group_record));
    EXPECT_FALSE(database()->FindCache(cache_->cache_id(), &cache_record));

    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_accessed_count_);
    EXPECT_EQ(0, mock_quota_manager_proxy_->notify_storage_modified_count_);

    TestFinished();
  }

  // MakeGroupObsolete  -------------------------------

  void MakeGroupObsolete() {
    // Make a group obsolete, should complete asynchronously.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_MakeGroupObsolete,
                            base::Unretained(this)));

    // Setup some preconditions. Create a group and newest cache that
    // appears to be "stored" and "currently in use".
    MakeCacheAndGroup(kManifestUrl, 1, 1, true);
    EXPECT_EQ(kDefaultEntrySize, storage()->usage_map_[kOrigin]);

    // Also insert some related records.
    AppCacheDatabase::EntryRecord entry_record;
    entry_record.cache_id = 1;
    entry_record.flags = AppCacheEntry::FALLBACK;
    entry_record.response_id = 1;
    entry_record.url = kEntryUrl;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));

    AppCacheDatabase::NamespaceRecord fallback_namespace_record;
    fallback_namespace_record.cache_id = 1;
    fallback_namespace_record.target_url = kEntryUrl;
    fallback_namespace_record.namespace_url = kFallbackNamespace;
    fallback_namespace_record.origin = kManifestUrl.GetOrigin();
    EXPECT_TRUE(database()->InsertNamespace(&fallback_namespace_record));

    AppCacheDatabase::OnlineWhiteListRecord online_whitelist_record;
    online_whitelist_record.cache_id = 1;
    online_whitelist_record.namespace_url = kOnlineNamespace;
    EXPECT_TRUE(database()->InsertOnlineWhiteList(&online_whitelist_record));

    // Conduct the test.
    storage()->MakeGroupObsolete(group_, delegate());
    EXPECT_FALSE(group_->is_obsolete());
  }

  void Verify_MakeGroupObsolete() {
    EXPECT_TRUE(delegate()->obsoleted_success_);
    EXPECT_EQ(group_.get(), delegate()->obsoleted_group_.get());
    EXPECT_TRUE(group_->is_obsolete());
    EXPECT_TRUE(storage()->usage_map_.empty());

    // The cache and group have been deleted from the database.
    AppCacheDatabase::GroupRecord group_record;
    AppCacheDatabase::CacheRecord cache_record;
    EXPECT_FALSE(database()->FindGroup(1, &group_record));
    EXPECT_FALSE(database()->FindCache(1, &cache_record));

    // The related records should have been deleted too.
    std::vector<AppCacheDatabase::EntryRecord> entry_records;
    database()->FindEntriesForCache(1, &entry_records);
    EXPECT_TRUE(entry_records.empty());
    std::vector<AppCacheDatabase::NamespaceRecord> intercept_records;
    std::vector<AppCacheDatabase::NamespaceRecord> fallback_records;
    database()->FindNamespacesForCache(
        1, &intercept_records, &fallback_records);
    EXPECT_TRUE(fallback_records.empty());
    std::vector<AppCacheDatabase::OnlineWhiteListRecord> whitelist_records;
    database()->FindOnlineWhiteListForCache(1, &whitelist_records);
    EXPECT_TRUE(whitelist_records.empty());

    // Verify quota bookkeeping
    EXPECT_TRUE(storage()->usage_map_.empty());
    EXPECT_EQ(1, mock_quota_manager_proxy_->notify_storage_modified_count_);
    EXPECT_EQ(kOrigin, mock_quota_manager_proxy_->last_origin_);
    EXPECT_EQ(-kDefaultEntrySize, mock_quota_manager_proxy_->last_delta_);

    TestFinished();
  }

  // MarkEntryAsForeign  -------------------------------

  void MarkEntryAsForeign() {
    // Setup some preconditions. Create a cache with an entry
    // in storage and in the working set.
    MakeCacheAndGroup(kManifestUrl, 1, 1, true);
    cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT));
    AppCacheDatabase::EntryRecord entry_record;
    entry_record.cache_id = 1;
    entry_record.url = kEntryUrl;
    entry_record.flags = AppCacheEntry::EXPLICIT;
    entry_record.response_id = 0;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));
    EXPECT_FALSE(cache_->GetEntry(kEntryUrl)->IsForeign());

    // Conduct the test.
    storage()->MarkEntryAsForeign(kEntryUrl, 1);

    // The entry in the working set should have been updated syncly.
    EXPECT_TRUE(cache_->GetEntry(kEntryUrl)->IsForeign());
    EXPECT_TRUE(cache_->GetEntry(kEntryUrl)->IsExplicit());

    // And the entry in storage should also be updated, but that
    // happens asynchronously on the db thread.
    FlushDbThreadTasks();
    AppCacheDatabase::EntryRecord entry_record2;
    EXPECT_TRUE(database()->FindEntry(1, kEntryUrl, &entry_record2));
    EXPECT_EQ(AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN,
              entry_record2.flags);
    TestFinished();
  }

  // MarkEntryAsForeignWithLoadInProgress  -------------------------------

  void MarkEntryAsForeignWithLoadInProgress() {
    PushNextTask(base::Bind(
       &AppCacheStorageImplTest::Verify_MarkEntryAsForeignWithLoadInProgress,
       base::Unretained(this)));

    // Setup some preconditions. Create a cache with an entry
    // in storage, but not in the working set.
    MakeCacheAndGroup(kManifestUrl, 1, 1, true);
    cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT));
    AppCacheDatabase::EntryRecord entry_record;
    entry_record.cache_id = 1;
    entry_record.url = kEntryUrl;
    entry_record.flags = AppCacheEntry::EXPLICIT;
    entry_record.response_id = 0;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));
    EXPECT_FALSE(cache_->GetEntry(kEntryUrl)->IsForeign());
    EXPECT_TRUE(cache_->HasOneRef());
    cache_ = NULL;
    group_ = NULL;

    // Conduct the test, start a cache load, and prior to completion
    // of that load, mark the entry as foreign.
    storage()->LoadCache(1, delegate());
    storage()->MarkEntryAsForeign(kEntryUrl, 1);
  }

  void Verify_MarkEntryAsForeignWithLoadInProgress() {
    EXPECT_EQ(1, delegate()->loaded_cache_id_);
    EXPECT_TRUE(delegate()->loaded_cache_.get());

    // The entry in the working set should have been updated upon load.
    EXPECT_TRUE(delegate()->loaded_cache_->GetEntry(kEntryUrl)->IsForeign());
    EXPECT_TRUE(delegate()->loaded_cache_->GetEntry(kEntryUrl)->IsExplicit());

    // And the entry in storage should also be updated.
    FlushDbThreadTasks();
    AppCacheDatabase::EntryRecord entry_record;
    EXPECT_TRUE(database()->FindEntry(1, kEntryUrl, &entry_record));
    EXPECT_EQ(AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN,
              entry_record.flags);
    TestFinished();
  }

  // FindNoMainResponse  -------------------------------

  void FindNoMainResponse() {
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_FindNoMainResponse,
                            base::Unretained(this)));

    // Conduct the test.
    storage()->FindResponseForMainRequest(kEntryUrl, GURL(), delegate());
    EXPECT_NE(kEntryUrl, delegate()->found_url_);
  }

  void Verify_FindNoMainResponse() {
    EXPECT_EQ(kEntryUrl, delegate()->found_url_);
    EXPECT_TRUE(delegate()->found_manifest_url_.is_empty());
    EXPECT_EQ(kNoCacheId, delegate()->found_cache_id_);
    EXPECT_EQ(kNoResponseId, delegate()->found_entry_.response_id());
    EXPECT_EQ(kNoResponseId, delegate()->found_fallback_entry_.response_id());
    EXPECT_TRUE(delegate()->found_namespace_entry_url_.is_empty());
    EXPECT_EQ(0, delegate()->found_entry_.types());
    EXPECT_EQ(0, delegate()->found_fallback_entry_.types());
    TestFinished();
  }

  // BasicFindMainResponse  -------------------------------

  void BasicFindMainResponseInDatabase() {
    BasicFindMainResponse(true);
  }

  void BasicFindMainResponseInWorkingSet() {
    BasicFindMainResponse(false);
  }

  void BasicFindMainResponse(bool drop_from_working_set) {
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_BasicFindMainResponse,
        base::Unretained(this)));

    // Setup some preconditions. Create a complete cache with an entry
    // in storage.
    MakeCacheAndGroup(kManifestUrl, 2, 1, true);
    cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT, 1));
    AppCacheDatabase::EntryRecord entry_record;
    entry_record.cache_id = 1;
    entry_record.url = kEntryUrl;
    entry_record.flags = AppCacheEntry::EXPLICIT;
    entry_record.response_id = 1;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));

    // Optionally drop the cache/group pair from the working set.
    if (drop_from_working_set) {
      EXPECT_TRUE(cache_->HasOneRef());
      cache_ = NULL;
      EXPECT_TRUE(group_->HasOneRef());
      group_ = NULL;
    }

    // Conduct the test.
    storage()->FindResponseForMainRequest(kEntryUrl, GURL(), delegate());
    EXPECT_NE(kEntryUrl,  delegate()->found_url_);
  }

  void Verify_BasicFindMainResponse() {
    EXPECT_EQ(kEntryUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl, delegate()->found_manifest_url_);
    EXPECT_EQ(1, delegate()->found_cache_id_);
    EXPECT_EQ(2, delegate()->found_group_id_);
    EXPECT_EQ(1, delegate()->found_entry_.response_id());
    EXPECT_TRUE(delegate()->found_entry_.IsExplicit());
    EXPECT_FALSE(delegate()->found_fallback_entry_.has_response_id());
    TestFinished();
  }

  // BasicFindMainFallbackResponse  -------------------------------

  void BasicFindMainFallbackResponseInDatabase() {
    BasicFindMainFallbackResponse(true);
  }

  void BasicFindMainFallbackResponseInWorkingSet() {
    BasicFindMainFallbackResponse(false);
  }

  void BasicFindMainFallbackResponse(bool drop_from_working_set) {
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_BasicFindMainFallbackResponse,
        base::Unretained(this)));

    // Setup some preconditions. Create a complete cache with a
    // fallback namespace and entry.
    MakeCacheAndGroup(kManifestUrl, 2, 1, true);
    cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::FALLBACK, 1));
    cache_->AddEntry(kEntryUrl2, AppCacheEntry(AppCacheEntry::FALLBACK, 2));
    cache_->fallback_namespaces_.push_back(
        Namespace(FALLBACK_NAMESPACE, kFallbackNamespace2, kEntryUrl2));
    cache_->fallback_namespaces_.push_back(
        Namespace(FALLBACK_NAMESPACE, kFallbackNamespace, kEntryUrl));
    AppCacheDatabase::CacheRecord cache_record;
    std::vector<AppCacheDatabase::EntryRecord> entries;
    std::vector<AppCacheDatabase::NamespaceRecord> intercepts;
    std::vector<AppCacheDatabase::NamespaceRecord> fallbacks;
    std::vector<AppCacheDatabase::OnlineWhiteListRecord> whitelists;
    cache_->ToDatabaseRecords(group_,
        &cache_record, &entries, &intercepts, &fallbacks, &whitelists);

    std::vector<AppCacheDatabase::EntryRecord>::const_iterator iter =
        entries.begin();
    while (iter != entries.end()) {
      // MakeCacheAndGroup has inserted the default entry record already.
      if (iter->url != kDefaultEntryUrl)
        EXPECT_TRUE(database()->InsertEntry(&(*iter)));
      ++iter;
    }

    EXPECT_TRUE(database()->InsertNamespaceRecords(fallbacks));
    EXPECT_TRUE(database()->InsertOnlineWhiteListRecords(whitelists));
    if (drop_from_working_set) {
      EXPECT_TRUE(cache_->HasOneRef());
      cache_ = NULL;
      EXPECT_TRUE(group_->HasOneRef());
      group_ = NULL;
    }

    // Conduct the test. The test url is in both fallback namespace urls,
    // but should match the longer of the two.
    storage()->FindResponseForMainRequest(kFallbackTestUrl, GURL(), delegate());
    EXPECT_NE(kFallbackTestUrl, delegate()->found_url_);
  }

  void Verify_BasicFindMainFallbackResponse() {
    EXPECT_EQ(kFallbackTestUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl, delegate()->found_manifest_url_);
    EXPECT_EQ(1, delegate()->found_cache_id_);
    EXPECT_EQ(2, delegate()->found_group_id_);
    EXPECT_FALSE(delegate()->found_entry_.has_response_id());
    EXPECT_EQ(2, delegate()->found_fallback_entry_.response_id());
    EXPECT_EQ(kEntryUrl2, delegate()->found_namespace_entry_url_);
    EXPECT_TRUE(delegate()->found_fallback_entry_.IsFallback());
    TestFinished();
  }

  // BasicFindMainInterceptResponse  -------------------------------

  void BasicFindMainInterceptResponseInDatabase() {
    BasicFindMainInterceptResponse(true);
  }

  void BasicFindMainInterceptResponseInWorkingSet() {
    BasicFindMainInterceptResponse(false);
  }

  void BasicFindMainInterceptResponse(bool drop_from_working_set) {
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_BasicFindMainInterceptResponse,
        base::Unretained(this)));

    // Setup some preconditions. Create a complete cache with an
    // intercept namespace and entry.
    MakeCacheAndGroup(kManifestUrl, 2, 1, true);
    cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::INTERCEPT, 1));
    cache_->AddEntry(kEntryUrl2, AppCacheEntry(AppCacheEntry::INTERCEPT, 2));
    cache_->intercept_namespaces_.push_back(
        Namespace(INTERCEPT_NAMESPACE, kInterceptNamespace2, kEntryUrl2));
    cache_->intercept_namespaces_.push_back(
        Namespace(INTERCEPT_NAMESPACE, kInterceptNamespace, kEntryUrl));
    AppCacheDatabase::CacheRecord cache_record;
    std::vector<AppCacheDatabase::EntryRecord> entries;
    std::vector<AppCacheDatabase::NamespaceRecord> intercepts;
    std::vector<AppCacheDatabase::NamespaceRecord> fallbacks;
    std::vector<AppCacheDatabase::OnlineWhiteListRecord> whitelists;
    cache_->ToDatabaseRecords(group_,
        &cache_record, &entries, &intercepts, &fallbacks, &whitelists);

    std::vector<AppCacheDatabase::EntryRecord>::const_iterator iter =
        entries.begin();
    while (iter != entries.end()) {
      // MakeCacheAndGroup has inserted  the default entry record already
      if (iter->url != kDefaultEntryUrl)
        EXPECT_TRUE(database()->InsertEntry(&(*iter)));
      ++iter;
    }

    EXPECT_TRUE(database()->InsertNamespaceRecords(intercepts));
    EXPECT_TRUE(database()->InsertOnlineWhiteListRecords(whitelists));
    if (drop_from_working_set) {
      EXPECT_TRUE(cache_->HasOneRef());
      cache_ = NULL;
      EXPECT_TRUE(group_->HasOneRef());
      group_ = NULL;
    }

    // Conduct the test. The test url is in both intercept namespaces,
    // but should match the longer of the two.
    storage()->FindResponseForMainRequest(
        kInterceptTestUrl, GURL(), delegate());
    EXPECT_NE(kInterceptTestUrl, delegate()->found_url_);
  }

  void Verify_BasicFindMainInterceptResponse() {
    EXPECT_EQ(kInterceptTestUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl, delegate()->found_manifest_url_);
    EXPECT_EQ(1, delegate()->found_cache_id_);
    EXPECT_EQ(2, delegate()->found_group_id_);
    EXPECT_EQ(2, delegate()->found_entry_.response_id());
    EXPECT_TRUE(delegate()->found_entry_.IsIntercept());
    EXPECT_EQ(kEntryUrl2, delegate()->found_namespace_entry_url_);
    EXPECT_FALSE(delegate()->found_fallback_entry_.has_response_id());
    TestFinished();
  }
  // FindMainResponseWithMultipleHits  -------------------------------

  void FindMainResponseWithMultipleHits() {
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_FindMainResponseWithMultipleHits,
        base::Unretained(this)));

    // Setup some preconditions, create a few caches with an identical set
    // of entries and fallback namespaces. Only the last one remains in
    // the working set to simulate appearing as "in use".
    MakeMultipleHitCacheAndGroup(kManifestUrl, 1);
    MakeMultipleHitCacheAndGroup(kManifestUrl2, 2);
    MakeMultipleHitCacheAndGroup(kManifestUrl3, 3);

    // Conduct the test, we should find the response from the last cache
    // since it's "in use".
    storage()->FindResponseForMainRequest(kEntryUrl, GURL(), delegate());
    EXPECT_NE(kEntryUrl, delegate()->found_url_);
  }

  void MakeMultipleHitCacheAndGroup(const GURL& manifest_url, int id) {
    MakeCacheAndGroup(manifest_url, id, id, true);
    AppCacheDatabase::EntryRecord entry_record;

    // Add an entry for kEntryUrl
    entry_record.cache_id = id;
    entry_record.url = kEntryUrl;
    entry_record.flags = AppCacheEntry::EXPLICIT;
    entry_record.response_id = id;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));
    cache_->AddEntry(
        entry_record.url,
        AppCacheEntry(entry_record.flags, entry_record.response_id));

    // Add an entry for the manifestUrl
    entry_record.cache_id = id;
    entry_record.url = manifest_url;
    entry_record.flags = AppCacheEntry::MANIFEST;
    entry_record.response_id = id + kManifestEntryIdOffset;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));
    cache_->AddEntry(
        entry_record.url,
        AppCacheEntry(entry_record.flags, entry_record.response_id));

    // Add a fallback entry and namespace
    entry_record.cache_id = id;
    entry_record.url = kEntryUrl2;
    entry_record.flags = AppCacheEntry::FALLBACK;
    entry_record.response_id = id + kFallbackEntryIdOffset;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));
    cache_->AddEntry(
        entry_record.url,
        AppCacheEntry(entry_record.flags, entry_record.response_id));
    AppCacheDatabase::NamespaceRecord fallback_namespace_record;
    fallback_namespace_record.cache_id = id;
    fallback_namespace_record.target_url = entry_record.url;
    fallback_namespace_record.namespace_url = kFallbackNamespace;
    fallback_namespace_record.origin = manifest_url.GetOrigin();
    EXPECT_TRUE(database()->InsertNamespace(&fallback_namespace_record));
    cache_->fallback_namespaces_.push_back(
        Namespace(FALLBACK_NAMESPACE, kFallbackNamespace, kEntryUrl2));
  }

  void Verify_FindMainResponseWithMultipleHits() {
    EXPECT_EQ(kEntryUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl3, delegate()->found_manifest_url_);
    EXPECT_EQ(3, delegate()->found_cache_id_);
    EXPECT_EQ(3, delegate()->found_group_id_);
    EXPECT_EQ(3, delegate()->found_entry_.response_id());
    EXPECT_TRUE(delegate()->found_entry_.IsExplicit());
    EXPECT_FALSE(delegate()->found_fallback_entry_.has_response_id());

    // Conduct another test perferring kManifestUrl
    delegate_.reset(new MockStorageDelegate(this));
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_FindMainResponseWithMultipleHits2,
        base::Unretained(this)));
    storage()->FindResponseForMainRequest(kEntryUrl, kManifestUrl, delegate());
    EXPECT_NE(kEntryUrl, delegate()->found_url_);
  }

  void Verify_FindMainResponseWithMultipleHits2() {
    EXPECT_EQ(kEntryUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl, delegate()->found_manifest_url_);
    EXPECT_EQ(1, delegate()->found_cache_id_);
    EXPECT_EQ(1, delegate()->found_group_id_);
    EXPECT_EQ(1, delegate()->found_entry_.response_id());
    EXPECT_TRUE(delegate()->found_entry_.IsExplicit());
    EXPECT_FALSE(delegate()->found_fallback_entry_.has_response_id());

    // Conduct the another test perferring kManifestUrl2
    delegate_.reset(new MockStorageDelegate(this));
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_FindMainResponseWithMultipleHits3,
        base::Unretained(this)));
    storage()->FindResponseForMainRequest(kEntryUrl, kManifestUrl2, delegate());
    EXPECT_NE(kEntryUrl, delegate()->found_url_);
  }

  void Verify_FindMainResponseWithMultipleHits3() {
    EXPECT_EQ(kEntryUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl2, delegate()->found_manifest_url_);
    EXPECT_EQ(2, delegate()->found_cache_id_);
    EXPECT_EQ(2, delegate()->found_group_id_);
    EXPECT_EQ(2, delegate()->found_entry_.response_id());
    EXPECT_TRUE(delegate()->found_entry_.IsExplicit());
    EXPECT_FALSE(delegate()->found_fallback_entry_.has_response_id());

    // Conduct another test with no preferred manifest that hits the fallback.
    delegate_.reset(new MockStorageDelegate(this));
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_FindMainResponseWithMultipleHits4,
        base::Unretained(this)));
    storage()->FindResponseForMainRequest(
        kFallbackTestUrl, GURL(), delegate());
    EXPECT_NE(kFallbackTestUrl, delegate()->found_url_);
  }

  void Verify_FindMainResponseWithMultipleHits4() {
    EXPECT_EQ(kFallbackTestUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl3, delegate()->found_manifest_url_);
    EXPECT_EQ(3, delegate()->found_cache_id_);
    EXPECT_EQ(3, delegate()->found_group_id_);
    EXPECT_FALSE(delegate()->found_entry_.has_response_id());
    EXPECT_EQ(3 + kFallbackEntryIdOffset,
              delegate()->found_fallback_entry_.response_id());
    EXPECT_TRUE(delegate()->found_fallback_entry_.IsFallback());
    EXPECT_EQ(kEntryUrl2, delegate()->found_namespace_entry_url_);

    // Conduct another test preferring kManifestUrl2 that hits the fallback.
    delegate_.reset(new MockStorageDelegate(this));
    PushNextTask(base::Bind(
        &AppCacheStorageImplTest::Verify_FindMainResponseWithMultipleHits5,
        base::Unretained(this)));
    storage()->FindResponseForMainRequest(
        kFallbackTestUrl, kManifestUrl2, delegate());
    EXPECT_NE(kFallbackTestUrl, delegate()->found_url_);
  }

  void Verify_FindMainResponseWithMultipleHits5() {
    EXPECT_EQ(kFallbackTestUrl, delegate()->found_url_);
    EXPECT_EQ(kManifestUrl2, delegate()->found_manifest_url_);
    EXPECT_EQ(2, delegate()->found_cache_id_);
    EXPECT_EQ(2, delegate()->found_group_id_);
    EXPECT_FALSE(delegate()->found_entry_.has_response_id());
    EXPECT_EQ(2 + kFallbackEntryIdOffset,
              delegate()->found_fallback_entry_.response_id());
    EXPECT_TRUE(delegate()->found_fallback_entry_.IsFallback());
    EXPECT_EQ(kEntryUrl2, delegate()->found_namespace_entry_url_);

    TestFinished();
  }

  // FindMainResponseExclusions  -------------------------------

  void FindMainResponseExclusionsInDatabase() {
    FindMainResponseExclusions(true);
  }

  void FindMainResponseExclusionsInWorkingSet() {
    FindMainResponseExclusions(false);
  }

  void FindMainResponseExclusions(bool drop_from_working_set) {
    // Setup some preconditions. Create a complete cache with a
    // foreign entry, an online namespace, and a second online
    // namespace nested within a fallback namespace.
    MakeCacheAndGroup(kManifestUrl, 1, 1, true);
    cache_->AddEntry(kEntryUrl,
        AppCacheEntry(AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN, 1));
    cache_->online_whitelist_namespaces_.push_back(kOnlineNamespace);
    cache_->AddEntry(kEntryUrl2, AppCacheEntry(AppCacheEntry::FALLBACK, 2));
    cache_->fallback_namespaces_.push_back(
        Namespace(FALLBACK_NAMESPACE, kFallbackNamespace, kEntryUrl2));
    cache_->online_whitelist_namespaces_.push_back(kOnlineNamespace);
    cache_->online_whitelist_namespaces_.push_back(
        kOnlineNamespaceWithinFallback);

    AppCacheDatabase::EntryRecord entry_record;
    entry_record.cache_id = 1;
    entry_record.url = kEntryUrl;
    entry_record.flags = AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN;
    entry_record.response_id = 1;
    EXPECT_TRUE(database()->InsertEntry(&entry_record));
    AppCacheDatabase::OnlineWhiteListRecord whitelist_record;
    whitelist_record.cache_id = 1;
    whitelist_record.namespace_url = kOnlineNamespace;
    EXPECT_TRUE(database()->InsertOnlineWhiteList(&whitelist_record));
    AppCacheDatabase::NamespaceRecord fallback_namespace_record;
    fallback_namespace_record.cache_id = 1;
    fallback_namespace_record.target_url = kEntryUrl2;
    fallback_namespace_record.namespace_url = kFallbackNamespace;
    fallback_namespace_record.origin = kManifestUrl.GetOrigin();
    EXPECT_TRUE(database()->InsertNamespace(&fallback_namespace_record));
    whitelist_record.cache_id = 1;
    whitelist_record.namespace_url = kOnlineNamespaceWithinFallback;
    EXPECT_TRUE(database()->InsertOnlineWhiteList(&whitelist_record));
    if (drop_from_working_set) {
      cache_ = NULL;
      group_ = NULL;
    }

    // We should not find anything for the foreign entry.
    PushNextTask(base::Bind(&AppCacheStorageImplTest::Verify_ExclusionNotFound,
                            base::Unretained(this), kEntryUrl, 1));
    storage()->FindResponseForMainRequest(kEntryUrl, GURL(), delegate());
  }

  void Verify_ExclusionNotFound(GURL expected_url, int phase) {
    EXPECT_EQ(expected_url, delegate()->found_url_);
    EXPECT_TRUE(delegate()->found_manifest_url_.is_empty());
    EXPECT_EQ(kNoCacheId, delegate()->found_cache_id_);
    EXPECT_EQ(0, delegate()->found_group_id_);
    EXPECT_EQ(kNoResponseId, delegate()->found_entry_.response_id());
    EXPECT_EQ(kNoResponseId, delegate()->found_fallback_entry_.response_id());
    EXPECT_TRUE(delegate()->found_namespace_entry_url_.is_empty());
    EXPECT_EQ(0, delegate()->found_entry_.types());
    EXPECT_EQ(0, delegate()->found_fallback_entry_.types());

    if (phase == 1) {
      // We should not find anything for the online namespace.
      PushNextTask(
          base::Bind(&AppCacheStorageImplTest::Verify_ExclusionNotFound,
                     base::Unretained(this), kOnlineNamespace, 2));
      storage()->FindResponseForMainRequest(
          kOnlineNamespace, GURL(), delegate());
      return;
    }
    if (phase == 2) {
      // We should not find anything for the online namespace nested within
      // the fallback namespace.
      PushNextTask(base::Bind(
          &AppCacheStorageImplTest::Verify_ExclusionNotFound,
          base::Unretained(this), kOnlineNamespaceWithinFallback, 3));
      storage()->FindResponseForMainRequest(
          kOnlineNamespaceWithinFallback, GURL(), delegate());
      return;
    }

    TestFinished();
  }

  // Test case helpers --------------------------------------------------

  AppCacheService* service() {
    return service_.get();
  }

  AppCacheStorageImpl* storage() {
    return static_cast<AppCacheStorageImpl*>(service()->storage());
  }

  AppCacheDatabase* database() {
    return storage()->database_;
  }

  MockStorageDelegate* delegate() {
    return delegate_.get();
  }

  void MakeCacheAndGroup(
      const GURL& manifest_url, int64 group_id, int64 cache_id,
      bool add_to_database) {
    AppCacheEntry default_entry(
        AppCacheEntry::EXPLICIT, cache_id + kDefaultEntryIdOffset,
        kDefaultEntrySize);
    group_ = new AppCacheGroup(service(), manifest_url, group_id);
    cache_ = new AppCache(service(), cache_id);
    cache_->AddEntry(kDefaultEntryUrl, default_entry);
    cache_->set_complete(true);
    group_->AddCache(cache_);
    if (add_to_database) {
      AppCacheDatabase::GroupRecord group_record;
      group_record.group_id = group_id;
      group_record.manifest_url = manifest_url;
      group_record.origin = manifest_url.GetOrigin();
      EXPECT_TRUE(database()->InsertGroup(&group_record));
      AppCacheDatabase::CacheRecord cache_record;
      cache_record.cache_id = cache_id;
      cache_record.group_id = group_id;
      cache_record.online_wildcard = false;
      cache_record.update_time = kZeroTime;
      cache_record.cache_size = kDefaultEntrySize;
      EXPECT_TRUE(database()->InsertCache(&cache_record));
      AppCacheDatabase::EntryRecord entry_record;
      entry_record.cache_id = cache_id;
      entry_record.url = kDefaultEntryUrl;
      entry_record.flags = default_entry.types();
      entry_record.response_id = default_entry.response_id();
      entry_record.response_size = default_entry.response_size();
      EXPECT_TRUE(database()->InsertEntry(&entry_record));

      storage()->usage_map_[manifest_url.GetOrigin()] =
          default_entry.response_size();
    }
  }

  // Data members --------------------------------------------------

  scoped_ptr<base::WaitableEvent> test_finished_event_;
  std::stack<base::Closure> task_stack_;
  scoped_ptr<AppCacheService> service_;
  scoped_ptr<MockStorageDelegate> delegate_;
  scoped_refptr<MockQuotaManagerProxy> mock_quota_manager_proxy_;
  scoped_refptr<AppCacheGroup> group_;
  scoped_refptr<AppCache> cache_;
  scoped_refptr<AppCache> cache2_;
};


TEST_F(AppCacheStorageImplTest, LoadCache_Miss) {
  RunTestOnIOThread(&AppCacheStorageImplTest::LoadCache_Miss);
}

TEST_F(AppCacheStorageImplTest, LoadCache_NearHit) {
  RunTestOnIOThread(&AppCacheStorageImplTest::LoadCache_NearHit);
}

TEST_F(AppCacheStorageImplTest, CreateGroupInEmptyOrigin) {
  RunTestOnIOThread(&AppCacheStorageImplTest::CreateGroupInEmptyOrigin);
}

TEST_F(AppCacheStorageImplTest, CreateGroupInPopulatedOrigin) {
  RunTestOnIOThread(&AppCacheStorageImplTest::CreateGroupInPopulatedOrigin);
}

TEST_F(AppCacheStorageImplTest, LoadGroupAndCache_FarHit) {
  RunTestOnIOThread(&AppCacheStorageImplTest::LoadGroupAndCache_FarHit);
}

TEST_F(AppCacheStorageImplTest, StoreNewGroup) {
  RunTestOnIOThread(&AppCacheStorageImplTest::StoreNewGroup);
}

TEST_F(AppCacheStorageImplTest, StoreExistingGroup) {
  RunTestOnIOThread(&AppCacheStorageImplTest::StoreExistingGroup);
}

TEST_F(AppCacheStorageImplTest, StoreExistingGroupExistingCache) {
  RunTestOnIOThread(&AppCacheStorageImplTest::StoreExistingGroupExistingCache);
}

TEST_F(AppCacheStorageImplTest, FailStoreGroup) {
  RunTestOnIOThread(&AppCacheStorageImplTest::FailStoreGroup);
}

TEST_F(AppCacheStorageImplTest, MakeGroupObsolete) {
  RunTestOnIOThread(&AppCacheStorageImplTest::MakeGroupObsolete);
}

TEST_F(AppCacheStorageImplTest, MarkEntryAsForeign) {
  RunTestOnIOThread(&AppCacheStorageImplTest::MarkEntryAsForeign);
}

TEST_F(AppCacheStorageImplTest, MarkEntryAsForeignWithLoadInProgress) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::MarkEntryAsForeignWithLoadInProgress);
}

TEST_F(AppCacheStorageImplTest, FindNoMainResponse) {
  RunTestOnIOThread(&AppCacheStorageImplTest::FindNoMainResponse);
}

TEST_F(AppCacheStorageImplTest, BasicFindMainResponseInDatabase) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::BasicFindMainResponseInDatabase);
}

TEST_F(AppCacheStorageImplTest, BasicFindMainResponseInWorkingSet) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::BasicFindMainResponseInWorkingSet);
}

TEST_F(AppCacheStorageImplTest, BasicFindMainFallbackResponseInDatabase) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::BasicFindMainFallbackResponseInDatabase);
}

TEST_F(AppCacheStorageImplTest, BasicFindMainFallbackResponseInWorkingSet) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::BasicFindMainFallbackResponseInWorkingSet);
}

TEST_F(AppCacheStorageImplTest, BasicFindMainInterceptResponseInDatabase) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::BasicFindMainInterceptResponseInDatabase);
}

TEST_F(AppCacheStorageImplTest, BasicFindMainInterceptResponseInWorkingSet) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::BasicFindMainInterceptResponseInWorkingSet);
}

TEST_F(AppCacheStorageImplTest, FindMainResponseWithMultipleHits) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::FindMainResponseWithMultipleHits);
}

TEST_F(AppCacheStorageImplTest, FindMainResponseExclusionsInDatabase) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::FindMainResponseExclusionsInDatabase);
}

TEST_F(AppCacheStorageImplTest, FindMainResponseExclusionsInWorkingSet) {
  RunTestOnIOThread(
      &AppCacheStorageImplTest::FindMainResponseExclusionsInWorkingSet);
}

// That's all folks!

}  // namespace appcache