// 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 <string>

#include "base/bind.h"
#include "base/file_path.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/stl_util.h"
#include "base/string16.h"
#include "base/time.h"
#include "base/values.h"
#include "chrome/browser/download/download_query.h"
#include "content/public/test/mock_download_item.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::_;
using base::Time;
using base::Value;
using content::DownloadItem;
typedef DownloadQuery::DownloadVector DownloadVector;

namespace {

bool IdNotEqual(int not_id, const DownloadItem& item) {
  return item.GetId() != not_id;
}

bool AlwaysReturn(bool result, const DownloadItem& item) {
  return result;
}

}  // anonymous namespace

class DownloadQueryTest : public testing::Test {
 public:
  DownloadQueryTest() {}

  virtual ~DownloadQueryTest() {}

  virtual void TearDown() {
    STLDeleteElements(&mocks_);
  }

  void CreateMocks(int count) {
    for (int i = 0; i < count; ++i) {
      mocks_.push_back(new content::MockDownloadItem());
    }
  }

  content::MockDownloadItem& mock(int index) { return *mocks_[index]; }

  DownloadQuery* query() { return &query_; }

  template<typename ValueType> void AddFilter(
      DownloadQuery::FilterType name, ValueType value);

  void Search() {
    query_.Search(mocks_.begin(), mocks_.end(), &results_);
  }

  DownloadVector* results() { return &results_; }

 private:
  std::vector<content::MockDownloadItem*> mocks_;
  DownloadQuery query_;
  DownloadVector results_;

  DISALLOW_COPY_AND_ASSIGN(DownloadQueryTest);
};

template<> void DownloadQueryTest::AddFilter(
    DownloadQuery::FilterType name, bool cpp_value) {
  scoped_ptr<base::Value> value(Value::CreateBooleanValue(cpp_value));
  CHECK(query_.AddFilter(name, *value.get()));
}

template<> void DownloadQueryTest::AddFilter(
    DownloadQuery::FilterType name, int cpp_value) {
  scoped_ptr<base::Value> value(Value::CreateIntegerValue(cpp_value));
  CHECK(query_.AddFilter(name, *value.get()));
}

template<> void DownloadQueryTest::AddFilter(
    DownloadQuery::FilterType name, const char* cpp_value) {
  scoped_ptr<base::Value> value(Value::CreateStringValue(cpp_value));
  CHECK(query_.AddFilter(name, *value.get()));
}

template<> void DownloadQueryTest::AddFilter(
    DownloadQuery::FilterType name, const char16* cpp_value) {
  scoped_ptr<base::Value> value(Value::CreateStringValue(string16(cpp_value)));
  CHECK(query_.AddFilter(name, *value.get()));
}

TEST_F(DownloadQueryTest, DownloadQueryEmptyNoItems) {
  Search();
  EXPECT_EQ(0U, results()->size());
}

TEST_F(DownloadQueryTest, DownloadQueryEmptySomeItems) {
  CreateMocks(3);
  Search();
  EXPECT_EQ(3U, results()->size());
}

TEST_F(DownloadQueryTest, DownloadQueryInvalidFilters) {
  scoped_ptr<base::Value> value(Value::CreateIntegerValue(0));
  EXPECT_FALSE(query()->AddFilter(
      static_cast<DownloadQuery::FilterType>(kint32max),
      *value.get()));
}

TEST_F(DownloadQueryTest, DownloadQueryLimit) {
  CreateMocks(2);
  query()->Limit(1);
  Search();
  EXPECT_EQ(1U, results()->size());
}

// Syntactic sugar for an expression version of the switch-case statement.
// Cf. Lisp's |case| form.
#define SWITCH2(_index, _col1, _ret1, _default) \
  ((_index == (_col1)) ? _ret1 : _default)
#define SWITCH3(_index, _col1, _ret1, _col2, _ret2, _default) \
  SWITCH2(_index, _col1, _ret1, SWITCH2(_index, _col2, _ret2, _default))
