// Copyright (c) 2020-now by the Zeek Project. See LICENSE for details.

#include <doctest/doctest.h>

#include <hilti/rt/type-info.h>

using namespace hilti::rt;
using namespace hilti::rt::string::literals;

TEST_SUITE_BEGIN("TypeInfo");

/* HILTI code to generate the type information used in this test:

module Test {

type X = struct {
    int<32> i;
    string s;
    Y y;
};

type Y = struct {
    bool b;
    real r;
};

*/

// Copied from output of hiltic.
namespace HILTI_INTERNAL_NS::type_info {
namespace {
extern const hilti::rt::TypeInfo __ti_Test_X;
extern const hilti::rt::TypeInfo __ti_Test_Y;
} // namespace
} // namespace HILTI_INTERNAL_NS::type_info

namespace {
namespace Test {

// Reduced declaration of the struct types, trusting that ours will match the
// layout coming out of HILTI ...
struct Y {
    hilti::rt::Bool b;
    double r;
};

struct X {
    hilti::rt::integer::safe<int32_t> i;
    std::string s;
    Y y;
};
} // namespace Test
} // namespace

// Copied from output of hiltic.
namespace HILTI_INTERNAL_NS::type_info {
namespace {
const hilti::rt::TypeInfo __ti_Test_X =
    {"Test::X",
     "Test::X",
     nullptr,
     new hilti::rt::type_info::Struct(std::vector<hilti::rt::type_info::struct_::Field>(
         {hilti::rt::type_info::struct_::
              Field{"i", &hilti::rt::type_info::int32, offsetof(Test::X, i), false, false, true},
          hilti::rt::type_info::struct_::
              Field{"s", &hilti::rt::type_info::string, offsetof(Test::X, s), false, false, true},
          hilti::rt::type_info::struct_::Field{"y", &__ti_Test_Y, offsetof(Test::X, y), false, false, true}}))};
const hilti::rt::TypeInfo __ti_Test_Y = {"Test::Y",
                                         "Test::Y",
                                         nullptr,
                                         new hilti::rt::type_info::Struct(
                                             std::vector<hilti::rt::type_info::struct_::Field>(
                                                 {hilti::rt::type_info::struct_::Field{"b",
                                                                                       &hilti::rt::type_info::bool_,
                                                                                       offsetof(Test::Y, b),
                                                                                       false,
                                                                                       false,
                                                                                       true},
                                                  hilti::rt::type_info::struct_::Field{"r",
                                                                                       &hilti::rt::type_info::real,
                                                                                       offsetof(Test::Y, r),
                                                                                       false,
                                                                                       false,
                                                                                       true}}))};
} // namespace
} // namespace HILTI_INTERNAL_NS::type_info

TEST_CASE("traverse structs") {
    // Check that we can traverse the structs and get expected values.

    auto sx = StrongReference<Test::X>({.i = 42, .s = "foo", .y = Test::Y{.b = true, .r = 3.14}});
    auto p = hilti::rt::type_info::value::Parent(sx);
    auto v = hilti::rt::type_info::Value(&*sx, &HILTI_INTERNAL_NS::type_info::__ti_Test_X, p);

    auto x = hilti::rt::type_info::value::auxType<hilti::rt::type_info::Struct>(v)->iterate(v);
    auto xi = x.begin();
    auto xf1 =
        hilti::rt::type_info::value::auxType<hilti::rt::type_info::SignedInteger<int32_t>>(xi->second)->get(xi->second);

    CHECK(xf1 == 42);
    xi++;

    auto xf2 = hilti::rt::type_info::value::auxType<hilti::rt::type_info::String>(xi->second)->get(xi->second);
    CHECK(xf2 == std::string("foo"_hs));
    xi++;

    auto y = hilti::rt::type_info::value::auxType<hilti::rt::type_info::Struct>(xi->second)->iterate(xi->second);
    auto yi = y.begin();

    auto yf1 = hilti::rt::type_info::value::auxType<hilti::rt::type_info::Bool>(yi->second)->get(yi->second);
    CHECK(yf1 == true);
    yi++;

    auto yf2 = hilti::rt::type_info::value::auxType<hilti::rt::type_info::Real>(yi->second)->get(yi->second);
    CHECK(yf2 == 3.14);
    yi++;

    xi++;
    CHECK(yi == y.end());
    CHECK(xi == x.end());
}

