//===-- unittests/Runtime/Namelist.cpp --------------------------*- C++ -*-===//
//
// 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 "flang-rt/runtime/namelist.h"
#include "CrashHandlerFixture.h"
#include "tools.h"
#include "flang-rt/runtime/descriptor.h"
#include "flang/Runtime/io-api.h"
#include <algorithm>
#include <cinttypes>
#include <complex>
#include <cstring>
#include <gtest/gtest.h>
#include <limits>
#include <string>
#include <vector>

using namespace Fortran::runtime;
using namespace Fortran::runtime::io;

struct NamelistTests : CrashHandlerFixture {};

static void ClearDescriptorStorage(const Descriptor &descriptor) {
  std::memset(descriptor.raw().base_addr, 0,
      descriptor.Elements() * descriptor.ElementBytes());
}

TEST(NamelistTests, BasicSanity) {
  static constexpr int numLines{12};
  static constexpr int lineLength{32};
  static char buffer[numLines][lineLength];
  StaticDescriptor<1, true> statDescs[1];
  Descriptor &internalDesc{statDescs[0].descriptor()};
  SubscriptValue extent[]{numLines};
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/lineLength,
      &buffer, 1, extent, CFI_attribute_pointer);
  // Set up data arrays
  std::vector<int> ints;
  for (int j{0}; j < 20; ++j) {
    ints.push_back(j % 2 == 0 ? (1 << j) : -(1 << j));
  }
  std::vector<double> reals{0.0, -0.0, std::numeric_limits<double>::infinity(),
      -std::numeric_limits<double>::infinity(),
      std::numeric_limits<double>::quiet_NaN(),
      std::numeric_limits<double>::max(), std::numeric_limits<double>::lowest(),
      std::numeric_limits<double>::epsilon()};
  std::vector<std::uint8_t> logicals;
  logicals.push_back(false);
  logicals.push_back(true);
  logicals.push_back(false);
  std::vector<std::complex<float>> complexes;
  complexes.push_back(std::complex<float>{123.0, -0.5});
  std::vector<std::string> characters;
  characters.emplace_back("aBcDeFgHiJkLmNoPqRsTuVwXyZ");
  characters.emplace_back("0123456789'\"..............");
  // Copy the data into new descriptors
  OwningPtr<Descriptor> intDesc{
      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
          std::vector<int>{5, 4}, std::move(ints))};
  OwningPtr<Descriptor> realDesc{
      MakeArray<TypeCategory::Real, static_cast<int>(sizeof(double))>(
          std::vector<int>{4, 2}, std::move(reals))};
  OwningPtr<Descriptor> logicalDesc{
      MakeArray<TypeCategory::Logical, static_cast<int>(sizeof(std::uint8_t))>(
          std::vector<int>{3}, std::move(logicals))};
  OwningPtr<Descriptor> complexDesc{
      MakeArray<TypeCategory::Complex, static_cast<int>(sizeof(float))>(
          std::vector<int>{}, std::move(complexes))};
  OwningPtr<Descriptor> characterDesc{MakeArray<TypeCategory::Character, 1>(
      std::vector<int>{2}, std::move(characters), characters[0].size())};
  // Create a NAMELIST group
  static constexpr int items{5};
  const NamelistGroup::Item itemArray[items]{{"ints", *intDesc},
      {"reals", *realDesc}, {"logicals", *logicalDesc},
      {"complexes", *complexDesc}, {"characters", *characterDesc}};
  const NamelistGroup group{"group1", items, itemArray};
  // Do an internal NAMELIST write and check results
  auto outCookie1{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(SetDelim)(outCookie1, "APOSTROPHE", 10));
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie1, group));
  auto outStatus1{IONAME(EndIoStatement)(outCookie1)};
  ASSERT_EQ(outStatus1, 0) << "Failed namelist output sanity, status "
                           << static_cast<int>(outStatus1);

  static const std::string expect{" &GROUP1 INTS= 1 -2 4 -8 16 -32 "
                                  " 64 -128 256 -512 1024 -2048    "
                                  " 4096 -8192 16384 -32768 65536  "
                                  " -131072 262144 -524288,REALS=  "
                                  " 0. -0. Inf -Inf NaN            "
                                  " 1.7976931348623157E+308        "
                                  " -1.7976931348623157E+308       "
                                  " 2.220446049250313E-16,LOGICALS="
                                  "F T F,COMPLEXES= (123.,-.5),    "
                                  " CHARACTERS= 'aBcDeFgHiJkLmNoPqR"
                                  "sTuVwXyZ' '0123456789''\"........"
                                  "......'/                        "};
  std::string got{buffer[0], sizeof buffer};
  EXPECT_EQ(got, expect);

  // Clear the arrays, read them back, write out again, and compare
  ClearDescriptorStorage(*intDesc);
  ClearDescriptorStorage(*realDesc);
  ClearDescriptorStorage(*logicalDesc);
  ClearDescriptorStorage(*complexDesc);
  ClearDescriptorStorage(*characterDesc);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  auto inStatus{IONAME(EndIoStatement)(inCookie)};
  ASSERT_EQ(inStatus, 0) << "Failed namelist input sanity, status "
                         << static_cast<int>(inStatus);
  auto outCookie2{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(SetDelim)(outCookie2, "APOSTROPHE", 10));
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie2, group));
  auto outStatus2{IONAME(EndIoStatement)(outCookie2)};
  ASSERT_EQ(outStatus2, 0) << "Failed namelist output sanity rewrite, status "
                           << static_cast<int>(outStatus2);
  std::string got2{buffer[0], sizeof buffer};
  EXPECT_EQ(got2, expect);
}