#define SWITCH4(_index, _col1, _ret1, _col2, _ret2, _col3, _ret3, _default) \
  SWITCH3(_index, _col1, _ret1, _col2, _ret2, \
      SWITCH2(_index, _col3, _ret3, _default))

TEST_F(DownloadQueryTest, DownloadQuery_QueryFilter) {
  FilePath match_filename(FILE_PATH_LITERAL("query"));
  FilePath fail_filename(FILE_PATH_LITERAL("fail"));
  GURL fail_url("http://example.com/fail");
  GURL match_url("http://query.com/query");
  scoped_ptr<base::Value> query_str(base::Value::CreateStringValue("query"));
  static const size_t kNumItems = 4;
  CreateMocks(kNumItems);
  for (size_t i = 0; i < kNumItems; ++i) {
    if (i != 0)
      EXPECT_CALL(mock(i), GetId()).WillOnce(Return(i));
    EXPECT_CALL(mock(i), GetTargetFilePath())
      .WillRepeatedly(ReturnRef((i & 1) ? match_filename : fail_filename));
    EXPECT_CALL(mock(i), GetOriginalUrl())
      .WillRepeatedly(ReturnRef((i & 2) ? match_url : fail_url));
    EXPECT_CALL(mock(i), GetBrowserContext()).WillRepeatedly(Return(
        static_cast<content::BrowserContext*>(NULL)));
  }
  query()->AddFilter(DownloadQuery::FILTER_QUERY, *query_str.get());
  Search();
  ASSERT_EQ(3U, results()->size());
  EXPECT_EQ(1, results()->at(0)->GetId());
  EXPECT_EQ(2, results()->at(1)->GetId());
  EXPECT_EQ(3, results()->at(2)->GetId());

  // TODO(phajdan.jr): Also test these strings:
  //   "/\xe4\xbd\xa0\xe5\xa5\xbd\xe4\xbd\xa0\xe5\xa5\xbd",
  //   L"/\x4f60\x597d\x4f60\x597d",
  //   "/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD"
}

