/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "Helpers.h"
#include "base/eintr_wrapper.h"
#include "gtest/gtest.h"
#include "mozilla/AsyncPlatformPipes.h"
#include "mozilla/RandomNum.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/gtest/MozAssertions.h"
#include "nsCOMPtr.h"
#include "nsStreamUtils.h"
#include "nsThreadUtils.h"

#ifdef XP_WIN
#  include <windows.h>
#else
#  include <fcntl.h>
#  include <unistd.h>
#endif

using mozilla::PlatformPipeReader;
using mozilla::SpinEventLoopUntil;
using mozilla::UniqueFileHandle;

namespace {

struct PipePair {
  RefPtr<PlatformPipeReader> mReader;
  UniqueFileHandle mWriteHandle;
};

PipePair CreatePipePair(uint32_t aCapacity) {
#ifdef XP_WIN
  wchar_t name[128];
  swprintf(name, std::size(name),
           L"\\\\.\\pipe\\gecko-test-platform-pipe-%lu-%I64u",
           ::GetCurrentProcessId(), mozilla::RandomUint64OrDie());

  UniqueFileHandle readHandle(
      CreateNamedPipeW(name,
                       PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED |
                           FILE_FLAG_FIRST_PIPE_INSTANCE,
                       PIPE_TYPE_BYTE | PIPE_WAIT, 1, 4096, 4096, 0, nullptr));
  MOZ_RELEASE_ASSERT(readHandle);

  UniqueFileHandle writeHandle(
      CreateFileW(name, GENERIC_WRITE, 0, nullptr, OPEN_EXISTING,
                  SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS, nullptr));
  MOZ_RELEASE_ASSERT(writeHandle);

  MOZ_RELEASE_ASSERT(!ConnectNamedPipe(readHandle.get(), nullptr) &&
                     GetLastError() == ERROR_PIPE_CONNECTED);

  return {new PlatformPipeReader(std::move(readHandle), aCapacity),
          std::move(writeHandle)};
#else
  int fds[2];
  MOZ_RELEASE_ASSERT(pipe(fds) == 0);
  MOZ_RELEASE_ASSERT(
      fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | O_NONBLOCK) == 0);
  return {new PlatformPipeReader(UniqueFileHandle(fds[0]), aCapacity),
          UniqueFileHandle(fds[1])};
#endif
}

void WriteAllRaw(UniqueFileHandle& aHandle, const char* aData,
                 uint32_t aCount) {
#ifdef XP_WIN
  DWORD written = 0;
  BOOL ok = WriteFile(aHandle.get(), aData, aCount, &written, nullptr);
  MOZ_RELEASE_ASSERT(ok);
  MOZ_RELEASE_ASSERT(written == aCount);
#else
  uint32_t total = 0;
  while (total < aCount) {
    ssize_t rv =
        HANDLE_EINTR(write(aHandle.get(), aData + total, aCount - total));
    MOZ_RELEASE_ASSERT(rv > 0);
    total += static_cast<uint32_t>(rv);
  }
#endif
}

void ReadExactly(nsIInputStream* aStream, uint32_t aCount,
                 nsACString& aResult) {
  aResult.SetLength(aCount);

  uint32_t read = 0;
  ASSERT_NS_SUCCEEDED(aStream->Read(aResult.BeginWriting(), aCount, &read));
  ASSERT_EQ(read, aCount);
}

struct PartialConsumeClosure {
  uint32_t mLimit;
  char* mBuffer;
};

nsresult ConsumeUpToLimit(nsIInputStream*, void* aClosure,
                          const char* aFromSegment, uint32_t aOffset,
                          uint32_t aCount, uint32_t* aWriteCount) {
  auto* state = static_cast<PartialConsumeClosure*>(aClosure);
  uint32_t toRead = std::min(state->mLimit - aOffset, aCount);
  memcpy(state->mBuffer + aOffset, aFromSegment, toRead);
  *aWriteCount = toRead;
  return NS_OK;
}

}  // namespace

