// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/disk_cache/sql/sql_persistent_store_queries.h"

#include <cstdint>
#include <memory>
#include <string_view>

#include "base/containers/fixed_flat_map.h"
#include "base/containers/fixed_flat_set.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "net/base/cache_type.h"
#include "net/base/features.h"
#include "net/disk_cache/sql/sql_async_task_manager.h"
#include "net/disk_cache/sql/sql_backend_constants.h"
#include "net/disk_cache/sql/sql_persistent_store.h"
#include "net/disk_cache/sql/test_util.h"
#include "sql/database.h"
#include "sql/statement.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace disk_cache_sql_queries {

// Defines the set of queries that are used for schema and index creation. These
// queries are executed during database initialization and do not operate on
// existing data, so their performance is not as critical as data manipulation
// queries.
constexpr auto kSchemaAndIndexQueries = base::MakeFixedFlatSet<Query>({
    Query::kInitSchema_CreateTableResources,
    Query::kInitSchema_CreateTableBlobs,
    Query::kIndex_ResourcesCacheKeyHashDoomed,
    Query::kIndex_LiveResourcesLastUsed,
    Query::kIndex_LiveResourcesHints,
    Query::kIndex_BlobsResIdStart,
});

class SqlPersistentStoreQueriesTest : public testing::TestWithParam<bool> {
 protected:
  void SetUp() override {
    if (GetParam()) {
      feature_list_.InitAndEnableFeature(
          net::features::kRendererAccessibleHttpCache);
    } else {
      feature_list_.InitAndDisableFeature(
          net::features::kRendererAccessibleHttpCache);
    }
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  }

  // Creates a database file with the correct schema in the temporary directory.
  // This is done by instantiating and initializing a SqlPersistentStore, which
  // handles schema creation, and then closing it to ensure all changes are
  // flushed to disk.
  void CreateDatabaseInTemDir() {
    const base::FilePath path = temp_dir_.GetPath();
    const int64_t kDefaultMaxBytes = 10 * 1024 * 1024;
    auto background_task_runner =
        base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()});

    // Create and initialize a store to create the DB file with schema.
    auto store = std::make_unique<disk_cache::SqlPersistentStore>(
        path, kDefaultMaxBytes, net::CacheType::DISK_CACHE,
        std::vector<scoped_refptr<base::SequencedTaskRunner>>(
            {background_task_runner}),
        async_task_manager_, /*cleanup_tracker=*/nullptr);

    base::test::TestFuture<disk_cache::SqlPersistentStore::Error> future;
    store->Initialize(future.GetCallback());
    ASSERT_EQ(future.Get(), disk_cache::SqlPersistentStore::Error::kOk);

    // Close the store by destroying it. This ensures all data is flushed.
    store.reset();
    // Wait for background tasks to finish by posting a task to the runner and
    // waiting for it to execute.
    base::RunLoop run_loop;
    background_task_runner->PostTask(FROM_HERE, run_loop.QuitClosure());
    run_loop.Run();
  }

  // Executes `EXPLAIN QUERY PLAN` for a given SQL query using the
  // `sqlite_dev_shell` tool. This allows verifying that the query optimizer is
  // using the expected indexes, which is critical for performance.
  std::string GetQueryPlan(base::cstring_view query) {
    return disk_cache::test::GetQueryPlan(
        temp_dir_.GetPath().Append(
            disk_cache::kSqlBackendDatabaseShard0FileName),
        query);
  }

  base::ScopedTempDir temp_dir_;

 private:
  base::test::ScopedFeatureList feature_list_;
  base::test::TaskEnvironment task_environment_;
  disk_cache::SqlAsyncTaskManager async_task_manager_;
};