TEST_F(DownloadQueryTest, DownloadQueryAllFilters) {
  // Set up mocks such that only mock(0) matches all filters, and every other
  // mock fails a different filter (or two for GREATER/LESS filters).
  static const size_t kNumItems = 18;
  CreateMocks(kNumItems);
  FilePath refail_filename(FILE_PATH_LITERAL("z"));
  FilePath fail_filename(FILE_PATH_LITERAL("fail"));
  FilePath match_filename(FILE_PATH_LITERAL("match"));
  GURL refail_url("http://z.com/");
  GURL fail_url("http://example.com/fail");
  GURL match_url("http://example.com/match");
  // Picture a 2D matrix. The rows are MockDownloadItems and the columns are
  // filter types. Every cell contains a value that matches all filters, except
  // for the diagonal. Every item matches all the filters except one filter,
  // which it fails, except one item, which matches all the filters without
  // exception. Each mocked method is used to test (corresponds to) one or more
  // filter types (columns). For example, GetTotalBytes() is used to test
  // FILTER_TOTAL_BYTES_GREATER, FILTER_TOTAL_BYTES_LESS, and
  // FILTER_TOTAL_BYTES, so it uses 3 columns: it returns 1 for row (item) 11,
  // it returns 4 for row 12, 3 for 13, and it returns 2 for all other rows
  // (items).
  for (size_t i = 0; i < kNumItems; ++i) {
    EXPECT_CALL(mock(i), GetId()).WillRepeatedly(Return(i));
    EXPECT_CALL(mock(i), GetReceivedBytes()).WillRepeatedly(Return(SWITCH2(i,
        1, 2,
           1)));
    EXPECT_CALL(mock(i), GetSafetyState()).WillRepeatedly(Return(SWITCH2(i,
        2, DownloadItem::DANGEROUS,
           DownloadItem::DANGEROUS_BUT_VALIDATED)));
    EXPECT_CALL(mock(i), GetTargetFilePath())
      .WillRepeatedly(ReturnRef(SWITCH3(i,
        3, refail_filename,
        4, fail_filename,
           match_filename)));
    EXPECT_CALL(mock(i), GetMimeType()).WillRepeatedly(Return(SWITCH2(i,
        5, "image",
           "text")));
    EXPECT_CALL(mock(i), IsPaused()).WillRepeatedly(Return(SWITCH2(i,
        6, false,
           true)));
    EXPECT_CALL(mock(i), GetStartTime()).WillRepeatedly(Return(SWITCH4(i,
        7, base::Time::FromTimeT(1),
        8, base::Time::FromTimeT(4),
        9, base::Time::FromTimeT(3),
           base::Time::FromTimeT(2))));
    EXPECT_CALL(mock(i), GetTotalBytes()).WillRepeatedly(Return(SWITCH4(i,
        10, 1,
        11, 4,
        12, 3,
            2)));
    EXPECT_CALL(mock(i), GetOriginalUrl()).WillRepeatedly(ReturnRef(SWITCH3(i,
        13, refail_url,
        14, fail_url,
            match_url)));
    // 15 is AddFilter(Bind(IdNotEqual, 15))
    EXPECT_CALL(mock(i), GetState()).WillRepeatedly(Return(SWITCH2(i,
        16, DownloadItem::CANCELLED,
            DownloadItem::IN_PROGRESS)));
    EXPECT_CALL(mock(i), GetDangerType()).WillRepeatedly(Return(SWITCH2(i,
        17, content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE,
            content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS)));
  }
  for (size_t i = 0; i < kNumItems; ++i) {
    switch (i) {
      case 0: break;
      case 1: AddFilter(DownloadQuery::FILTER_BYTES_RECEIVED, 1); break;
      case 2: AddFilter(DownloadQuery::FILTER_DANGER_ACCEPTED, true);
              break;
      case 3: AddFilter(DownloadQuery::FILTER_FILENAME_REGEX, "a"); break;
      case 4: AddFilter(DownloadQuery::FILTER_FILENAME,
                        match_filename.value().c_str()); break;
      case 5: AddFilter(DownloadQuery::FILTER_MIME, "text"); break;
      case 6: AddFilter(DownloadQuery::FILTER_PAUSED, true); break;
      case 7: AddFilter(DownloadQuery::FILTER_STARTED_AFTER, 1000); break;
      case 8: AddFilter(DownloadQuery::FILTER_STARTED_BEFORE, 4000);
              break;
      case 9: AddFilter(DownloadQuery::FILTER_START_TIME, 2000); break;
      case 10: AddFilter(DownloadQuery::FILTER_TOTAL_BYTES_GREATER, 1);
               break;
      case 11: AddFilter(DownloadQuery::FILTER_TOTAL_BYTES_LESS, 4);
               break;
      case 12: AddFilter(DownloadQuery::FILTER_TOTAL_BYTES, 2); break;
      case 13: AddFilter(DownloadQuery::FILTER_URL_REGEX, "example");
               break;
      case 14: AddFilter(DownloadQuery::FILTER_URL,
                         match_url.spec().c_str()); break;
      case 15: CHECK(query()->AddFilter(base::Bind(&IdNotEqual, 15))); break;
      case 16: query()->AddFilter(DownloadItem::IN_PROGRESS); break;
      case 17: query()->AddFilter(content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS);
        break;
      default: NOTREACHED(); break;
    }
    Search();
    ASSERT_EQ(kNumItems - i, results()->size())
      << "Failing filter: " << i;
    if (i > 0) {
      ASSERT_EQ(0, results()->at(0)->GetId())
        << "Failing filter: " << i;
      for (size_t j = 1; j < kNumItems - i; ++j) {
        ASSERT_EQ(static_cast<int32>(j + i), results()->at(j)->GetId())
          << "Failing filter: " << i;
      }
    }
  }
}

