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

#include "chrome/installer/util/duplicate_tree_detector.h"

#include <windows.h>

#include <fstream>
#include <string>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_util.h"
#include "base/test/mock_log.h"
#include "base/test/test_file_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

using ::testing::_;
using ::testing::AnyNumber;
using ::testing::HasSubstr;
using ::testing::Return;

class DuplicateTreeDetectorTest : public ::testing::Test {
 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_source_dir_.CreateUniqueTempDir());
    ASSERT_TRUE(temp_dest_dir_.CreateUniqueTempDir());
  }

  // Simple function to dump some text into a new file.
  void CreateTextFile(const std::string& filename,
                      const std::wstring& contents) {
    std::wofstream file;
    file.open(filename.c_str());
    ASSERT_TRUE(file.is_open());
    file << contents;
    file.close();
  }

  // Creates a two level deep source dir with a file in each in |first_root| and
  // copy it (files properties will be identical) in |second_root|.
  void CreateTwoIdenticalHierarchies(const base::FilePath& first_root,
                                     const base::FilePath& second_root) {
    base::FilePath d1(first_root);
    d1 = d1.AppendASCII("D1");
    base::CreateDirectory(d1);
    ASSERT_TRUE(base::PathExists(d1));

    base::FilePath f1(d1);
    f1 = f1.AppendASCII("F1");
    CreateTextFile(f1.MaybeAsASCII(), text_content_1_);
    ASSERT_TRUE(base::PathExists(f1));

    base::FilePath d2(d1);
    d2 = d2.AppendASCII("D2");
    base::CreateDirectory(d2);
    ASSERT_TRUE(base::PathExists(d2));

    base::FilePath f2(d2);
    f2 = f2.AppendASCII("F2");
    CreateTextFile(f2.MaybeAsASCII(), text_content_2_);
    ASSERT_TRUE(base::PathExists(f2));

    ASSERT_TRUE(base::CopyDirectory(d1, second_root, true));
  }

  base::ScopedTempDir temp_source_dir_;
  base::ScopedTempDir temp_dest_dir_;

  static const wchar_t text_content_1_[];
  static const wchar_t text_content_2_[];
  static const wchar_t text_content_3_[];
};

const wchar_t DuplicateTreeDetectorTest::text_content_1_[] =
    L"Gooooooooooooooooooooogle";
const wchar_t DuplicateTreeDetectorTest::text_content_2_[] = L"Overwrite Me";
const wchar_t DuplicateTreeDetectorTest::text_content_3_[] =
    L"I'd rather see your watermelon and raise you ham and a half.";

}  // namespace

// Test the DuplicateTreeChecker's definition of identity on two identical
// directory structures.
TEST_F(DuplicateTreeDetectorTest, TestIdenticalDirs) {
  CreateTwoIdenticalHierarchies(temp_source_dir_.GetPath(),
                                temp_dest_dir_.GetPath());

  EXPECT_TRUE(installer::IsIdenticalFileHierarchy(temp_source_dir_.GetPath(),
                                                  temp_dest_dir_.GetPath()));
}

// Test when source entirely contains dest but contains other files as well.
// IsIdenticalTo should return false in this case.
TEST_F(DuplicateTreeDetectorTest, TestSourceContainsDest) {
  CreateTwoIdenticalHierarchies(temp_source_dir_.GetPath(),
                                temp_dest_dir_.GetPath());

  base::FilePath new_file(temp_source_dir_.GetPath());
  new_file = new_file.AppendASCII("FNew");
  CreateTextFile(new_file.MaybeAsASCII(), text_content_1_);
  ASSERT_TRUE(base::PathExists(new_file));

  EXPECT_FALSE(installer::IsIdenticalFileHierarchy(temp_source_dir_.GetPath(),
                                                   temp_dest_dir_.GetPath()));
}

// Test when dest entirely contains source but contains other files as well.
// IsIdenticalTo should return true in this case.
TEST_F(DuplicateTreeDetectorTest, TestDestContainsSource) {
  CreateTwoIdenticalHierarchies(temp_source_dir_.GetPath(),
                                temp_dest_dir_.GetPath());

  base::FilePath new_file(temp_dest_dir_.GetPath());
  new_file = new_file.AppendASCII("FNew");
  CreateTextFile(new_file.MaybeAsASCII(), text_content_1_);
  ASSERT_TRUE(base::PathExists(new_file));

  EXPECT_TRUE(installer::IsIdenticalFileHierarchy(temp_source_dir_.GetPath(),
                                                  temp_dest_dir_.GetPath()));
}

