/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

// Regression test for the Predicate cross-DSO RTTI/ABI fix (the out-of-line
// `Predicate::~Predicate` key function in common/predicate/predicate.cpp).
//
// `Predicate` is an abstract base with only pure virtuals. Without an
// out-of-line virtual ("key function"), the compiler has no single home for its
// vtable and typeinfo, so it emits them *weakly* in every translation unit that
// uses them. Combined with `-fvisibility=hidden`, separately linked modules
// (e.g. libpaimon.so vs libpaimon_parquet_file_format.so, or plugins loaded via
// dlopen) can each end up with their own `Predicate` typeinfo. A cross-module
// `dynamic_cast`/`dynamic_pointer_cast` then compares mismatched typeinfo and
// fails. Giving `Predicate` an out-of-line destructor anchors a single,
// exported definition of its RTTI that all other modules import.
//
// This file guards the fix on two levels:
//   1. Behavior: `Predicate` objects are created inside libpaimon.so and cast to
//      derived types here in the test module. This catches the failure on
//      toolchains that compare std::type_info by pointer (e.g. libc++) and in
//      downstream builds that link plugins aggressively.
//   2. Symbol layout (Linux/ELF only): assert that `Predicate`'s typeinfo has a
//      single home in libpaimon.so which the format plugins import, rather than
//      each plugin emitting its own duplicate copy. This is the deterministic
//      signature of the key function on glibc/libstdc++ toolchains, which merge
//      weak typeinfo by name and therefore would not fail the behavioral cast
//      even without the fix.

#include <memory>

#include "gtest/gtest.h"
#include "paimon/defs.h"
#include "paimon/predicate/compound_predicate.h"
#include "paimon/predicate/leaf_predicate.h"
#include "paimon/predicate/literal.h"
#include "paimon/predicate/predicate.h"
#include "paimon/predicate/predicate_builder.h"
#include "paimon/testing/utils/testharness.h"

namespace paimon::test {

// A leaf predicate built in libpaimon.so must survive dynamic_pointer_cast to
// LeafPredicate across the module boundary.
TEST(PredicateAbiInteTest, LeafPredicateCastsAcrossModule) {
    std::shared_ptr<Predicate> predicate =
        PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
                                Literal(static_cast<int64_t>(5)));
    ASSERT_NE(predicate, nullptr);

    std::shared_ptr<LeafPredicate> leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate);
    ASSERT_NE(leaf, nullptr) << "dynamic_pointer_cast<LeafPredicate> failed across the module "
                                "boundary; Predicate RTTI is likely duplicated (missing key "
                                "function)";
    EXPECT_EQ(leaf->FieldName(), "f0");
    EXPECT_EQ(leaf->FieldIndex(), 0);

    // A leaf predicate must not be mistaken for a compound predicate.
    EXPECT_EQ(std::dynamic_pointer_cast<CompoundPredicate>(predicate), nullptr);
}

// A compound predicate built in libpaimon.so must survive dynamic_pointer_cast
// to CompoundPredicate, and its children must remain castable to LeafPredicate.
TEST(PredicateAbiInteTest, CompoundPredicateCastsAcrossModule) {
    std::shared_ptr<Predicate> left =
        PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT,
                                Literal(static_cast<int64_t>(5)));
    std::shared_ptr<Predicate> right = PredicateBuilder::Equal(
        /*field_index=*/1, /*field_name=*/"f1", FieldType::INT, Literal(10));

    ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate,
                         PredicateBuilder::And({left, right}));

    std::shared_ptr<CompoundPredicate> compound =
        std::dynamic_pointer_cast<CompoundPredicate>(predicate);
    ASSERT_NE(compound, nullptr)
        << "dynamic_pointer_cast<CompoundPredicate> failed across the module boundary";
    ASSERT_EQ(compound->Children().size(), 2u);

    EXPECT_EQ(std::dynamic_pointer_cast<LeafPredicate>(predicate), nullptr);
    for (const auto& child : compound->Children()) {
        EXPECT_NE(std::dynamic_pointer_cast<LeafPredicate>(child), nullptr);
    }
}

}  // namespace paimon::test

#if defined(__linux__) && defined(__ELF__)
#include <elf.h>
#include <fcntl.h>
#include <link.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <cstdint>
#include <cstring>
#include <string>