TEST_F(DownloadQueryTest, DownloadQuerySortBytesReceived) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetReceivedBytes()).WillRepeatedly(Return(0));
  EXPECT_CALL(mock(1), GetReceivedBytes()).WillRepeatedly(Return(1));
  query()->AddSorter(
      DownloadQuery::SORT_BYTES_RECEIVED, DownloadQuery::DESCENDING);
  Search();
  EXPECT_EQ(1, results()->at(0)->GetReceivedBytes());
  EXPECT_EQ(0, results()->at(1)->GetReceivedBytes());
}

TEST_F(DownloadQueryTest, DownloadQuerySortDanger) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetDangerType()).WillRepeatedly(Return(
        content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE));
  EXPECT_CALL(mock(1), GetDangerType()).WillRepeatedly(Return(
        content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS));
  query()->AddSorter(
      DownloadQuery::SORT_DANGER, DownloadQuery::ASCENDING);
  Search();
  EXPECT_EQ(content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS,
            results()->at(0)->GetDangerType());
  EXPECT_EQ(content::DOWNLOAD_DANGER_TYPE_DANGEROUS_FILE,
            results()->at(1)->GetDangerType());
}

TEST_F(DownloadQueryTest, DownloadQuerySortDangerAccepted) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetSafetyState()).WillRepeatedly(Return(
        DownloadItem::DANGEROUS));
  EXPECT_CALL(mock(1), GetSafetyState()).WillRepeatedly(Return(
        DownloadItem::DANGEROUS_BUT_VALIDATED));
  query()->AddSorter(
      DownloadQuery::SORT_DANGER_ACCEPTED, DownloadQuery::DESCENDING);
  Search();
  EXPECT_EQ(DownloadItem::DANGEROUS_BUT_VALIDATED,
            results()->at(0)->GetSafetyState());
  EXPECT_EQ(DownloadItem::DANGEROUS, results()->at(1)->GetSafetyState());
}

TEST_F(DownloadQueryTest, DownloadQuerySortFilename) {
  CreateMocks(2);
  FilePath a_path(FILE_PATH_LITERAL("a"));
  FilePath b_path(FILE_PATH_LITERAL("b"));
  EXPECT_CALL(mock(0), GetTargetFilePath()).WillRepeatedly(ReturnRef(b_path));
  EXPECT_CALL(mock(1), GetTargetFilePath()).WillRepeatedly(ReturnRef(a_path));
  query()->AddSorter(
      DownloadQuery::SORT_FILENAME, DownloadQuery::ASCENDING);
  Search();
  EXPECT_EQ(a_path, results()->at(0)->GetTargetFilePath());
  EXPECT_EQ(b_path, results()->at(1)->GetTargetFilePath());
}

TEST_F(DownloadQueryTest, DownloadQuerySortMime) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetMimeType()).WillRepeatedly(Return("a"));
  EXPECT_CALL(mock(1), GetMimeType()).WillRepeatedly(Return("b"));
  query()->AddSorter(
      DownloadQuery::SORT_MIME, DownloadQuery::DESCENDING);
  Search();
  EXPECT_EQ("b", results()->at(0)->GetMimeType());
  EXPECT_EQ("a", results()->at(1)->GetMimeType());
}

TEST_F(DownloadQueryTest, DownloadQuerySortPaused) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), IsPaused()).WillRepeatedly(Return(true));
  EXPECT_CALL(mock(1), IsPaused()).WillRepeatedly(Return(false));
  query()->AddSorter(
      DownloadQuery::SORT_PAUSED, DownloadQuery::ASCENDING);
  Search();
  EXPECT_FALSE(results()->at(0)->IsPaused());
  EXPECT_TRUE(results()->at(1)->IsPaused());
}

