#include "doctest/doctest.h"
// has to go first as it violates our requirements
#include "core/Error.h"
#include "core/ErrorCollector.h"
#include "core/ErrorQueue.h"
#include "core/NameSubstitution.h"
#include "core/Refcounting.h"
#include "core/TrailingObjects.h"
#include "core/TypePtr.h"
#include "core/Unfreeze.h"
#include "core/core.h"
#include "core/errors/internal.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/spdlog.h"

using namespace std;

namespace sorbet::core {
auto logger = spdlog::stderr_color_mt("parse");
auto errorCollector = make_shared<core::ErrorCollector>();
auto errorQueue = make_shared<ErrorQueue>(*logger, *logger, errorCollector);

struct Offset2PosTest {
    string src;
    optional<uint32_t> off;
    uint32_t line;
    uint32_t col;
};

string showOffset(const optional<uint32_t> &off) {
    if (off.has_value()) {
        return to_string(off.value());
    } else {
        return "nullopt";
    }
}

TEST_CASE("TestOffset2Pos2Offset") {
    GlobalState gs(errorQueue);
    gs.initEmpty();
    UnfreezeFileTable fileTableAccess(gs);

    vector<Offset2PosTest> cases = {
        {"hello", 0, 1, 1},
        {"line 1\nline 2", 1, 1, 2},
        {"line 1\nline 2", 7, 2, 1},
        {"line 1\nline 2", 11, 2, 5},
        {"a long line with no newlines\n", 20, 1, 21},
        {"line 1\nline 2\nline3\n", 7, 2, 1},
        {"line 1\nline 2\nline3", 6, 1, 7},
        {"line 1\nline 2\nline3", 7, 2, 1},
    };

    int i = 0;
    for (auto &tc : cases) {
        auto name = string("case: ") + to_string(i);
        INFO(name);
        FileRef f = gs.enterFile(name, tc.src);

        auto detail = Loc::pos2Detail(f.data(gs), tc.off.value());

        CHECK_EQ(tc.col, detail.column);
        CHECK_EQ(tc.line, detail.line);

        // Test that it's reversible
        auto offset = Loc::detail2Pos(f.data(gs), detail);
        CHECK_EQ(tc.off, offset);

        i++;
    }
}

TEST_CASE("TestPos2OffsetNull") {
    GlobalState gs(errorQueue);
    gs.initEmpty();
    UnfreezeFileTable fileTableAccess(gs);

    vector<Offset2PosTest> cases = {
        {"hello", nullopt, 0, 1},
        {"hello", UINT32_MAX, 1, 0},
        {"hello", 0, 1, 1},

        {"hello", 4, 1, 5},
        {"hello", 5, 1, 6},
        {"hello", nullopt, 1, 7},

        {"hello", 5, 2, 0}, // kind of strange?
        {"hello", nullopt, 2, 1},

        {"hello\n", 5, 2, 0},
        {"hello\n", 6, 2, 1},
        {"hello\n", nullopt, 2, 2},

        {"line 1\nline 2", 5, 1, 6},
        {"line 1\nline 2", 6, 1, 7},
        {"line 1\nline 2", nullopt, 1, 8},

        {"line 1\nline 2", 6, 2, 0},
        {"line 1\nline 2", 7, 2, 1},

        {"line 1\n\nline 2", 7, 2, 1},
        {"line 1\n\nline 2", nullopt, 2, 2},
    };

    int i = 0;
    for (auto &tc : cases) {
        auto name = string("case: ") + to_string(i);
        FileRef f = gs.enterFile(name, tc.src);

        auto actualOffset = Loc::detail2Pos(f.data(gs), Loc::Detail{tc.line, tc.col});

        INFO(fmt::format("i={}, CHECK_EQ({}, {})", i, showOffset(tc.off), showOffset(actualOffset)));
        CHECK_EQ(tc.off, actualOffset);
        i++;
    }
}

TEST_CASE("Errors") {
    GlobalState gs(errorQueue);
    gs.initEmpty();
    UnfreezeFileTable fileTableAccess(gs);
    FileRef f = gs.enterFile("a/foo.rb", string("def foo\n  hi\nend\n"));
    if (auto e = gs.beginError(Loc{f, 0, 3}, errors::Internal::InternalError)) {
        e.setHeader("Use of metavariable: `{}`", "foo");
    }
    gs.errorQueue->flushAllErrors(gs);
    REQUIRE(gs.hadCriticalError());
    REQUIRE_EQ(1, errorCollector->drainErrors().size());
}

TEST_CASE("SymbolRef") {
    GlobalState gs(errorQueue);
    gs.initEmpty();
    auto ref = Symbols::Object();
    CHECK_EQ(SymbolRef(ref), ref.data(gs)->ref(gs));
}

struct FileIsTypedCase {
    string_view src;
    StrictLevel strict;
};

TEST_CASE("FileIsTyped") {
    vector<FileIsTypedCase> cases = {
        {"", StrictLevel::None},
        {"# typed: true", StrictLevel::True},
        {"\n# typed: true\n", StrictLevel::True},
        {"not a typed: sigil\n# typed: true\n", StrictLevel::True},
        {"typed:\n# typed: nonsense\n", StrictLevel::None},
        {"# typed: strict\n", StrictLevel::Strict},
        {"# typed: strong\n", StrictLevel::Strong},
        {"# typed: autogenerated\n", StrictLevel::Autogenerated},
        {"# typed: false\n", StrictLevel::False},
        {"# typed: lax\n", StrictLevel::None},
        {"# typed: ignore\n", StrictLevel::Ignore},
        {"#    typed:      true\n", StrictLevel::True},
        {"typed: true\n", StrictLevel::None},

        // We no longer support the old sigil
        {"# @typed", StrictLevel::None},
        {"\n# @typed\n", StrictLevel::None},
    };
    for (auto &tc : cases) {
        CHECK_EQ(tc.strict, File::fileStrictSigil(tc.src));
    }
}

TEST_CASE("Substitute") {
    GlobalState gs1(errorQueue);
    gs1.initEmpty();

    GlobalState gs2(errorQueue);
    gs2.initEmpty();

    NameRef foo1, bar1, other1, cnstBaz1, uniqueBaz1, otherCnstBart1;
    NameRef foo2, bar2, cnstBaz2, uniqueBaz2;
    {
        UnfreezeNameTable thaw1(gs1);
        UnfreezeNameTable thaw2(gs2);

        foo1 = gs1.enterNameUTF8("foo");
        bar1 = gs1.enterNameUTF8("bar");
        other1 = gs1.enterNameUTF8("other");
        cnstBaz1 = gs1.enterNameConstant("Baz");
        uniqueBaz1 = gs1.freshNameUnique(UniqueNameKind::Namer, cnstBaz1, 1);
        otherCnstBart1 = gs1.enterNameConstant("Bart");

        foo2 = gs2.enterNameUTF8("foo");
        bar2 = gs2.enterNameUTF8("bar");
        cnstBaz2 = gs2.enterNameConstant("Baz");
        uniqueBaz2 = gs2.freshNameUnique(UniqueNameKind::Namer, cnstBaz1, 1);
        gs1.enterNameUTF8("name");
    }

    NameSubstitution subst(gs1, gs2);

    CHECK_EQ(subst.substitute(foo1), foo2);
    CHECK_EQ(subst.substitute(bar1), bar2);
    CHECK_EQ(subst.substitute(uniqueBaz1), uniqueBaz2);
    CHECK_EQ(subst.substitute(cnstBaz1), cnstBaz2);

    auto other2 = subst.substitute(other1);
    REQUIRE(other2.exists());
    REQUIRE(other2.kind() == NameKind::UTF8);
    REQUIRE_EQ("<U other>", other2.showRaw(gs2));
}

// Privileged class that is friends with TypePtr
class TypePtrTestHelper {
public:
    static uint64_t inlinedValue(const TypePtr &ptr) {
        CHECK(!ptr.containsPtr());
        return ptr.inlinedValue();
    }

