// Unit tests for the protobuf-compatible message API surface that onnx-light
// exposes on generated proto classes: ParseFromString / SerializeToString now
// return bool (true on success) and ByteSizeLong() reports the serialized size
// without performing a real serialization (it delegates to SerializeSize()).
#include "onnx.h"
#include <filesystem>
#include <fstream>
#include <sstream>
#if defined(_WIN32)
#include <fcntl.h>
#include <io.h>
#include <sys/stat.h>
#else
#include <fcntl.h>
#include <unistd.h>
#endif
#include <gtest/gtest.h>
#include <string>
#include <vector>

using namespace ONNX_LIGHT_NAMESPACE;

namespace {

// Builds a small AttributeProto carrying an int payload.
AttributeProto MakeIntAttr() {
  AttributeProto attr;
  attr.set_name("i_attr");
  attr.ref_i() = 123456789;
  return attr;
}

int OpenTempFdForWrite(const std::string &path) {
#if defined(_WIN32)
  int fd = -1;
  _sopen_s(&fd, path.c_str(), _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY, _SH_DENYNO,
           _S_IREAD | _S_IWRITE);
  return fd;
#else
  return ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
#endif
}

void CloseFd(int fd) {
#if defined(_WIN32)
  _close(fd);
#else
  ::close(fd);
#endif
}

std::string ReadWholeFile(const std::string &path) {
  std::ifstream in(path, std::ios::binary);
  return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}

} // namespace

TEST(proto_stream_api, SerializeToStringReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  std::string serialized;
  EXPECT_TRUE(attr.SerializeToString(serialized));
  EXPECT_FALSE(serialized.empty());
}

TEST(proto_stream_api, ParseFromStringReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  std::string serialized;
  ASSERT_TRUE(attr.SerializeToString(serialized));

  AttributeProto parsed;
  EXPECT_TRUE(parsed.ParseFromString(serialized));
  EXPECT_TRUE(parsed.has_i());
  EXPECT_EQ(parsed.ref_i(), 123456789);
  EXPECT_EQ(parsed.ref_name(), "i_attr");
}

TEST(proto_stream_api, SerializeToStringWithOptionsReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  SerializeOptions sopts;
  std::string serialized;
  EXPECT_TRUE(attr.SerializeToString(serialized, sopts));

  AttributeProto parsed;
  ParseOptions popts;
  EXPECT_TRUE(parsed.ParseFromString(serialized, popts));
  EXPECT_EQ(parsed.ref_i(), 123456789);
}

TEST(proto_stream_api, ByteSizeLongMatchesSerializedSize) {
  AttributeProto attr = MakeIntAttr();
  std::string serialized;
  ASSERT_TRUE(attr.SerializeToString(serialized));

  // ByteSizeLong() must equal the number of bytes an actual serialization
  // produces, and equal SerializeSize().size(), without serializing itself.
  EXPECT_EQ(attr.ByteSizeLong(), serialized.size());
  EXPECT_EQ(attr.ByteSizeLong(), static_cast<size_t>(attr.SerializeSize().size()));
}

TEST(proto_stream_api, ByteSizeLongOnModelProto) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");
  std::string serialized;
  ASSERT_TRUE(model.SerializeToString(serialized));
  EXPECT_EQ(model.ByteSizeLong(), serialized.size());
}

TEST(proto_stream_api, SerializeToFileDescriptorReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  const std::filesystem::path path =
      std::filesystem::temp_directory_path() / "onnx_light_proto_fd.bin";
  const int fd = OpenTempFdForWrite(path.string());
  ASSERT_GE(fd, 0);
  ASSERT_TRUE(attr.SerializeToFileDescriptor(fd));
  CloseFd(fd);

  AttributeProto parsed;
  ASSERT_TRUE(parsed.ParseFromString(ReadWholeFile(path.string())));
  EXPECT_EQ(parsed.ref_i(), 123456789);
  EXPECT_EQ(parsed.ref_name(), "i_attr");
  std::filesystem::remove(path);
}

TEST(proto_stream_api, SerializeToFileDescriptorWithOptionsReturnsTrue) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");
  SerializeOptions sopts;
  const std::filesystem::path path =
      std::filesystem::temp_directory_path() / "onnx_light_model_proto_fd.bin";
  const int fd = OpenTempFdForWrite(path.string());
  ASSERT_GE(fd, 0);
  ASSERT_TRUE(model.SerializeToFileDescriptor(fd, sopts));
  CloseFd(fd);

  ModelProto parsed;
  ASSERT_TRUE(parsed.ParseFromString(ReadWholeFile(path.string())));
  EXPECT_EQ(parsed.ref_ir_version(), 7);
  EXPECT_EQ(parsed.ref_producer_name(), "onnx-light");
  std::filesystem::remove(path);
}

TEST(proto_stream_api, SaveToFileDescriptorReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  const std::filesystem::path path =
      std::filesystem::temp_directory_path() / "onnx_light_save_proto_fd.bin";
  const int fd = OpenTempFdForWrite(path.string());
  ASSERT_GE(fd, 0);
  ASSERT_TRUE(attr.SaveToFileDescriptor(fd));
  CloseFd(fd);

  AttributeProto parsed;
  ASSERT_TRUE(parsed.ParseFromString(ReadWholeFile(path.string())));
  EXPECT_EQ(parsed.ref_i(), 123456789);
  EXPECT_EQ(parsed.ref_name(), "i_attr");
  std::filesystem::remove(path);
}