TEST_F(DownloadQueryTest, DownloadQuerySortStartTime) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetStartTime()).WillRepeatedly(Return(
        base::Time::FromTimeT(0)));
  EXPECT_CALL(mock(1), GetStartTime()).WillRepeatedly(Return(
        base::Time::FromTimeT(1)));
  query()->AddSorter(
      DownloadQuery::SORT_START_TIME, DownloadQuery::DESCENDING);
  Search();
  EXPECT_EQ(base::Time::FromTimeT(1), results()->at(0)->GetStartTime());
  EXPECT_EQ(base::Time::FromTimeT(0), results()->at(1)->GetStartTime());
}

TEST_F(DownloadQueryTest, DownloadQuerySortState) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetState()).WillRepeatedly(Return(
        DownloadItem::IN_PROGRESS));
  EXPECT_CALL(mock(1), GetState()).WillRepeatedly(Return(
        DownloadItem::COMPLETE));
  query()->AddSorter(
      DownloadQuery::SORT_STATE, DownloadQuery::ASCENDING);
  Search();
  EXPECT_EQ(DownloadItem::IN_PROGRESS, results()->at(0)->GetState());
  EXPECT_EQ(DownloadItem::COMPLETE, results()->at(1)->GetState());
}

TEST_F(DownloadQueryTest, DownloadQuerySortTotalBytes) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetTotalBytes()).WillRepeatedly(Return(0));
  EXPECT_CALL(mock(1), GetTotalBytes()).WillRepeatedly(Return(1));
  query()->AddSorter(
      DownloadQuery::SORT_TOTAL_BYTES, DownloadQuery::DESCENDING);
  Search();
  EXPECT_EQ(1, results()->at(0)->GetTotalBytes());
  EXPECT_EQ(0, results()->at(1)->GetTotalBytes());
}

TEST_F(DownloadQueryTest, DownloadQuerySortUrl) {
  CreateMocks(2);
  GURL a_url("http://example.com/a");
  GURL b_url("http://example.com/b");
  EXPECT_CALL(mock(0), GetOriginalUrl()).WillRepeatedly(ReturnRef(b_url));
  EXPECT_CALL(mock(1), GetOriginalUrl()).WillRepeatedly(ReturnRef(a_url));
  query()->AddSorter(
      DownloadQuery::SORT_URL, DownloadQuery::ASCENDING);
  Search();
  EXPECT_EQ(a_url, results()->at(0)->GetOriginalUrl());
  EXPECT_EQ(b_url, results()->at(1)->GetOriginalUrl());
}

TEST_F(DownloadQueryTest, DownloadQuerySortId) {
  CreateMocks(2);
  EXPECT_CALL(mock(0), GetReceivedBytes()).WillRepeatedly(Return(0));
  EXPECT_CALL(mock(1), GetReceivedBytes()).WillRepeatedly(Return(0));
  EXPECT_CALL(mock(0), GetId()).WillRepeatedly(Return(1));
  EXPECT_CALL(mock(1), GetId()).WillRepeatedly(Return(0));
  query()->AddSorter(
      DownloadQuery::SORT_BYTES_RECEIVED, DownloadQuery::DESCENDING);
  Search();
  EXPECT_EQ(0, results()->at(0)->GetId());
  EXPECT_EQ(1, results()->at(1)->GetId());
}

TEST_F(DownloadQueryTest, DownloadQueryFilterPerformance) {
  static const int kNumItems = 10000;
  static const int kNumFilters = 1000;
  CreateMocks(kNumItems);
  for (size_t i = 0; i < (kNumFilters - 1); ++i) {
    query()->AddFilter(base::Bind(&AlwaysReturn, true));
  }
  query()->AddFilter(base::Bind(&AlwaysReturn, false));
  base::Time start = base::Time::Now();
  Search();
  base::Time end = base::Time::Now();
  double nanos = (end - start).InMillisecondsF() * 1000.0 * 1000.0;
  double nanos_per_item = nanos / static_cast<double>(kNumItems);
  double nanos_per_item_per_filter = nanos_per_item
    / static_cast<double>(kNumFilters);
  std::cout << "Search took " << nanos_per_item_per_filter
            << " nanoseconds per item per filter.\n";
}