//===-- Unittests for getopt ----------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "src/unistd/getopt.h"
#include "test/UnitTest/Test.h"

#include "src/__support/CPP/array.h"
#include "src/stdio/fflush.h"
#include "src/stdio/fopencookie.h"

using LIBC_NAMESPACE::cpp::array;

namespace test_globals {
char *optarg;
int optind = 1;
int optopt;
int opterr = 1;

unsigned optpos;
} // namespace test_globals

// This can't be a constructor because it will get run before the constructor
// which sets the default state in getopt.
void set_state(FILE *errstream) {
  LIBC_NAMESPACE::impl::set_getopt_state(
      &test_globals::optarg, &test_globals::optind, &test_globals::optopt,
      &test_globals::optpos, &test_globals::opterr, errstream);
}

static void my_memcpy(char *dest, const char *src, size_t size) {
  for (size_t i = 0; i < size; i++)
    dest[i] = src[i];
}

ssize_t cookie_write(void *cookie, const char *buf, size_t size) {
  char **pos = static_cast<char **>(cookie);
  my_memcpy(*pos, buf, size);
  *pos += size;
  return size;
}

static cookie_io_functions_t cookie{nullptr, &cookie_write, nullptr, nullptr};

// TODO: <stdio> could be either llvm-libc's or the system libc's. The former
// doesn't currently support fmemopen but does have fopencookie. In the future
// just use that instead. This memopen does no error checking for the size
// of the buffer, etc.
FILE *memopen(char **pos) {
  return LIBC_NAMESPACE::fopencookie(pos, "w", cookie);
}

struct LlvmLibcGetoptTest : public LIBC_NAMESPACE::testing::Test {
  FILE *errstream;
  char buf[256];
  char *pos = buf;

  void reset_errstream() { pos = buf; }
  const char *get_error_msg() {
    LIBC_NAMESPACE::fflush(errstream);
    return buf;
  }

  void SetUp() override {
    ASSERT_TRUE(!!(errstream = memopen(&pos)));
    set_state(errstream);
    ASSERT_EQ(test_globals::optind, 1);
  }

  void TearDown() override {
    test_globals::optind = 1;
    test_globals::opterr = 1;
  }
};

// This is safe because getopt doesn't currently permute argv like GNU's getopt
// does so this just helps silence warnings.
char *operator""_c(const char *c, size_t) { return const_cast<char *>(c); }

TEST_F(LlvmLibcGetoptTest, NoMatch) {
  array<char *, 3> argv{"prog"_c, "arg1"_c, nullptr};

  // optind >= argc
  EXPECT_EQ(LIBC_NAMESPACE::getopt(1, argv.data(), "..."), -1);

  // argv[optind] == nullptr
  test_globals::optind = 2;
  EXPECT_EQ(LIBC_NAMESPACE::getopt(100, argv.data(), "..."), -1);

  // argv[optind][0] != '-'
  test_globals::optind = 1;
  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "a"), -1);
  ASSERT_EQ(test_globals::optind, 1);

  // argv[optind] == "-"
  argv[1] = "-"_c;
  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "a"), -1);
  ASSERT_EQ(test_globals::optind, 1);

  // argv[optind] == "--", then return -1 and incremement optind
  argv[1] = "--"_c;
  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "a"), -1);
  EXPECT_EQ(test_globals::optind, 2);
}

TEST_F(LlvmLibcGetoptTest, WrongMatch) {
  array<char *, 3> argv{"prog"_c, "-b"_c, nullptr};

  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "a"), int('?'));
  EXPECT_EQ(test_globals::optopt, (int)'b');
  EXPECT_EQ(test_globals::optind, 1);
  EXPECT_STREQ(get_error_msg(), "prog: illegal option -- b\n");
}

TEST_F(LlvmLibcGetoptTest, OpterrFalse) {
  array<char *, 3> argv{"prog"_c, "-b"_c, nullptr};

  test_globals::opterr = 0;
  set_state(errstream);
  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "a"), int('?'));
  EXPECT_EQ(test_globals::optopt, (int)'b');
  EXPECT_EQ(test_globals::optind, 1);
  EXPECT_STREQ(get_error_msg(), "");
}

TEST_F(LlvmLibcGetoptTest, MissingArg) {
  array<char *, 3> argv{"prog"_c, "-b"_c, nullptr};

  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), ":b:"), (int)':');
  ASSERT_EQ(test_globals::optind, 1);
  EXPECT_STREQ(get_error_msg(), "prog: option requires an argument -- b\n");
  reset_errstream();
  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "b:"), int('?'));
  EXPECT_EQ(test_globals::optind, 1);
  EXPECT_STREQ(get_error_msg(), "prog: option requires an argument -- b\n");
}

TEST_F(LlvmLibcGetoptTest, ParseArgInCurrent) {
  array<char *, 3> argv{"prog"_c, "-barg"_c, nullptr};

  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "b:"), (int)'b');
  EXPECT_STREQ(test_globals::optarg, "arg");
  EXPECT_EQ(test_globals::optind, 2);
}

TEST_F(LlvmLibcGetoptTest, ParseArgInNext) {
  array<char *, 4> argv{"prog"_c, "-b"_c, "arg"_c, nullptr};

  EXPECT_EQ(LIBC_NAMESPACE::getopt(3, argv.data(), "b:"), (int)'b');
  EXPECT_STREQ(test_globals::optarg, "arg");
  EXPECT_EQ(test_globals::optind, 3);
}

TEST_F(LlvmLibcGetoptTest, ParseMultiInOne) {
  array<char *, 3> argv{"prog"_c, "-abc"_c, nullptr};

  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "abc"), (int)'a');
  ASSERT_EQ(test_globals::optind, 1);
  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "abc"), (int)'b');
  ASSERT_EQ(test_globals::optind, 1);
  EXPECT_EQ(LIBC_NAMESPACE::getopt(2, argv.data(), "abc"), (int)'c');
  EXPECT_EQ(test_globals::optind, 2);
}