TEST(AsyncPlatformPipes, ReadAsyncWait)
{
  auto pipe = CreatePipePair(64);

  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
  RefPtr callback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(callback, 0, 0, thread));

  nsCString payload("hello async pipe"_ns);
  WriteAllRaw(pipe.mWriteHandle, payload.get(), payload.Length());

  MOZ_ALWAYS_TRUE(
      SpinEventLoopUntil("xpcom:TEST(AsyncPlatformPipes, ReadAsyncWait)"_ns,
                         [&] { return callback->Called(); }));

  nsCString output;
  ReadExactly(pipe.mReader, payload.Length(), output);
  ASSERT_TRUE(payload.Equals(output));
  pipe.mWriteHandle.reset();
  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());
}

TEST(AsyncPlatformPipes, ReadAsyncWaitClosureOnly)
{
  auto pipe = CreatePipePair(64);

  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
  RefPtr callback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(
      callback, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, thread));

  pipe.mWriteHandle.reset();

  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, ReadAsyncWaitClosureOnly)"_ns,
      [&] { return callback->Called(); }));

  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());
}

TEST(AsyncPlatformPipes, InputStreamIsBufferedDoesNotStartRead)
{
  auto pipe = CreatePipePair(8);

  nsCString payload("12345678"_ns);
  WriteAllRaw(pipe.mWriteHandle, payload.get(), payload.Length());

  uint64_t available = 1;
  ASSERT_NS_SUCCEEDED(pipe.mReader->Available(&available));
  ASSERT_EQ(available, 0u);

  ASSERT_TRUE(NS_InputStreamIsBuffered(pipe.mReader));

  ASSERT_NS_SUCCEEDED(pipe.mReader->Available(&available));
  ASSERT_EQ(available, 0u);

  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
  RefPtr readCallback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(readCallback, 0, 0, thread));
  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, InputStreamIsBufferedDoesNotStartRead, Read)"_ns,
      [&] { return readCallback->Called(); }));

  nsCString output;
  ReadExactly(pipe.mReader, payload.Length(), output);
  ASSERT_TRUE(payload.Equals(output));
  pipe.mWriteHandle.reset();
  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());
}

TEST(AsyncPlatformPipes, LargeDataMultipleCycles)
{
  // Use a small reader buffer (8 bytes) to force multiple read cycles for a
  // larger payload (1024 bytes).
  auto pipe = CreatePipePair(8);
  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();

  const uint32_t kTotalBytes = 1024;
  nsCString payload;
  payload.SetLength(kTotalBytes);
  for (uint32_t i = 0; i < kTotalBytes; ++i) {
    payload.BeginWriting()[i] = static_cast<char>(i & 0xff);
  }

  WriteAllRaw(pipe.mWriteHandle, payload.get(), payload.Length());
  pipe.mWriteHandle.reset();

  nsCString result;
  uint32_t totalRead = 0;
  while (totalRead < kTotalBytes) {
    RefPtr readCallback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
    ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(readCallback, 0, 0, thread));
    MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
        "xpcom:TEST(AsyncPlatformPipes, LargeDataMultipleCycles)"_ns,
        [&] { return readCallback->Called(); }));

    uint64_t avail = 0;
    ASSERT_NS_SUCCEEDED(pipe.mReader->Available(&avail));
    if (avail == 0) {
      break;
    }

    uint32_t prevLen = result.Length();
    result.SetLength(prevLen + static_cast<uint32_t>(avail));
    uint32_t read = 0;
    ASSERT_NS_SUCCEEDED(pipe.mReader->Read(
        result.BeginWriting() + prevLen, static_cast<uint32_t>(avail), &read));
    ASSERT_EQ(read, static_cast<uint32_t>(avail));
    totalRead += read;
  }

  ASSERT_EQ(totalRead, kTotalBytes);
  ASSERT_TRUE(payload.Equals(result));
  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());
}