TEST(NamelistTests, Subscripts) {
  // INTEGER :: A(-1:0, -1:1)
  OwningPtr<Descriptor> aDesc{
      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
          std::vector<int>{2, 3}, std::vector<int>(6, 0))};
  aDesc->GetDimension(0).SetBounds(-1, 0);
  aDesc->GetDimension(1).SetBounds(-1, 1);
  const NamelistGroup::Item items[]{{"a", *aDesc}};
  const NamelistGroup group{"justa", 1, items};
  static char t1[]{"&justa A(0,+1:-1:-2)=1 2/"};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  internalDesc.Establish(TypeCode{CFI_type_char},
      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  auto inStatus{IONAME(EndIoStatement)(inCookie)};
  ASSERT_EQ(inStatus, 0) << "Failed namelist input subscripts, status "
                         << static_cast<int>(inStatus);
  char out[40];
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
      out, 0, nullptr, CFI_attribute_pointer);
  auto outCookie{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
  auto outStatus{IONAME(EndIoStatement)(outCookie)};
  ASSERT_EQ(outStatus, 0)
      << "Failed namelist output subscripts rewrite, status "
      << static_cast<int>(outStatus);
  std::string got{out, sizeof out};
  static const std::string expect{" &JUSTA A= 0 2 0 0 0 1/                 "};
  EXPECT_EQ(got, expect);
}

TEST(NamelistTests, ShortArrayInput) {
  OwningPtr<Descriptor> aDesc{
      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
          std::vector<int>{2}, std::vector<int>(2, -1))};
  OwningPtr<Descriptor> bDesc{
      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
          std::vector<int>{2}, std::vector<int>(2, -2))};
  const NamelistGroup::Item items[]{{"a", *aDesc}, {"b", *bDesc}};
  const NamelistGroup group{"nl", 2, items};
  // Two 12-character lines of internal input
  static char t1[]{"&nl a = 1 b "
                   " = 2 /      "};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  SubscriptValue shape{2};
  internalDesc.Establish(1, 12, t1, 1, &shape, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  auto inStatus{IONAME(EndIoStatement)(inCookie)};
  ASSERT_EQ(inStatus, 0) << "Failed namelist input subscripts, status "
                         << static_cast<int>(inStatus);
  EXPECT_EQ(*aDesc->ZeroBasedIndexedElement<int>(0), 1);
  EXPECT_EQ(*aDesc->ZeroBasedIndexedElement<int>(1), -1);
  EXPECT_EQ(*bDesc->ZeroBasedIndexedElement<int>(0), 2);
  EXPECT_EQ(*bDesc->ZeroBasedIndexedElement<int>(1), -2);
}

TEST(NamelistTests, ScalarSubstring) {
  OwningPtr<Descriptor> scDesc{MakeArray<TypeCategory::Character, 1>(
      std::vector<int>{}, std::vector<std::string>{"abcdefgh"}, 8)};
  const NamelistGroup::Item items[]{{"a", *scDesc}};
  const NamelistGroup group{"justa", 1, items};
  static char t1[]{"&justa A(2:5)='BCDE'/"};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  internalDesc.Establish(TypeCode{CFI_type_char},
      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
      << "namelist scalar substring input";
  char out[32];
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
      out, 0, nullptr, CFI_attribute_pointer);
  auto outCookie{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(SetDelim)(outCookie, "apostrophe", 10));
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
  std::string got{out, sizeof out};
  static const std::string expect{" &JUSTA A= 'aBCDEfgh'/          "};
  EXPECT_EQ(got, expect);
}

TEST(NamelistTests, ArraySubstring) {
  OwningPtr<Descriptor> scDesc{
      MakeArray<TypeCategory::Character, 1>(std::vector<int>{2},
          std::vector<std::string>{"abcdefgh", "ijklmnop"}, 8)};
  const NamelistGroup::Item items[]{{"a", *scDesc}};
  const NamelistGroup group{"justa", 1, items};
  static char t1[]{"&justa A(:)(2:+5)='BCDE' 'JKLM'/"};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  internalDesc.Establish(TypeCode{CFI_type_char},
      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
      << "namelist scalar substring input";
  char out[40];
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
      out, 0, nullptr, CFI_attribute_pointer);
  auto outCookie{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(SetDelim)(outCookie, "apostrophe", 10));
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
  std::string got{out, sizeof out};
  static const std::string expect{" &JUSTA A= 'aBCDEfgh' 'iJKLMnop'/       "};
  EXPECT_EQ(got, expect);
}