TEST(proto_stream_api, SaveToFileDescriptorWithOptionsReturnsTrue) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");
  SerializeOptions sopts;
  const std::filesystem::path path =
      std::filesystem::temp_directory_path() / "onnx_light_save_model_proto_fd.bin";
  const int fd = OpenTempFdForWrite(path.string());
  ASSERT_GE(fd, 0);
  ASSERT_TRUE(model.SaveToFileDescriptor(fd, sopts));
  CloseFd(fd);

  ModelProto parsed;
  ASSERT_TRUE(parsed.ParseFromString(ReadWholeFile(path.string())));
  EXPECT_EQ(parsed.ref_ir_version(), 7);
  EXPECT_EQ(parsed.ref_producer_name(), "onnx-light");
  std::filesystem::remove(path);
}

TEST(proto_stream_api, SaveToFileDescriptorMatchesSerializeToFileDescriptor) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");

  const std::filesystem::path path1 =
      std::filesystem::temp_directory_path() / "onnx_light_serialize_fd.bin";
  const int fd1 = OpenTempFdForWrite(path1.string());
  ASSERT_GE(fd1, 0);
  ASSERT_TRUE(model.SerializeToFileDescriptor(fd1));
  CloseFd(fd1);

  const std::filesystem::path path2 =
      std::filesystem::temp_directory_path() / "onnx_light_save_fd.bin";
  const int fd2 = OpenTempFdForWrite(path2.string());
  ASSERT_GE(fd2, 0);
  ASSERT_TRUE(model.SaveToFileDescriptor(fd2));
  CloseFd(fd2);

  EXPECT_EQ(ReadWholeFile(path1.string()), ReadWholeFile(path2.string()));
  std::filesystem::remove(path1);
  std::filesystem::remove(path2);
}

TEST(proto_stream_api, SerializeAsString) {
  AttributeProto attr = MakeIntAttr();
  const std::string serialized = attr.SerializeAsString();
  EXPECT_FALSE(serialized.empty());

  AttributeProto parsed;
  EXPECT_TRUE(parsed.ParseFromString(serialized));
  EXPECT_EQ(parsed.ref_i(), 123456789);
  EXPECT_EQ(parsed.ref_name(), "i_attr");
}

TEST(proto_stream_api, SerializeAsStringMatchesSerializeToString) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");

  std::string via_to_string;
  ASSERT_TRUE(model.SerializeToString(via_to_string));
  const std::string via_as_string = model.SerializeAsString();
  EXPECT_EQ(via_to_string, via_as_string);
}

TEST(proto_stream_api, SerializeToArrayReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  const size_t n = attr.ByteSizeLong();
  std::vector<uint8_t> buffer(n);
  ASSERT_TRUE(attr.SerializeToArray(buffer.data(), static_cast<int>(n)));

  AttributeProto parsed;
  ASSERT_TRUE(
      parsed.ParseFromString(std::string(reinterpret_cast<const char *>(buffer.data()), n)));
  EXPECT_EQ(parsed.ref_i(), 123456789);
  EXPECT_EQ(parsed.ref_name(), "i_attr");
}

TEST(proto_stream_api, SerializeToArrayMatchesSerializeToString) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");

  std::string via_to_string;
  ASSERT_TRUE(model.SerializeToString(via_to_string));
  const size_t n = via_to_string.size();
  std::vector<uint8_t> buffer(n);
  ASSERT_TRUE(model.SerializeToArray(buffer.data(), static_cast<int>(n)));
  EXPECT_EQ(std::string(reinterpret_cast<const char *>(buffer.data()), n), via_to_string);
}

TEST(proto_stream_api, SerializeToOstreamReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  std::ostringstream oss;
  ASSERT_TRUE(attr.SerializeToOstream(&oss));
  const std::string serialized = oss.str();
  EXPECT_FALSE(serialized.empty());

  AttributeProto parsed;
  EXPECT_TRUE(parsed.ParseFromString(serialized));
  EXPECT_EQ(parsed.ref_i(), 123456789);
  EXPECT_EQ(parsed.ref_name(), "i_attr");
}

TEST(proto_stream_api, SerializeToOstreamMatchesSerializeToString) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");

  std::string via_to_string;
  ASSERT_TRUE(model.SerializeToString(via_to_string));
  std::ostringstream oss;
  ASSERT_TRUE(model.SerializeToOstream(&oss));
  EXPECT_EQ(oss.str(), via_to_string);
}

TEST(proto_stream_api, SerializeToOStreamReturnsTrue) {
  AttributeProto attr = MakeIntAttr();
  std::ostringstream oss;
  ASSERT_TRUE(attr.SerializeToOStream(&oss));
  const std::string serialized = oss.str();
  EXPECT_FALSE(serialized.empty());

  AttributeProto parsed;
  EXPECT_TRUE(parsed.ParseFromString(serialized));
  EXPECT_EQ(parsed.ref_i(), 123456789);
  EXPECT_EQ(parsed.ref_name(), "i_attr");
}

TEST(proto_stream_api, SerializeToOStreamMatchesSerializeToOstream) {
  ModelProto model;
  model.set_ir_version(7);
  model.set_producer_name("onnx-light");

  std::ostringstream oss1;
  ASSERT_TRUE(model.SerializeToOstream(&oss1));
  std::ostringstream oss2;
  ASSERT_TRUE(model.SerializeToOStream(&oss2));
  EXPECT_EQ(oss1.str(), oss2.str());
}
