// Copyright 2024 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/windows_services/service_program/test_support/scoped_log_grabber.h"

#include <windows.h>

#include <ostream>

#include "base/command_line.h"
#include "base/containers/heap_array.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/logging.h"
#include "base/strings/string_view_util.h"
#include "base/test/mock_callback.h"
#include "base/test/multiprocess_test.h"
#include "chrome/windows_services/service_program/logging_support.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

using ::testing::_;
using ::testing::DoAll;
using ::testing::HasSubstr;
using ::testing::Return;
using ::testing::SaveArg;

namespace {

MULTIPROCESS_TEST_MAIN(WriteToLog) {
  InitializeLogging(*base::CommandLine::ForCurrentProcess());

  // Emitting a log message should go to the parent.
  LOG(ERROR) << __func__;
  return ERROR_SUCCESS;
}

// Emits a multi-line log message.
MULTIPROCESS_TEST_MAIN(WriteMultiLineToLog) {
  InitializeLogging(*base::CommandLine::ForCurrentProcess());

  LOG(ERROR) << __func__ << std::endl << __func__;
  return ERROR_SUCCESS;
}

}  // namespace

// A test fixture that redirects the test process's stderr to a pipe so that
// tests can validate messages written to stderr by code under test. There is
// also a facility for launching a child proc, whereby the child proc's log
// output is sent to the test's process's stderr.
class ScopedLogGrabberTest : public testing::Test {
 protected:
  ScopedLogGrabberTest() : original_stderr_(::GetStdHandle(STD_ERROR_HANDLE)) {}
  ~ScopedLogGrabberTest() override {
    // Restore stderr for the test process.
    ::SetStdHandle(STD_ERROR_HANDLE, original_stderr_);
  }

  void SetUp() override {
    ASSERT_NE(original_stderr_, INVALID_HANDLE_VALUE);

    // Create a pipe to intercept this process's stderr.
    HANDLE read_handle = nullptr;
    HANDLE write_handle = nullptr;
    ASSERT_TRUE(::CreatePipe(&read_handle, &write_handle,
                             /*lpPipeAttributes=*/nullptr, /*nSize=*/0));
    read_pipe_ = base::File(std::exchange(read_handle, nullptr));
    write_pipe_ = base::File(std::exchange(write_handle, nullptr));
    ASSERT_TRUE(
        ::SetStdHandle(STD_ERROR_HANDLE, write_pipe_.GetPlatformFile()));
  }

  // Returns a handle to a child process that will run `function_name` (which
  // must call `InitializeLogging()`) and grab its log output via `log_grabber`.
  base::Process SpawnChild(std::string_view function_name,
                           const ScopedLogGrabber& log_grabber) {
    base::CommandLine command_line =
        base::GetMultiProcessTestChildBaseCommandLine();
    log_grabber.AddLoggingSwitches(command_line);
    base::LaunchOptions launch_options;
    launch_options.start_hidden = true;
    return base::SpawnMultiProcessTestChild(std::string(function_name),
                                            command_line, launch_options);
  }

  // Returns the read end of the pipe connected to the test process's stderr.
  base::File& read_pipe() { return read_pipe_; }

 private:
  // The process's original stderr handle for restoration at destruction.
  const HANDLE original_stderr_;

  // Handles to an anonymous pipe used to intercept this test process's stderr.
  base::File read_pipe_;
  base::File write_pipe_;
};

// Tests that log message from a child process are emitting to this process's
// stderr.
TEST_F(ScopedLogGrabberTest, TestLogging) {
  // Prepare to capture logs from the child process.
  ScopedLogGrabber log_grabber;

  // Launch the child process with the logging options.
  constexpr std::string_view kChildFunction = "WriteToLog";
  base::Process child_process = SpawnChild(kChildFunction, log_grabber);

  // Read logs from the child until it logs its name.
  auto buffer = base::HeapArray<uint8_t>::Uninit(4096);
  while (true) {
    std::optional<size_t> bytes_read =
        read_pipe().ReadAtCurrentPos(buffer.as_span());
    ASSERT_TRUE(bytes_read.has_value());
    const auto log_message = base::as_string_view(buffer.first(*bytes_read));
    if (log_message.find(kChildFunction) != std::string_view::npos) {
      break;
    }
  }

  // Wait for the child to terminate.
  int exit_code = ERROR_SUCCESS;
  ASSERT_TRUE(child_process.WaitForExit(&exit_code));
  EXPECT_EQ(exit_code, ERROR_SUCCESS);
}

// Tests that log message from a child process are passed to a callback.
TEST_F(ScopedLogGrabberTest, TestWithDelegate) {
  // Prepare to capture logs from the child process with a callback. Ignore
  base::MockCallback<ScopedLogGrabber::LogMessageCallback> mock_callback;

  ScopedLogGrabber log_grabber;
  log_grabber.SetLogMessageCallback(mock_callback.Get());

  // Ignore all messages except one expected multi-line message.
  base::ProcessId pid = base::kNullProcessId;
  EXPECT_CALL(mock_callback, Run(_, _)).WillRepeatedly(Return(true));
  EXPECT_CALL(mock_callback,
              Run(_, HasSubstr("WriteMultiLineToLog\nWriteMultiLineToLog\n")))
      .WillOnce(DoAll(SaveArg<0>(&pid), Return(true)));

  // Launch the child process with the logging options.
  constexpr std::string_view kChildFunction = "WriteMultiLineToLog";
  base::Process child_process = SpawnChild(kChildFunction, log_grabber);

  // Wait for the child to terminate.
  int exit_code = ERROR_SUCCESS;
  ASSERT_TRUE(child_process.WaitForExit(&exit_code));
  EXPECT_EQ(exit_code, ERROR_SUCCESS);

  // The callback should have been run with the child process's pid.
  EXPECT_EQ(pid, child_process.Pid());
}

TEST(ScopedLogGrabberParseTest, ProcessId) {
  // Inputs that don't match "^\[[0-9]+:.*"
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId(""), base::kNullProcessId);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("["), base::kNullProcessId);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("[1"), base::kNullProcessId);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("1:"), base::kNullProcessId);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("[a"), base::kNullProcessId);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("[:"), base::kNullProcessId);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("hi, mom"), base::kNullProcessId);

  // Inputs that do.
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("[1:"), 1U);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId("[535:"), 535U);
  EXPECT_EQ(ScopedLogGrabber::ParseProcessId(
                "[11916:15856:ERROR:service_unittest.cc(91)] blah"),
            11916U);
}