namespace paimon::test {
namespace {

// Mangled name of `typeinfo for paimon::Predicate`.
constexpr const char* kPredicateTypeInfoSymbol = "_ZTIN6paimon9PredicateE";

// Result of looking up a symbol in an ELF dynamic symbol table.
enum class DynSym {
    kError,     // could not read the file
    kAbsent,    // symbol not present in .dynsym
    kDefined,   // symbol has a definition in this module (st_shndx != SHN_UNDEF)
    kImported,  // symbol is undefined here and resolved from another module
};

// On-disk path of the loaded shared object whose path contains `needle`.
std::string FindLoadedModule(const char* needle) {
    struct Ctx {
        const char* needle;
        std::string path;
    } ctx{needle, {}};
    dl_iterate_phdr(
        [](struct dl_phdr_info* info, size_t, void* data) -> int {
            auto* c = static_cast<Ctx*>(data);
            if (info->dlpi_name != nullptr && std::strstr(info->dlpi_name, c->needle) != nullptr) {
                c->path = info->dlpi_name;
                return 1;
            }
            return 0;
        },
        &ctx);
    return ctx.path;
}

// Whether `sym_name` is defined in, imported by, or absent from the dynamic
// symbol table of the ELF file at `path`.
//
// We look at whether the symbol is *defined* (has a section index) rather than
// at its binding (GLOBAL vs WEAK): a class with a key function has WEAK typeinfo
// on GCC but GLOBAL typeinfo on Clang, so the binding is not portable. What both
// compilers guarantee is that the key function makes the typeinfo a single
// definition that other modules import.
DynSym LookupDynSym(const std::string& path, const char* sym_name) {
    int fd = ::open(path.c_str(), O_RDONLY);
    if (fd < 0) {
        return DynSym::kError;
    }
    struct stat st{};
    if (::fstat(fd, &st) != 0) {
        ::close(fd);
        return DynSym::kError;
    }
    void* map = ::mmap(nullptr, static_cast<size_t>(st.st_size), PROT_READ, MAP_PRIVATE, fd, 0);
    ::close(fd);
    if (map == MAP_FAILED) {
        return DynSym::kError;
    }

    const auto* base = static_cast<const uint8_t*>(map);
    const auto* ehdr = reinterpret_cast<const Elf64_Ehdr*>(base);
    DynSym result = DynSym::kAbsent;
    if (std::memcmp(ehdr->e_ident, ELFMAG, SELFMAG) == 0 && ehdr->e_ident[EI_CLASS] == ELFCLASS64) {
        const auto* shdrs = reinterpret_cast<const Elf64_Shdr*>(base + ehdr->e_shoff);
        for (int i = 0; i < ehdr->e_shnum; ++i) {
            if (shdrs[i].sh_type != SHT_DYNSYM) {
                continue;
            }
            const auto* syms = reinterpret_cast<const Elf64_Sym*>(base + shdrs[i].sh_offset);
            const char* strtab =
                reinterpret_cast<const char*>(base + shdrs[shdrs[i].sh_link].sh_offset);
            size_t count = shdrs[i].sh_size / sizeof(Elf64_Sym);
            for (size_t s = 0; s < count; ++s) {
                if (std::strcmp(strtab + syms[s].st_name, sym_name) == 0) {
                    result = syms[s].st_shndx == SHN_UNDEF ? DynSym::kImported : DynSym::kDefined;
                    break;
                }
            }
            break;
        }
    }
    ::munmap(map, static_cast<size_t>(st.st_size));
    return result;
}

}  // namespace

// Deterministic guard (compiler-agnostic across GCC and Clang): the out-of-line
// `~Predicate()` key function must give `Predicate`'s typeinfo a single home in
// libpaimon.so, which the format plugins then import instead of each emitting
// their own copy. Without the key function the typeinfo is weak and every
// module that casts a Predicate defines its own duplicate -- exactly the
// condition that breaks cross-DSO dynamic_cast. Reverting the fix flips this
// test from PASS to FAIL.
TEST(PredicateAbiInteTest, PredicateTypeInfoHasSingleHome) {
    std::string core = FindLoadedModule("libpaimon.so");
    ASSERT_FALSE(core.empty()) << "could not locate loaded libpaimon.so";

    // The core library owns the one definition of the typeinfo.
    EXPECT_EQ(LookupDynSym(core, kPredicateTypeInfoSymbol), DynSym::kDefined)
        << "typeinfo for paimon::Predicate must be defined in " << core;

    // A format plugin that casts predicates must import that definition rather
    // than defining its own duplicate. The parquet plugin performs
    // dynamic_pointer_cast<LeafPredicate>/<CompoundPredicate> on Predicate, so it
    // references the typeinfo; with the key function that reference resolves to
    // libpaimon.so (imported), without it the plugin carries its own weak copy.
    std::string plugin = FindLoadedModule("libpaimon_parquet_file_format.so");
    ASSERT_FALSE(plugin.empty()) << "could not locate loaded libpaimon_parquet_file_format.so";

    DynSym in_plugin = LookupDynSym(plugin, kPredicateTypeInfoSymbol);
    ASSERT_NE(in_plugin, DynSym::kError) << "could not read " << plugin;
    ASSERT_NE(in_plugin, DynSym::kAbsent)
        << "typeinfo for paimon::Predicate is not referenced by " << plugin
        << "; the parquet plugin is expected to cast Predicate across the DSO boundary";
    EXPECT_EQ(in_plugin, DynSym::kImported)
        << "typeinfo for paimon::Predicate is defined inside " << plugin
        << " instead of being imported from libpaimon.so; the Predicate RTTI is duplicated across "
           "DSOs (the ~Predicate() key function is missing), which breaks cross-DSO dynamic_cast";
}

}  // namespace paimon::test
#endif  // defined(__linux__) && defined(__ELF__)
