/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "src/traced_relay/relay_service.h"

#include <memory>

#include "perfetto/ext/base/unix_socket.h"
#include "protos/perfetto/ipc/wire_protocol.gen.h"
#include "src/base/test/test_task_runner.h"
#include "src/ipc/buffered_frame_deserializer.h"
#include "test/gtest_and_gmock.h"

// Disable tests on MacOS and Windows as neither abstract sockets nor pseudo
// boot ID are supported.
#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)

namespace perfetto {
namespace {

using ::testing::_;

class TestEventListener : public base::UnixSocket::EventListener {
 public:
  MOCK_METHOD(void, OnDataAvailable, (base::UnixSocket*), (override));
  MOCK_METHOD(void, OnConnect, (base::UnixSocket*, bool), (override));
  MOCK_METHOD(void, OnNewIncomingConnection, (base::UnixSocket*));

  void OnNewIncomingConnection(
      base::UnixSocket*,
      std::unique_ptr<base::UnixSocket> new_connection) override {
    // Need to keep |new_connection| alive.
    client_connection_ = std::move(new_connection);
    OnNewIncomingConnection(client_connection_.get());
  }

 private:
  std::unique_ptr<base::UnixSocket> client_connection_;
};

// Exercises the relay service and also validates that the relay service injects
// a SetPeerIdentity message:
//
// producer (client UnixSocket) <- @producer.sock -> relay service
// <- 127.0.0.1.* -> tcp_server (listening UnixSocet).
TEST(RelayServiceTest, SetPeerIdentity) {
  base::TestTaskRunner task_runner;
  auto relay_service = std::make_unique<RelayService>(&task_runner);
  // Disable the extra socket connection created by RelayClient.
  relay_service->SetRelayClientDisabledForTesting(true);

  // Set up a server UnixSocket to find an unused TCP port.
  // The TCP connection emulates the socket to the host traced.
  TestEventListener tcp_listener;
  auto tcp_server = base::UnixSocket::Listen(
      "127.0.0.1:0", &tcp_listener, &task_runner, base::SockFamily::kInet,
      base::SockType::kStream);
  ASSERT_TRUE(tcp_server->is_listening());
  auto tcp_sock_name = tcp_server->GetSockAddr();
  auto* unix_sock_name = "@producer.sock";  // Use abstract socket for server.

  // Start the relay service.
  relay_service->Start(unix_sock_name, tcp_sock_name.c_str());

  // Emulates the producer connection.
  TestEventListener producer_listener;
  auto producer = base::UnixSocket::Connect(
      unix_sock_name, &producer_listener, &task_runner, base::SockFamily::kUnix,
      base::SockType::kStream);
  auto producer_connected = task_runner.CreateCheckpoint("producer_connected");
  EXPECT_CALL(producer_listener, OnConnect(_, _))
      .WillOnce([&](base::UnixSocket* s, bool conn) {
        EXPECT_TRUE(conn);
        EXPECT_EQ(s, producer.get());
        producer_connected();
      });
  task_runner.RunUntilCheckpoint("producer_connected");

  // Add some producer data.
  ipc::Frame test_frame;
  test_frame.add_data_for_testing("test_data");
  auto test_data = ipc::BufferedFrameDeserializer::Serialize(test_frame);
  producer->SendStr(test_data);

  base::UnixSocket* tcp_client_connection = nullptr;
  auto tcp_client_connected =
      task_runner.CreateCheckpoint("tcp_client_connected");
  EXPECT_CALL(tcp_listener, OnNewIncomingConnection(_))
      .WillOnce([&](base::UnixSocket* client) {
        tcp_client_connection = client;
        tcp_client_connected();
      });
  task_runner.RunUntilCheckpoint("tcp_client_connected");

  // Asserts that we can receive the SetPeerIdentity message.
  auto peer_identity_recv = task_runner.CreateCheckpoint("peer_identity_recv");
  ipc::BufferedFrameDeserializer deserializer;
  EXPECT_CALL(tcp_listener, OnDataAvailable(_))
      .WillRepeatedly([&](base::UnixSocket* tcp_conn) {
        auto buf = deserializer.BeginReceive();
        auto rsize = tcp_conn->Receive(buf.data, buf.size);
        EXPECT_TRUE(deserializer.EndReceive(rsize));

        auto frame = deserializer.PopNextFrame();
        EXPECT_TRUE(frame->has_set_peer_identity());

        const auto& set_peer_identity = frame->set_peer_identity();
        EXPECT_EQ(set_peer_identity.pid(), getpid());
        EXPECT_EQ(set_peer_identity.uid(), static_cast<int32_t>(geteuid()));
        EXPECT_TRUE(set_peer_identity.has_machine_id_hint());

        frame = deserializer.PopNextFrame();
        EXPECT_EQ(1u, frame->data_for_testing().size());
        EXPECT_EQ(std::string("test_data"), frame->data_for_testing()[0]);

        peer_identity_recv();
      });
  task_runner.RunUntilCheckpoint("peer_identity_recv");
}

TEST(RelayServiceTest, MachineIDHint) {
  base::TestTaskRunner task_runner;
  auto relay_service = std::make_unique<RelayService>(&task_runner);

  auto hint1 = relay_service->GetMachineIdHint();
  auto hint2 =
      relay_service->GetMachineIdHint(/*use_pseudo_boot_id_for_testing=*/true);
  EXPECT_NE(hint1, hint2);

  // Add a short sleep to verify that pseudo boot ID isn't affected.
  std::this_thread::sleep_for(std::chrono::milliseconds(1));

  relay_service = std::make_unique<RelayService>(&task_runner);
  auto hint3 = relay_service->GetMachineIdHint();
  auto hint4 =
      relay_service->GetMachineIdHint(/*use_pseudo_boot_id_for_testing=*/true);
  EXPECT_NE(hint3, hint4);

  EXPECT_FALSE(hint1.empty());
#if !PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
  // This test can run on Android kernel 3.x, but pseudo boot ID uses statx(2)
  // that requires kernel 4.11.
  EXPECT_FALSE(hint2.empty());
#endif

  EXPECT_EQ(hint1, hint3);
  EXPECT_EQ(hint2, hint4);
}

// Test that the RelayClient notifies its usr with the callback on
// connection errors.
TEST(RelayClientTest, OnErrorCallback) {
  base::TestTaskRunner task_runner;

  // Set up a server UnixSocket to find an unused TCP port.
  // The TCP connection emulates the socket to the host traced.
  TestEventListener tcp_listener;
  auto tcp_server = base::UnixSocket::Listen(
      "127.0.0.1:0", &tcp_listener, &task_runner, base::SockFamily::kInet,
      base::SockType::kStream);
  ASSERT_TRUE(tcp_server->is_listening());
  auto tcp_sock_name = tcp_server->GetSockAddr();

  auto on_relay_client_error =
      task_runner.CreateCheckpoint("on_relay_client_error");
  auto on_error_callback = [&]() { on_relay_client_error(); };
  auto relay_client = std::make_unique<RelayClient>(
      tcp_sock_name, "fake_machine_id_hint", "machine1", &task_runner,
      on_error_callback);

  base::UnixSocket* tcp_client_connection = nullptr;
  auto tcp_client_connected =
      task_runner.CreateCheckpoint("tcp_client_connected");
  EXPECT_CALL(tcp_listener, OnNewIncomingConnection(_))
      .WillOnce([&](base::UnixSocket* client) {
        tcp_client_connection = client;
        tcp_client_connected();
      });
  task_runner.RunUntilCheckpoint("tcp_client_connected");

  // Just drain the data passed over the socket.
  EXPECT_CALL(tcp_listener, OnDataAvailable(_))
      .WillRepeatedly([&](base::UnixSocket* tcp_conn) {
        ::testing::IgnoreResult(tcp_conn->ReceiveString());
      });

  EXPECT_FALSE(relay_client->clock_synced_with_service_for_testing());
  // Shutdown the connected connection. The RelayClient should notice this
  // error.
  tcp_client_connection->Shutdown(true);
  task_runner.RunUntilCheckpoint("on_relay_client_error");

  // Shutdown the server. The RelayClient should notice that the connection is
  // refused.
  tcp_server->Shutdown(true);
  on_relay_client_error =
      task_runner.CreateCheckpoint("on_relay_client_error_2");
  relay_client = std::make_unique<RelayClient>(
      tcp_sock_name, "fake_machine_id_hint", "machine1", &task_runner,
      [&]() { on_relay_client_error(); });
  task_runner.RunUntilCheckpoint("on_relay_client_error_2");
}

TEST(RelayClientTest, SetPeerIdentity) {
  base::TestTaskRunner task_runner;
  // Set up a server UnixSocket to find an unused TCP port.
  // The TCP connection emulates the socket to the host traced.
  TestEventListener tcp_listener;
  auto tcp_server = base::UnixSocket::Listen(
      "127.0.0.1:0", &tcp_listener, &task_runner, base::SockFamily::kInet,
      base::SockType::kStream);
  ASSERT_TRUE(tcp_server->is_listening());
  auto tcp_sock_name = tcp_server->GetSockAddr();
  auto on_error_callback = [&]() { FAIL() << "Should not be called"; };
  auto relay_service = std::make_unique<RelayClient>(
      tcp_sock_name, "fake_machine_id_hint", "machine1", &task_runner,
      on_error_callback);

  base::UnixSocket* tcp_client_connection = nullptr;
  auto tcp_client_connected =
      task_runner.CreateCheckpoint("tcp_client_connected");
  EXPECT_CALL(tcp_listener, OnNewIncomingConnection(_))
      .WillOnce([&](base::UnixSocket* client) {
        tcp_client_connection = client;
        tcp_client_connected();
      });
  task_runner.RunUntilCheckpoint("tcp_client_connected");

  // Asserts that we can receive the SetPeerIdentity message.
  auto peer_identity_recv = task_runner.CreateCheckpoint("peer_identity_recv");
  ipc::BufferedFrameDeserializer deserializer;
  EXPECT_CALL(tcp_listener, OnDataAvailable(_))
      .WillRepeatedly([&](base::UnixSocket* tcp_conn) {
        auto buf = deserializer.BeginReceive();
        auto rsize = tcp_conn->Receive(buf.data, buf.size);
        EXPECT_TRUE(deserializer.EndReceive(rsize));

        auto frame = deserializer.PopNextFrame();
        EXPECT_TRUE(frame->has_set_peer_identity());

        const auto& set_peer_identity = frame->set_peer_identity();
        EXPECT_EQ(set_peer_identity.pid(), getpid());
        EXPECT_EQ(set_peer_identity.uid(), static_cast<int32_t>(geteuid()));
        EXPECT_EQ(set_peer_identity.machine_id_hint(), "fake_machine_id_hint");

        peer_identity_recv();
      });
  task_runner.RunUntilCheckpoint("peer_identity_recv");
}

}  // namespace
}  // namespace perfetto

#endif