TEST(AsyncPlatformPipes, ClosureOnlyCallbackIgnoresData)
{
  auto pipe = CreatePipePair(64);

  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
  RefPtr callback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(
      callback, nsIAsyncInputStream::WAIT_CLOSURE_ONLY, 0, thread));

  nsCString payload("closure ignores this"_ns);
  WriteAllRaw(pipe.mWriteHandle, payload.get(), payload.Length());

  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, ClosureOnlyCallbackIgnoresData) buffered"_ns,
      [&] {
        uint64_t avail = 0;
        return NS_SUCCEEDED(pipe.mReader->Available(&avail)) && avail > 0;
      }));

  ASSERT_FALSE(callback->Called());

  nsCString output;
  ReadExactly(pipe.mReader, payload.Length(), output);
  ASSERT_TRUE(payload.Equals(output));
  ASSERT_FALSE(callback->Called());

  pipe.mWriteHandle.reset();

  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, ClosureOnlyCallbackIgnoresData) closed"_ns,
      [&] { return callback->Called(); }));

  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());
}

TEST(AsyncPlatformPipes, ReadSegmentsPartialConsume)
{
  auto pipe = CreatePipePair(64);
  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();

  nsCString payload("abcdefghij"_ns);
  WriteAllRaw(pipe.mWriteHandle, payload.get(), payload.Length());

  RefPtr callback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(callback, 0, 0, thread));
  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, ReadSegmentsPartialConsume) wait"_ns,
      [&] { return callback->Called(); }));

  uint64_t avail = 0;
  ASSERT_NS_SUCCEEDED(pipe.mReader->Available(&avail));
  ASSERT_EQ(avail, payload.Length());

  char firstHalf[5] = {0};
  PartialConsumeClosure closure{5, firstHalf};
  uint32_t read = 0;
  ASSERT_NS_SUCCEEDED(pipe.mReader->ReadSegments(ConsumeUpToLimit, &closure,
                                                 payload.Length(), &read));
  ASSERT_EQ(read, 5u);
  ASSERT_EQ(memcmp(firstHalf, "abcde", 5), 0);

  ASSERT_NS_SUCCEEDED(pipe.mReader->Available(&avail));
  ASSERT_EQ(avail, 5u);

  nsCString rest;
  ReadExactly(pipe.mReader, 5, rest);
  ASSERT_TRUE(rest.Equals("fghij"_ns));

  pipe.mWriteHandle.reset();
  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());
}

TEST(AsyncPlatformPipes, ReadAfterWriteSideClosed)
{
  auto pipe = CreatePipePair(64);
  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();

  nsCString payload("farewell"_ns);
  WriteAllRaw(pipe.mWriteHandle, payload.get(), payload.Length());
  pipe.mWriteHandle.reset();

  RefPtr readCallback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(readCallback, 0, 0, thread));
  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, ReadAfterWriteSideClosed) data"_ns,
      [&] { return readCallback->Called(); }));

  nsCString output;
  ReadExactly(pipe.mReader, payload.Length(), output);
  ASSERT_TRUE(payload.Equals(output));

  RefPtr eofCallback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(eofCallback, 0, 0, thread));
  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, ReadAfterWriteSideClosed) eof"_ns,
      [&] { return eofCallback->Called(); }));

  ASSERT_EQ(pipe.mReader->StreamStatus(), NS_BASE_STREAM_CLOSED);

  char extra[8] = {0};
  uint32_t read = 42;
  ASSERT_NS_SUCCEEDED(pipe.mReader->Read(extra, sizeof(extra), &read));
  ASSERT_EQ(read, 0u);

  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());
}

TEST(AsyncPlatformPipes, CloseWithOutstandingIO)
{
  auto pipe = CreatePipePair(64);
  nsCOMPtr<nsIThread> thread = do_GetCurrentThread();

  RefPtr callback = mozilla::MakeRefPtr<testing::InputStreamCallback>();
  ASSERT_NS_SUCCEEDED(pipe.mReader->AsyncWait(callback, 0, 0, thread));

  ASSERT_NS_SUCCEEDED(pipe.mReader->Close());

  MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
      "xpcom:TEST(AsyncPlatformPipes, CloseWithOutstandingIO)"_ns,
      [&] { return callback->Called(); }));

  ASSERT_EQ(pipe.mReader->StreamStatus(), NS_BASE_STREAM_CLOSED);

  char buf[8] = {0};
  uint32_t read = 42;
  ASSERT_NS_SUCCEEDED(pipe.mReader->Read(buf, sizeof(buf), &read));
  ASSERT_EQ(read, 0u);

  pipe.mWriteHandle.reset();
}