TEST_CASE("life-time") {
    // Check that we catch when values become inaccessible because of the
    // associated parent going away.
    Test::Y y{.b = true, .r = 3.14};

    auto x = StrongReference<Test::X>({.i = 42, .s = "foo", .y = y});
    auto p = hilti::rt::type_info::value::Parent(x);
    auto v = hilti::rt::type_info::Value(&*x, &HILTI_INTERNAL_NS::type_info::__ti_Test_X, p);

    // v is valid
    v.pointer();

    p = hilti::rt::type_info::value::Parent();

    // Now invalid.
    CHECK_THROWS_WITH_AS(v.pointer(), "type info value expired", const InvalidValue&);
}

TEST_CASE("no parent") {
    Test::Y y{.b = true, .r = 3.14};

    auto x = StrongReference<Test::X>({.i = 42, .s = "foo", .y = y});
    auto p = hilti::rt::type_info::value::Parent(x);
    auto v = hilti::rt::type_info::Value(&*x, &HILTI_INTERNAL_NS::type_info::__ti_Test_X); // no parent

    CHECK_EQ(v.pointer(), &*x); // access to the value works even without parent
}

TEST_CASE("internal fields") {
    struct A {
        integer::safe<int32_t> f1;
        std::string f2;
        bool __internal;
    };

    const TypeInfo ti = {"A",
                         "A",
                         nullptr,
                         new hilti::rt::type_info::Struct(
                             {hilti::rt::type_info::struct_::Field{"f1",
                                                                   &hilti::rt::type_info::int32,
                                                                   offsetof(A, f1),
                                                                   false,
                                                                   false,
                                                                   true},
                              hilti::rt::type_info::struct_::Field{"f2",
                                                                   &hilti::rt::type_info::string,
                                                                   offsetof(A, f2),
                                                                   false,
                                                                   false,
                                                                   true},
                              hilti::rt::type_info::struct_::Field{HILTI_INTERNAL_ID("internal"),
                                                                   &hilti::rt::type_info::bool_,
                                                                   offsetof(A, __internal),
                                                                   true,
                                                                   false,
                                                                   true}})};

    auto sx = StrongReference<A>({42, "foo", true});
    auto p = hilti::rt::type_info::value::Parent(sx);
    auto v = hilti::rt::type_info::Value(&*sx, &ti, p);

    const auto* const s = hilti::rt::type_info::value::auxType<hilti::rt::type_info::Struct>(v);

    CHECK_EQ(s->fields().size(), 2U);
    CHECK_EQ(s->fields(false).size(), 2U);
    CHECK_EQ(s->fields(true).size(), 3U);

    CHECK_EQ(s->iterate(v).size(), 2U);
    CHECK_EQ(s->iterate(v, false).size(), 2U);
    CHECK_EQ(s->iterate(v, true).size(), 3U);
}

TEST_CASE("anonymous fields") {
    struct A {
        std::string f1;
    };

    const TypeInfo ti = {"A",
                         "A",
                         nullptr,
                         new hilti::rt::type_info::Struct(
                             {hilti::rt::type_info::struct_::Field{"f1",
                                                                   &hilti::rt::type_info::int32,
                                                                   offsetof(A, f1),
                                                                   false,
                                                                   true,
                                                                   true}})};

    auto sx = StrongReference<A>({"foo"});
    auto p = hilti::rt::type_info::value::Parent(sx);
    auto v = hilti::rt::type_info::Value(&*sx, &ti, p);

    const auto* const s = hilti::rt::type_info::value::auxType<hilti::rt::type_info::Struct>(v);

    CHECK_EQ(s->fields().size(), 1U);
    CHECK(s->fields()[0].get().isAnonymous());
    CHECK(s->fields()[0].get().isEmitted());
}

TEST_CASE("no-emit fields") {
    struct A {
        std::string f1;
    };

    const TypeInfo ti = {"A",
                         "A",
                         nullptr,
                         new hilti::rt::type_info::Struct(
                             {hilti::rt::type_info::struct_::Field{"f1",
                                                                   &hilti::rt::type_info::int32,
                                                                   offsetof(A, f1),
                                                                   false,
                                                                   false,
                                                                   false}})};

    auto sx = StrongReference<A>({"foo"});
    auto p = hilti::rt::type_info::value::Parent(sx);
    auto v = hilti::rt::type_info::Value(&*sx, &ti, p);

    const auto* const s = hilti::rt::type_info::value::auxType<hilti::rt::type_info::Struct>(v);

    CHECK_EQ(s->fields().size(), 1U);
    CHECK(! s->fields()[0].get().isEmitted());

    // We shouldn't see this field when iterating.
    auto count =
        std::ranges::distance(hilti::rt::type_info::value::auxType<hilti::rt::type_info::Struct>(v)->iterate(v));
    CHECK_EQ(count, 0);
}

TEST_SUITE_END();