TEST(NamelistTests, Skip) {
  OwningPtr<Descriptor> scDesc{
      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
          std::vector<int>{}, std::vector<int>{-1})};
  const NamelistGroup::Item items[]{{"j", *scDesc}};
  const NamelistGroup group{"nml", 1, items};
  static char t1[]{"&skip a='str''ing'/&nml j=123/"};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  internalDesc.Establish(TypeCode{CFI_type_char},
      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
      << "namelist input with skipping";
  char out[20];
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
      out, 0, nullptr, CFI_attribute_pointer);
  auto outCookie{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
  std::string got{out, sizeof out};
  static const std::string expect{" &NML J= 123/       "};
  EXPECT_EQ(got, expect);
}

// Tests DECIMAL=COMMA mode
TEST(NamelistTests, Comma) {
  OwningPtr<Descriptor> scDesc{
      MakeArray<TypeCategory::Complex, static_cast<int>(sizeof(float))>(
          std::vector<int>{2}, std::vector<std::complex<float>>{{}, {}})};
  const NamelistGroup::Item items[]{{"z", *scDesc}};
  const NamelistGroup group{"nml", 1, items};
  static char t1[]{"&nml z=(-1,0;2,0);(-3,0;0,5)/"};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  internalDesc.Establish(TypeCode{CFI_type_char},
      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(SetDecimal)(inCookie, "COMMA", 5));
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
      << "namelist input with skipping";
  char out[30];
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
      out, 0, nullptr, CFI_attribute_pointer);
  auto outCookie{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(SetDecimal)(outCookie, "COMMA", 5));
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
  std::string got{out, sizeof out};
  static const std::string expect{" &NML Z= (-1,;2,) (-3,;,5)/   "};
  EXPECT_EQ(got, expect);
}

// Tests REAL-looking input to integers
TEST(NamelistTests, RealValueForInt) {
  OwningPtr<Descriptor> scDesc{
      MakeArray<TypeCategory::Integer, static_cast<int>(sizeof(int))>(
          std::vector<int>{}, std::vector<int>{{}})};
  const NamelistGroup::Item items[]{{"j", *scDesc}};
  const NamelistGroup group{"nml", 1, items};
  static char t1[]{"&nml j=123.456/"};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  internalDesc.Establish(TypeCode{CFI_type_char},
      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
      << "namelist real input for integer";
  char out[16];
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
      out, 0, nullptr, CFI_attribute_pointer);
  auto outCookie{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
  std::string got{out, sizeof out};
  static const std::string expect{" &NML J= 123/   "};
  EXPECT_EQ(got, expect);
}

TEST(NamelistTests, NanInputAmbiguity) {
  OwningPtr<Descriptor> xDesc{// real :: x(5) = 0.
      MakeArray<TypeCategory::Real, static_cast<int>(sizeof(float))>(
          std::vector<int>{5}, std::vector<float>{{0, 0, 0, 0, 0}})};
  OwningPtr<Descriptor> nanDesc{// real :: nan(2) = 0.
      MakeArray<TypeCategory::Real, static_cast<int>(sizeof(float))>(
          std::vector<int>{2}, std::vector<float>{{0, 0}})};
  const NamelistGroup::Item items[]{{"x", *xDesc}, {"nan", *nanDesc}};
  const NamelistGroup group{"nml", 2, items};
  static char t1[]{"&nml x=1 2 nan(q) 4 nan(1)=5 nan(q)/"};
  StaticDescriptor<1, true> statDesc;
  Descriptor &internalDesc{statDesc.descriptor()};
  internalDesc.Establish(TypeCode{CFI_type_char},
      /*elementBytes=*/std::strlen(t1), t1, 0, nullptr, CFI_attribute_pointer);
  auto inCookie{IONAME(BeginInternalArrayListInput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(InputNamelist)(inCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(inCookie), IostatOk)
      << "namelist real input for nans";
  char out[40];
  internalDesc.Establish(TypeCode{CFI_type_char}, /*elementBytes=*/sizeof out,
      out, 0, nullptr, CFI_attribute_pointer);
  auto outCookie{IONAME(BeginInternalArrayListOutput)(
      internalDesc, nullptr, 0, __FILE__, __LINE__)};
  ASSERT_TRUE(IONAME(OutputNamelist)(outCookie, group));
  ASSERT_EQ(IONAME(EndIoStatement)(outCookie), IostatOk) << "namelist output";
  std::string got{out, sizeof out};
  static const std::string expect{" &NML X= 1. 2. NaN 4. 0.,NAN= 5. NaN/   "};
  EXPECT_EQ(got, expect);
}

// TODO: Internal NAMELIST error tests