// Test when the file hierarchies are the same but one of the files is changed.
TEST_F(DuplicateTreeDetectorTest, TestIdenticalDirsDifferentFiles) {
  CreateTwoIdenticalHierarchies(temp_source_dir_.GetPath(),
                                temp_dest_dir_.GetPath());

  base::FilePath existing_file(temp_dest_dir_.GetPath());
  existing_file =
      existing_file.AppendASCII("D1").AppendASCII("D2").AppendASCII("F2");
  CreateTextFile(existing_file.MaybeAsASCII(), text_content_3_);

  EXPECT_FALSE(installer::IsIdenticalFileHierarchy(temp_source_dir_.GetPath(),
                                                   temp_dest_dir_.GetPath()));
}

// Test when both file hierarchies are empty.
TEST_F(DuplicateTreeDetectorTest, TestEmptyDirs) {
  EXPECT_TRUE(installer::IsIdenticalFileHierarchy(temp_source_dir_.GetPath(),
                                                  temp_dest_dir_.GetPath()));
}

// Test on single files.
TEST_F(DuplicateTreeDetectorTest, TestSingleFiles) {
  // Create a source file.
  base::FilePath source_file(temp_source_dir_.GetPath());
  source_file = source_file.AppendASCII("F1");
  CreateTextFile(source_file.MaybeAsASCII(), text_content_1_);

  // This file should be the same.
  base::FilePath dest_file(temp_dest_dir_.GetPath());
  dest_file = dest_file.AppendASCII("F1");
  ASSERT_TRUE(base::CopyFile(source_file, dest_file));

  // This file should be different.
  base::FilePath other_file(temp_dest_dir_.GetPath());
  other_file = other_file.AppendASCII("F2");
  CreateTextFile(other_file.MaybeAsASCII(), text_content_2_);

  EXPECT_TRUE(installer::IsIdenticalFileHierarchy(source_file, dest_file));
  EXPECT_FALSE(installer::IsIdenticalFileHierarchy(source_file, other_file));
}

// Test enumeration failure handling.
TEST_F(DuplicateTreeDetectorTest, IsIdenticalFileHierarchyEnumerationFailure) {
  CreateTwoIdenticalHierarchies(temp_source_dir_.GetPath(),
                                temp_dest_dir_.GetPath());

  // Make the source directory unreadable to trigger enumeration failure.
  base::FilePermissionRestorer restorer(temp_source_dir_.GetPath());
  ASSERT_TRUE(base::MakeFileUnreadable(temp_source_dir_.GetPath()));

  base::test::MockLog log;
  EXPECT_CALL(log, Log(_, _, _, _, _)).Times(AnyNumber());
  EXPECT_CALL(log, Log(::logging::LOGGING_ERROR, _, _, _,
                       HasSubstr("Failed to enumerate files in directory: ")))
      .WillOnce(Return(true));

  log.StartCapturingLogs();

  // The enumeration should fail, and IsIdenticalFileHierarchy should return
  // false.
  EXPECT_FALSE(installer::IsIdenticalFileHierarchy(temp_source_dir_.GetPath(),
                                                   temp_dest_dir_.GetPath()));
}

// Test GetFileInfo failure handling for source path.
TEST_F(DuplicateTreeDetectorTest,
       IsIdenticalFileHierarchySourceFileInfoFailure) {
  base::FilePath valid_path = temp_source_dir_.GetPath();
  base::FilePath non_existent_path =
      valid_path.Append(FILE_PATH_LITERAL("non_existent_file_or_dir"));

  base::test::MockLog log;
  EXPECT_CALL(log, Log(_, _, _, _, _)).Times(AnyNumber());
  EXPECT_CALL(log, Log(::logging::LOGGING_ERROR, _, _, _,
                       HasSubstr("Failed to get file info for source path: ")))
      .WillOnce(Return(true));

  log.StartCapturingLogs();

  EXPECT_FALSE(
      installer::IsIdenticalFileHierarchy(non_existent_path, valid_path));
}

// Test that a non-existent destination path returns false without logging an
// error.
TEST_F(DuplicateTreeDetectorTest, IsIdenticalFileHierarchyDestFileInfoFailure) {
  base::FilePath valid_path = temp_source_dir_.GetPath();
  base::FilePath non_existent_path =
      valid_path.Append(FILE_PATH_LITERAL("non_existent_file_or_dir"));

  base::test::MockLog log;
  EXPECT_CALL(log, Log(_, _, _, _, _)).Times(0);

  log.StartCapturingLogs();

  EXPECT_FALSE(
      installer::IsIdenticalFileHierarchy(valid_path, non_existent_path));
}
