#include "test.h"

using Test = TestSv<Stash>;
using panda::string_view;

TEST_CASE("Stash", "[Stash]") {
    perlvars vars;
    Stash my(vars.stash);
    Hash h_valid(vars.stash), h_invalid(vars.hv);
    Sv oth_valid(vars.stash), oth_invalid(vars.hv);
    my.erase("test");

    SECTION("ctor") {
        SECTION("empty") {
            Stash o;
            REQUIRE(!o);
        }
        SECTION("SV") {
            SECTION("undef SV")  { Test::ctor(vars.undef, behaviour_t::EMPTY); }
            SECTION("number SV") { Test::ctor(vars.iv, behaviour_t::THROWS); }
            SECTION("string SV") { Test::ctor(vars.pv, behaviour_t::THROWS); }
            SECTION("RV")        { Test::ctor(vars.rv, behaviour_t::THROWS); }
            SECTION("AV")        { Test::ctor((SV*)vars.av, behaviour_t::THROWS); }
            SECTION("HV")        { Test::ctor((SV*)vars.hv, behaviour_t::THROWS); }
            SECTION("OHV")       { Test::ctor((SV*)vars.ohv, behaviour_t::THROWS); }
            SECTION("SHV")       { Test::ctor((SV*)vars.stash, behaviour_t::VALID); }
            SECTION("CV")        { Test::ctor((SV*)vars.cv, behaviour_t::THROWS); }
            SECTION("GV")        { Test::ctor((SV*)vars.gv, behaviour_t::THROWS); }
            SECTION("IO")        { Test::ctor((SV*)vars.io, behaviour_t::THROWS); }
        }
        SECTION("HV")  { Test::ctor(vars.hv, behaviour_t::THROWS); }
        SECTION("OHV") { Test::ctor(vars.ohv, behaviour_t::THROWS); }
        SECTION("SHV") { Test::ctor(vars.stash, behaviour_t::VALID); }
        SECTION("string") {
            Stash o("MyTest");
            REQUIRE(o);
            REQUIRE(o == gv_stashpvs("MyTest", 0));
        }

        SECTION("Stash")        { Test::ctor(my, behaviour_t::VALID); }
        SECTION("valid Hash")   { Test::ctor(h_valid, behaviour_t::VALID); }
        SECTION("invalid Hash") { Test::ctor(h_invalid, behaviour_t::THROWS); }
        SECTION("valid Sv")     { Test::ctor(oth_valid, behaviour_t::VALID); }
        SECTION("invalid Sv")   { Test::ctor(oth_invalid, behaviour_t::THROWS); }
    }

    SECTION("operator=") {
        Stash o(gv_stashpvs("MyTest::__Dummy", GV_ADD));
        SECTION("SV") {
            SECTION("undef SV")  { Test::assign(o, vars.undef, behaviour_t::EMPTY); }
            SECTION("number SV") { Test::assign(o, vars.iv, behaviour_t::THROWS); }
            SECTION("string SV") { Test::assign(o, vars.pv, behaviour_t::THROWS); }
            SECTION("RV")        { Test::assign(o, vars.rv, behaviour_t::THROWS); }
            SECTION("AV")        { Test::assign(o, (SV*)vars.av, behaviour_t::THROWS); }
            SECTION("HV")        { Test::assign(o, (SV*)vars.hv, behaviour_t::THROWS); }
            SECTION("OHV")       { Test::assign(o, (SV*)vars.ohv, behaviour_t::THROWS); }
            SECTION("SHV")       { Test::assign(o, (SV*)vars.stash, behaviour_t::VALID); }
            SECTION("CV")        { Test::assign(o, (SV*)vars.cv, behaviour_t::THROWS); }
            SECTION("GV")        { Test::assign(o, (SV*)vars.gv, behaviour_t::THROWS); }
            SECTION("IO")        { Test::assign(o, (SV*)vars.io, behaviour_t::THROWS); }
        }
        SECTION("HV")           { Test::assign(o, vars.hv, behaviour_t::THROWS); }
        SECTION("OHV")          { Test::assign(o, vars.ohv, behaviour_t::THROWS); }
        SECTION("SHV")          { Test::assign(o, vars.stash, behaviour_t::VALID); }
        SECTION("Stash")        { Test::assign(o, my, behaviour_t::VALID); }
        SECTION("valid Hash")   { Test::assign(o, h_valid, behaviour_t::VALID); }
        SECTION("invalid Hash") { Test::assign(o, h_invalid, behaviour_t::THROWS); }
        SECTION("valid Sv")     { Test::assign(o, oth_valid, behaviour_t::VALID); }
        SECTION("invalid Sv")   { Test::assign(o, oth_invalid, behaviour_t::THROWS); }
    }

    SECTION("set") {
        Stash o;
        SECTION("SV") {
            auto cnt = SvREFCNT(vars.iv);
            o.set(vars.iv); // no checks
            REQUIRE(o);
            REQUIRE(SvREFCNT(vars.iv) == cnt+1);
            REQUIRE(o.get() == vars.iv);
        }
        SECTION("HV") {
            auto cnt = SvREFCNT(vars.hv);
            o.set(vars.hv); // no checks
            REQUIRE(o);
            REQUIRE(SvREFCNT(vars.hv) == cnt+1);
            REQUIRE(o.get<HV>() == vars.hv);
        }
    }

    SECTION("cast") {
        Stash o(vars.stash);
        auto rcnt = SvREFCNT(vars.stash);
        SECTION("to SV") {
            SV* sv = o;
            REQUIRE(sv == (SV*)vars.stash);
            REQUIRE(SvREFCNT(vars.stash) == rcnt);
        }
        SECTION("to HV") {
            HV* sv = o;
            REQUIRE(sv == vars.stash);
            REQUIRE(SvREFCNT(vars.stash) == rcnt);
        }
    }

    SECTION("get") {
        Stash o(vars.stash);
        auto rcnt = SvREFCNT(vars.stash);
        REQUIRE(o.get<>() == (SV*)vars.stash);
        REQUIRE(o.get<SV>() == (SV*)vars.stash);
        REQUIRE(o.get<HV>() == vars.stash);
        REQUIRE(SvREFCNT(vars.stash) == rcnt);
    }

    SECTION("name/effective_name") {
        REQUIRE(my.name() == "M1");
        REQUIRE(my.effective_name() == "M1");
    }

    SECTION("fetch") {
        auto glob = my.fetch("method");
        REQUIRE(glob);
        REQUIRE(glob.sub() == get_cv("M1::method", 0));
    }

    SECTION("[]const") {
        const Stash& o = my;
        auto glob = o["method"];
        REQUIRE(glob);
        REQUIRE(glob.sub() == get_cv("M1::method", 0));
    }

    SECTION("at") {
        auto glob = my.at("method");
        REQUIRE(glob);
        REQUIRE(glob.sub() == get_cv("M1::method", 0));
        REQUIRE_THROWS(my.at("jopa"));
    }

    SECTION("[]=") {
        SECTION("nullptr") {
            my["test"].scalar(Simple(10));
            REQUIRE(my.fetch("test").scalar());
            my["test"] = nullptr;
            REQUIRE(!my.fetch("test").scalar());
        }
        SECTION("SV") {
            SECTION("undef") {
                auto v = Sv::create();
                my["test"] = v.get();
                REQUIRE(my["test"].scalar() == v);
            }
            SECTION("simple") {
                Simple v(100);
                my["test"] = v.get();
                REQUIRE(my["test"].scalar() == v);
            }
            SECTION("AV") {
                auto v = Array::create();
                my["test"] = v.get();
                REQUIRE(my["test"].array() == v);
            }
            SECTION("HV") {
                auto v = Hash::create();
                my["test"] = v.get();
                REQUIRE(my["test"].hash() == v);
            }
            SECTION("CV") {
                Sub v(vars.cv);
                my["test"] = v.get();
                REQUIRE(my["test"].sub() == v);
            }
            SECTION("GV") {
                auto v = my["method"];
                my["test"] = v.get();
                REQUIRE(my["test"] == v);
            }
            SECTION("IO") {
                Io v(vars.io);
                my["test"] = v.get();
                REQUIRE(my["test"].io() == v);
            }
        }
        SECTION("AV") {
            auto v = Array::create();
            my["test"] = v.get<AV>();
            REQUIRE(my["test"].array() == v);
        }
        SECTION("HV") {
            auto v = Hash::create();
            my["test"] = v.get<HV>();
            REQUIRE(my["test"].hash() == v);
        }
        SECTION("CV") {
            Sub v(vars.cv);
            my["test"] = v.get<CV>();
            REQUIRE(my["test"].sub() == v);
        }
        SECTION("GV") {
            auto v = my["method"];
            my["test"] = v.get<GV>();
            REQUIRE(my["test"] == v);

            my.erase("test");
            auto o = my["test"];
            o.scalar(Simple(100));
            o.array(Array::create());
            o.hash(Hash::create());
            o.sub(Sub(vars.cv));
            my["test"] = (GV*)NULL;
            REQUIRE(!my["test"].scalar());
            REQUIRE(!my["test"].array());
            REQUIRE(!my["test"].hash());
            REQUIRE(!my["test"].sub());
        }
        SECTION("IO") {
            Io v(vars.io);
            my["test"] = v.get<IO>();
            REQUIRE(my["test"].io() == v);
        }
        SECTION("Sv") {
            Sv v = Simple(100);
            my["test"] = v;
            REQUIRE(my["test"].scalar() == v);
            v = Array::create();
            my["test"] = v;
            REQUIRE(my["test"].array() == v);
        }
        SECTION("Scalar") {
            Scalar v = Simple(222);
            my["test"] = v;
            REQUIRE(my["test"].scalar() == v);
        }
        SECTION("Ref") {
            Ref v = Ref::create(Simple(100));
            my["test"] = v;
            REQUIRE(my["test"].scalar() == v);
        }
        SECTION("Simple") {
            Simple v(222);
            my["test"] = v;
            REQUIRE(my["test"].scalar() == v);
        }
        SECTION("Array") {
            auto v = Array::create();
            my["test"] = v;
            REQUIRE(my["test"].array() == v);
        }
        SECTION("Hash") {
            auto v = Hash::create();
            my["test"] = v;
            REQUIRE(my["test"].hash() == v);
        }
        SECTION("Stash") {
            auto v = Stash(vars.stash);
            my["test"] = v;
            REQUIRE(my["test"].hash() == v);
        }
        SECTION("Sub") {
            Sub v(vars.cv);
            my["test"] = v;
            REQUIRE(my["test"].sub() == v);
        }
        SECTION("Object") {
            Object v(vars.ov);
            my["test"] = v;
            REQUIRE(my["test"].scalar() == v);
            v = vars.oav;
            my["test"] = v;
            REQUIRE(my["test"].array() == v);
        }
        SECTION("Glob") {
            my["test"] = my["method"];
            REQUIRE(my["test"] == my.fetch("method"));
            REQUIRE(my.fetch("test").sub() == get_cv("M1::method", 0));

            my.erase("test");
            auto o = my["test"];
            o.scalar(Simple(100));
            o.array(Array::create());
            o.hash(Hash::create());
            o.sub(Sub(vars.cv));
            my["test"] = Glob();
            REQUIRE(!my["test"].scalar());
            REQUIRE(!my["test"].array());
            REQUIRE(!my["test"].hash());
            REQUIRE(!my["test"].sub());
        }
        SECTION("Io") {
            Io v(vars.io);
            my["test"] = v;
            REQUIRE(my["test"].io() == v);
        }
    }

    SECTION("store") {
        my.store("test", my["method"]);
        REQUIRE(my.fetch("test"));
        REQUIRE(my.fetch("test").sub() == get_cv("M1::method", 0));
        my.erase("test");
        my.store("test", vars.iv);
        REQUIRE(my.fetch("test"));
        REQUIRE(my.fetch("test").scalar() == vars.iv);
    }

    SECTION("const sub promote") {
        Hash(my).store("test", Ref::create(Simple(1)));
        REQUIRE(my.fetch("test"));
        REQUIRE(my.fetch("test").sub() == get_cv("M1::test", 0));
    }

    if (PERL_VERSION >= 22) { // in older perls only globs and refs to primitives allowed in symbol tables
        SECTION("sub promote") {
            Hash(my).store("test", get_sv("M1::anon", 0));
            REQUIRE(my.fetch("test"));
            REQUIRE(my.fetch("test").sub() == get_cv("M1::test", 0));
        }
    }

    SECTION("scalar") {
        REQUIRE(!my.scalar("jopa"));
        auto v = Simple(333);
        my.scalar("test", v);
        REQUIRE(my["test"].scalar() == v);
        REQUIRE(my.scalar("test") == v);
    }

    SECTION("array") {
        REQUIRE(!my.array("jopa"));
        auto v = Array::create();
        my.array("test", v);
        REQUIRE(my["test"].array() == v);
        REQUIRE(my.array("test") == v);
    }

    SECTION("hash") {
        REQUIRE(!my.hash("jopa"));
        auto v = Hash::create();
        my.hash("test", v);
        REQUIRE(my["test"].hash() == v);
        REQUIRE(my.hash("test") == v);
    }

    SECTION("sub") {
        REQUIRE(!my.sub("jopa"));
        auto v = Sub(vars.cv);
        my.sub("test", v);
        REQUIRE(my["test"].sub() == v);
        REQUIRE(my.sub("test") == v);
    }

    SECTION("io") {
        REQUIRE(!my.io("jopa"));
        auto v = Io(vars.io);
        my.io("test", v);
        REQUIRE(my["test"].io() == v);
        REQUIRE(my.io("test") == v);
    }

    SECTION("path") {
        Stash o("AA::BB::CC", GV_ADD);
        REQUIRE(o.path() == "AA/BB/CC.pm");
    }

    SECTION("mark_as_loaded") {
        Stash o("M1");
        o.mark_as_loaded(Stash("MyTest"));
        REQUIRE(Stash::root().hash("INC").fetch(o.path()).is_true());
        REQUIRE_THROWS(o.mark_as_loaded(Stash("Nonexistent")));
    }

    SECTION("inherit") {
        Stash o("M_INH", GV_ADD);
        o.mark_as_loaded(Stash("MyTest"));
        o.inherit(Stash("M1"));
        auto ISA = o.array("ISA");
        REQUIRE(ISA.size() == 1);
        REQUIRE(Simple(ISA[0]) == "M1");
    }

    SECTION("method/method_strict") {
        Stash o("M2");
        REQUIRE(o.method("child_method"));
        REQUIRE(o.method("child_method") == get_cv("M2::child_method", 0));
        REQUIRE(o.method("child_method") == o.method_strict("child_method"));
        REQUIRE(o.method("method"));
        REQUIRE(o.method("method") == get_cv("M1::method", 0));
        REQUIRE(o.method("method") == o.method_strict("method"));
        REQUIRE(!o.method("nomethod"));
        REQUIRE_THROWS(o.method_strict("nomethod"));
    }

    SECTION("name_hek") {
        auto hek = my.name_hek();
        REQUIRE(HEK_KEY(hek) == my.name().data());
    }

    SECTION("name_sv") {
        auto nm = my.name_sv();
        REQUIRE((string_view)nm == my.name());
        REQUIRE(nm.c_str() == my.name().data());
    }

    SECTION("isa") {
        Stash o("M2");
        REQUIRE(o.isa("M1"));
        REQUIRE(o.isa(Stash("M1")));
        REQUIRE(!o.isa("M254"));
        REQUIRE(!o.isa(Stash("M255", GV_ADD)));
    }

    SECTION("bless") {
        Stash st("MyTest");
        Simple obase(100);
        SECTION("SV") {
            auto o = st.bless(obase);
            REQUIRE(o);
            REQUIRE(o == obase);
            REQUIRE(o.stash() == st);
        }
        SECTION("RV") {
            auto r = Ref::create(obase);
            auto o = st.bless(r);
            REQUIRE(o);
            REQUIRE(o == obase);
            REQUIRE(o.stash() == st);
            REQUIRE(o.ref() == r);
        }
    }

    SECTION("call") {
        Simple ret = my.call("class_method");
        REQUIRE(ret);
        REQUIRE(ret == string_view("M1-hi"));
        my.call<void>("class_method", (SV**)NULL, 0);
        my.call<void>("class_method", (SV*)NULL);
        my.call<void>("class_method", Sv());
        my.call<Sv>("class_method");
    }

    SECTION("call_next/super/SUPER") {
        Stash s("M4");
        Sub sub = s.method("meth");
        Simple res;

        SECTION("SUPER") {
            res = s.call_SUPER(sub);
            REQUIRE(res == "M4-2");
            sub = sub.SUPER_strict();
            REQUIRE(sub == get_cv("M2::meth", 0));

            res = s.call_SUPER(sub);
            REQUIRE(res == "M4-1");
            sub = sub.SUPER_strict();
            REQUIRE(sub == get_cv("M1::meth", 0));

            REQUIRE(!sub.SUPER());
            REQUIRE_THROWS(sub.SUPER_strict());
            REQUIRE_THROWS(s.call_SUPER<void>(sub));
        }

        SECTION("next") {
            res = s.call_next(sub);
            REQUIRE(res == "M4-2");
            sub = s.next_method_strict(sub);
            REQUIRE(sub == get_cv("M2::meth", 0));

            res = s.call_next(sub);
            REQUIRE(res == "M4-3");
            sub = s.next_method_strict(sub);
            REQUIRE(sub == get_cv("M3::meth", 0));

            res = s.call_next(sub);
            REQUIRE(res == "M4-1");
            sub = s.next_method_strict(sub);
            REQUIRE(sub == get_cv("M1::meth", 0));

            REQUIRE(!s.next_method(sub));
            REQUIRE_THROWS(s.next_method_strict(sub));
            REQUIRE_THROWS(s.call_next<void>(sub));
        }

        SECTION("next_maybe") {
            res = s.call_next_maybe(sub);
            REQUIRE(res == "M4-2");
            sub = s.next_method(sub);

            res = s.call_next_maybe(sub);
            REQUIRE(res == "M4-3");
            sub = s.next_method(sub);

            res = s.call_next_maybe(sub);
            REQUIRE(res == "M4-1");
            sub = s.next_method(sub);

            res = s.call_next_maybe(sub);
            REQUIRE(!res);
        }

        SECTION("super/dfs") {
            res = s.call_super(sub);
            REQUIRE(res == "M4-2");
            sub = s.super_method_strict(sub);
            REQUIRE(sub == get_cv("M2::meth", 0));

            res = s.call_super(sub);
            REQUIRE(res == "M4-1");
            sub = s.super_method_strict(sub);
            REQUIRE(sub == get_cv("M1::meth", 0));

            REQUIRE(!s.super_method(sub));
            REQUIRE_THROWS(s.super_method_strict(sub));
            REQUIRE_THROWS(s.call_super<void>(sub));
        }

        SECTION("super/c3") {
            s.call<void>("enable_c3");

            res = s.call_super(sub);
            REQUIRE(res == "M4-2");
            sub = s.super_method_strict(sub);
            REQUIRE(sub == get_cv("M2::meth", 0));

            res = s.call_super(sub);
            REQUIRE(res == "M4-3");
            sub = s.super_method_strict(sub);
            REQUIRE(sub == get_cv("M3::meth", 0));

            res = s.call_super(sub);
            REQUIRE(res == "M4-1");
            sub = s.super_method_strict(sub);
            REQUIRE(sub == get_cv("M1::meth", 0));

            REQUIRE(!s.super_method(sub));
            REQUIRE_THROWS(s.super_method_strict(sub));
            REQUIRE_THROWS(s.call_super<void>(sub));

            s.call<void>("disable_c3");
        }

        SECTION("super_maybe") {
            res = s.call_super_maybe(sub);
            REQUIRE(res == "M4-2");
            sub = s.super_method(sub);

            res = s.call_super_maybe(sub);
            REQUIRE(res == "M4-1");
            sub = s.super_method(sub);

            res = s.call_super_maybe(sub);
            REQUIRE(!res);
        }
    }

    SECTION("add_const_sub") {
        Stash st(vars.stash);
        REQUIRE(!st.fetch("MYCONST"));

        SECTION("scalar") {
            Simple v(123);
            st.add_const_sub("MYCONST", v);
            REQUIRE(v.use_count() == 2);
            REQUIRE(st.fetch("MYCONST"));
            auto s = st["MYCONST"].sub();
            REQUIRE(s);
            Simple v2 = s.call();
            REQUIRE(v2 == v.get());
        }

        if (PERL_VERSION >= 20) { // in older perls values can only be scalars
            SECTION("array") {
                Array v({ Simple(1), Simple(2), Simple(3) });
                st.add_const_sub("MYCONST", v);
                REQUIRE(v.use_count() == 2);
                auto res = st["MYCONST"].sub().call<List>();
                REQUIRE(res.size() == v.size());
                REQUIRE(res != v);
                REQUIRE(res[0] == v[0]);
                REQUIRE(res[1] == v[1]);
                REQUIRE(res[2] == v[2]);
            }
        }

        st.erase("MYCONST");
        REQUIRE(!st.fetch("MYCONST"));
    }
}