    static TypePtr::tagged_storage store(const TypePtr &ptr) {
        return ptr.store;
    }

    static Refcountable *get(const TypePtr &ptr) {
        CHECK(ptr.containsPtr());
        return ptr.get();
    }

    static TypePtr create(TypePtr::Tag tag, Refcountable *type) {
        return TypePtr(tag, type);
    }

    static TypePtr createInlined(TypePtr::Tag tag, uint64_t inlinedValue) {
        return TypePtr(tag, inlinedValue);
    }
};

TEST_SUITE("TypePtr") {
    TEST_CASE("Does not allocate a counter for null type") {
        TypePtr ptr;
        CHECK_EQ(0, TypePtrTestHelper::store(ptr));
    }

    TEST_CASE("Properly manages counter") {
        auto ptr = make_type<UnresolvedClassType>(Symbols::untyped(), vector<NameRef>{});
        auto refptr = TypePtrTestHelper::get(ptr);
        REQUIRE_NE(nullptr, refptr);
        REQUIRE(!refptr->hasMultipleRefs());

        {
            // Copy should increment counter
            TypePtr ptrCopy(ptr);
            auto refptrCopy = TypePtrTestHelper::get(ptrCopy);
            REQUIRE_EQ(refptr, refptrCopy);
            REQUIRE(refptr->hasMultipleRefs());
        }

        // Destruction of copy should decrement counter
        REQUIRE(!refptr->hasMultipleRefs());

        {
            TypePtr ptrCopy(ptr);
            // Assigning/overwriting should decrement counter
            ptr = TypePtr();
            REQUIRE(!refptr->hasMultipleRefs());

            // Moving should keep counter the same
            ptr = move(ptrCopy);
            REQUIRE(!refptr->hasMultipleRefs());

            // Moving should clear counter from ptrCopy and make it an empty TypePtr
            CHECK_EQ(0, TypePtrTestHelper::store(ptrCopy));
            CHECK_EQ(TypePtr(), ptrCopy);

            // Assigning to nullptr should increment counter (and not try to increment the null counter field in
            // ptrCopy)
            ptrCopy = ptr;
            REQUIRE(refptr->hasMultipleRefs());
        }
        REQUIRE(!refptr->hasMultipleRefs());
    }

    TEST_CASE("Tagging works as expected") {
        {
            auto rawPtr = new UnresolvedClassType(Symbols::untyped(), {});
            auto ptr = TypePtrTestHelper::create(TypePtr::Tag::UnresolvedClassType, rawPtr);
            CHECK_EQ(TypePtr::Tag::UnresolvedClassType, ptr.tag());
            CHECK_EQ(rawPtr, TypePtrTestHelper::get(ptr));
        }
    }

    TEST_CASE("Supports inlined values") {
        // Let's try edge cases.
        std::list<pair<uint32_t, uint64_t>> valuesArray = {
            {0, 0},
            {1, 1},
            {0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF},
        };

        for (auto values : valuesArray) {
            SUBCASE(fmt::format("{}, {}", values.first, values.second).c_str()) {
                auto type = TypePtrTestHelper::createInlined(TypePtr::Tag::SelfType, values.first);
                CHECK_EQ(TypePtr::Tag::SelfType, type.tag());
                CHECK_EQ(values.first, TypePtrTestHelper::inlinedValue(type));
            }
        }
    }
}

class TOTestNeedsAlignment final : public TrailingObjects<TOTestNeedsAlignment, int> {
public:
    uint16_t member;
};

class TOTestSufficientlyAligned final : public TrailingObjects<TOTestSufficientlyAligned, int> {
public:
    uint64_t member;
};

TEST_SUITE("TrailingObjects") {
    TEST_CASE("trailing objects need alignment") {
        CHECK_EQ(alignof(TOTestNeedsAlignment), 4);
        CHECK_EQ(4, TOTestNeedsAlignment::totalSizeToAlloc<int>(0));
        CHECK_EQ(8, TOTestNeedsAlignment::totalSizeToAlloc<int>(1));
        CHECK_EQ(12, TOTestNeedsAlignment::totalSizeToAlloc<int>(2));
    }

    TEST_CASE("base object over-aligned for trailing objects") {
        CHECK_EQ(alignof(TOTestSufficientlyAligned), 8);
        CHECK_EQ(8, TOTestSufficientlyAligned::totalSizeToAlloc<int>(0));
        CHECK_EQ(12, TOTestSufficientlyAligned::totalSizeToAlloc<int>(1));
        CHECK_EQ(16, TOTestSufficientlyAligned::totalSizeToAlloc<int>(2));
    }
}

namespace {
class TestRefCounted final : public sorbet::core::RefCounted<TestRefCounted> {
public:
    int value;
    int *destructor_count;
    TestRefCounted(int v, int &count) : value(v), destructor_count(&count) {}
    ~TestRefCounted() {
        (*destructor_count)++;
    }
};
} // namespace

TEST_SUITE("RefPtr") {
    TEST_CASE("Default constructs to null") {
        sorbet::core::RefPtr<TestRefCounted> ptr;
        CHECK(!ptr);
        CHECK(ptr == nullptr);
        CHECK(ptr.get() == nullptr);
    }

    TEST_CASE("Constructs from raw pointer and releases") {
        int dtor_count = 0;
        {
            auto ptr = sorbet::core::makeRefPtr<TestRefCounted>(42, dtor_count);
            CHECK(ptr != nullptr);
            CHECK_EQ(42, ptr->value);
            CHECK_EQ(42, (*ptr).value);
        }
        CHECK_EQ(1, dtor_count);
    }

    TEST_CASE("Copy increments refcount") {
        int dtor_count = 0;
        {
            auto ptr = sorbet::core::makeRefPtr<TestRefCounted>(7, dtor_count);
            CHECK(!ptr->hasMultipleRefs());
            {
                sorbet::core::RefPtr<TestRefCounted> copy(ptr);
                CHECK(ptr->hasMultipleRefs());
                CHECK_EQ(ptr.get(), copy.get());
            }
            CHECK(!ptr->hasMultipleRefs());
            CHECK_EQ(0, dtor_count);
        }
        CHECK_EQ(1, dtor_count);
    }

    TEST_CASE("Move does not increment refcount") {
        int dtor_count = 0;
        {
            auto ptr = sorbet::core::makeRefPtr<TestRefCounted>(9, dtor_count);
            auto *raw = ptr.get();
            CHECK(!raw->hasMultipleRefs());

            sorbet::core::RefPtr<TestRefCounted> moved(std::move(ptr));
            CHECK(!ptr);
            CHECK(moved);
            CHECK_EQ(raw, moved.get());
            CHECK(!raw->hasMultipleRefs());
        }
        CHECK_EQ(1, dtor_count);
    }

    TEST_CASE("Copy assignment") {
        int dtor_count = 0;
        {
            auto a = sorbet::core::makeRefPtr<TestRefCounted>(1, dtor_count);
            auto b = sorbet::core::makeRefPtr<TestRefCounted>(2, dtor_count);
            auto *rawB = b.get();

            a = b;
            CHECK_EQ(rawB, a.get());
            CHECK(rawB->hasMultipleRefs());
            CHECK_EQ(1, dtor_count); // old 'a' destroyed
        }
        CHECK_EQ(2, dtor_count);
    }

    TEST_CASE("Move assignment") {
        int dtor_count = 0;
        {
            auto a = sorbet::core::makeRefPtr<TestRefCounted>(1, dtor_count);
            auto b = sorbet::core::makeRefPtr<TestRefCounted>(2, dtor_count);
            auto *rawB = b.get();

            a = std::move(b);
            CHECK(!b);
            CHECK_EQ(rawB, a.get());
            CHECK(!rawB->hasMultipleRefs());
            CHECK_EQ(1, dtor_count); // old 'a' destroyed
        }
        CHECK_EQ(2, dtor_count);
    }

    TEST_CASE("Assign nullptr releases") {
        int dtor_count = 0;
        {
            auto ptr = sorbet::core::makeRefPtr<TestRefCounted>(5, dtor_count);
            ptr = nullptr;
            CHECK(!ptr);
            CHECK_EQ(1, dtor_count);
        }
        CHECK_EQ(1, dtor_count);
    }
}

TEST_CASE("isPackageSpecSymbol") {
    GlobalState gs(errorQueue);
    gs.initEmpty();

    {
        auto unfrezePackages = gs.packageDB().unfreeze();
        gs.setPackagerOptions({}, {}, {}, {}, {}, {}, {}, "", packages::GenPackagesMode::Disabled, false, false, false);
    }

    CHECK_FALSE(Symbols::root().isPackageSpecSymbol(gs));

    {
        UnfreezeSymbolTable unfreeze(gs);
        auto testNamespace = gs.enterClassOrModuleSymbol(Loc::none(), Symbols::root(), Names::Constants::Test());
        REQUIRE(testNamespace.exists());
        CHECK_FALSE(testNamespace.isPackageSpecSymbol(gs));
    }
}

} // namespace sorbet::core