// This test verifies that critical SQL queries use the intended indexes by
// checking their query plans. This is essential for ensuring the performance of
// database operations. A query that performs a full table scan instead of using
// an index can lead to significant performance degradation.
TEST_P(SqlPersistentStoreQueriesTest, AllQueriesHaveValidPlan) {
  CreateDatabaseInTemDir();

  // Defines the expected query plan for each query. An empty plan indicates
  // that no specific plan is expected (e.g., for INSERT statements), or that
  // the query is a schema-related query that cannot be explained.
  constexpr auto kAllQueriesAndPlans =
      base::MakeFixedFlatMap<Query, std::string_view>(
          {{Query::kOpenEntry_SelectLiveResources,
            "`--SEARCH resources USING "
            "INDEX index_resources_cache_key_hash_doomed "
            "(cache_key_hash=? AND doomed=?)"},
           {Query::kCreateEntry_InsertIntoResources, ""},
           {Query::kDoomEntry_MarkDoomedResources,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kDeleteDoomedEntry_DeleteFromResources,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kDeleteLiveEntry_DeleteFromResources,
            "`--SEARCH resources USING "
            "INDEX index_resources_cache_key_hash_doomed "
            "(cache_key_hash=? AND doomed=?)"},
           {Query::kDeleteAllEntries_DeleteFromResources, ""},
           {Query::kDeleteAllEntries_DeleteFromBlobs, ""},
           {Query::kDeleteLiveEntriesBetween_SelectLiveResources,
            "`--SEARCH resources USING "
            "COVERING INDEX index_live_resources_last_used_bytes_usage "
            "(last_used>? AND last_used<?)"},
           {Query::kDeleteResourceByResIds_DeleteFromResources,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kDeleteResourceByResIdReturnHash,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kDeleteLiveResourceByResIdReturnUsageAndHash,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateEntryLastUsedByKey_UpdateResourceLastUsed,
            "`--SEARCH resources USING "
            "INDEX index_resources_cache_key_hash_doomed "
            "(cache_key_hash=? AND doomed=?)"},
           {Query::kInsertIntoResources, ""},
           {Query::kUpdateLastUsed,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateLastUsedHeader,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateLastUsedHeaderHints,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateLastUsedHints,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateLastUsedBody,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateLastUsedBodyHints,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateLastUsedBodyHeader,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kUpdateLastUsedBodyHeaderHints,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kWriteEntryData_UpdateResource,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kMoveBlobsToSharedCache_UpdateResource,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kTrimOverlappingBlobs_DeleteContained,
            "`--SEARCH blobs USING "
            "INDEX index_blobs_res_id_start "
            "(res_id=? AND start>?)"},
           {Query::kTrimOverlappingBlobs_SelectOverlapping,
            "`--SEARCH blobs USING "
            "INDEX index_blobs_res_id_start "
            "(res_id=? AND start<?)"},
           {Query::kTruncateBlobsAfter_DeleteAfter,
            "`--SEARCH blobs USING "
            "COVERING INDEX index_blobs_res_id_start "
            "(res_id=? AND start>?)"},
           {Query::kInsertNewBlob_InsertIntoBlobs, ""},
           {Query::kDeleteBlobById_DeleteFromBlobs,
            "`--SEARCH blobs USING "
            "INTEGER PRIMARY KEY (rowid=?)"},
           {Query::kDeleteBlobsByResId_DeleteFromBlobs,
            "`--SEARCH blobs USING "
            "INDEX index_blobs_res_id_start "
            "(res_id=?)"},
           {Query::kReadEntryData_SelectOverlapping,
            "`--SEARCH blobs USING "
            "INDEX index_blobs_res_id_start "
            "(res_id=? AND start<?)"},
           {Query::kGetEntryAvailableRange_SelectOverlapping,
            "`--SEARCH blobs USING "
            "INDEX index_blobs_res_id_start "
            "(res_id=? AND start<?)"},
           {Query::kCalculateSizeOfEntriesBetween_SelectLiveResources,
            "`--SEARCH resources USING "
            "COVERING INDEX index_live_resources_last_used_bytes_usage "
            "(last_used>? AND last_used<?)"},
           {Query::kOpenNextEntry_SelectLiveResources,
            "`--SEARCH resources USING "
            "INTEGER PRIMARY KEY (rowid<?)"},
           {Query::kStartEviction_SelectLiveResources,
            "`--SCAN resources USING "
            "COVERING INDEX index_live_resources_last_used_bytes_usage"},
           {Query::kCalculateResourceEntryCount_SelectCountFromLiveResources,
            "`--SCAN resources USING "
            "COVERING INDEX index_live_resources_last_used_bytes_usage"},
           {Query::kCalculateTotalSize_SelectTotalSizeFromLiveResources,
            "`--SCAN resources USING "
            "COVERING INDEX index_live_resources_last_used_bytes_usage"},
           {Query::kLoadInMemoryIndex_SelectCacheKeyHashFromLiveResources,
            "`--SCAN resources USING COVERING INDEX "
            "index_resources_cache_key_hash_doomed"},
           {Query::kLoadInMemoryIndex_SelectHintsFromLiveResources,
            "`--SCAN resources USING COVERING INDEX "
            "index_live_resources_hints"}});
  static_assert(kAllQueriesAndPlans.size() + kSchemaAndIndexQueries.size() ==
                static_cast<int>(Query::kMaxValue) + 1);
  for (const auto& it : kAllQueriesAndPlans) {
    if (!GetParam() &&
        it.first == Query::kMoveBlobsToSharedCache_UpdateResource) {
      continue;
    }
    const base::cstring_view query_string = GetQuery(it.first, GetParam());
    SCOPED_TRACE(query_string);
    EXPECT_EQ(GetQueryPlan(query_string), it.second);
  }
}

INSTANTIATE_TEST_SUITE_P(All, SqlPersistentStoreQueriesTest, testing::Bool());

}  // namespace disk_cache_sql_queries
