#include <catch.hpp>
#include <leatherman/ruby/api.hpp>
#include <limits>

using namespace std;
using namespace leatherman::ruby;

TEST_CASE("api::eval", "[ruby-api]") {
    SECTION("can load api and evaluate ruby code") {
        auto& ruby = api::instance();
        ruby.initialize();
        REQUIRE(ruby.initialized());

        REQUIRE(ruby.get_load_path().size() > 0u);

        REQUIRE(ruby.to_string(ruby.eval("'foo'")) == "foo");
    }
}

TEST_CASE("api::is_*", "[ruby-api]") {
    auto& ruby = api::instance();
    ruby.initialize();
    REQUIRE(ruby.initialized());

    SECTION("can correctly identify nil values") {
        REQUIRE(ruby.is_nil(ruby.nil_value()));
        REQUIRE_FALSE(ruby.is_nil(ruby.true_value()));
    }

    SECTION("can correctly identify true and false values") {
        REQUIRE(ruby.is_true(ruby.true_value()));
        REQUIRE_FALSE(ruby.is_true(ruby.false_value()));
        REQUIRE(ruby.is_false(ruby.false_value()));
        REQUIRE_FALSE(ruby.is_false(ruby.true_value()));
    }

    SECTION("can correctly identify strings") {
        REQUIRE(ruby.is_string(ruby.utf8_value("'I'm a string'")));
        REQUIRE_FALSE(ruby.is_string(ruby.true_value()));
    }

    SECTION("can correctly identify symbols") {
        REQUIRE(ruby.is_symbol(ruby.to_symbol("mysymbol")));
        REQUIRE_FALSE(ruby.is_symbol(ruby.false_value()));
    }

    SECTION("can correctly identify numbers") {
        REQUIRE(ruby.is_float(ruby.eval("1.5")));
        REQUIRE_FALSE(ruby.is_float(ruby.utf8_value("foo")));

        REQUIRE(ruby.is_integer(ruby.eval("2")));
        REQUIRE_FALSE(ruby.is_integer(ruby.eval("1.5")));
    }

    SECTION("can correctly identify hashes") {
        REQUIRE(ruby.is_hash(ruby.eval("{ 'red' => 2 }")));
        REQUIRE_FALSE(ruby.is_hash(ruby.utf8_value("foo")));
    }

    SECTION("can correctly identify type") {
        REQUIRE(ruby.is_a(ruby.eval("1"), ruby.eval("Integer")));
        REQUIRE_FALSE(ruby.is_a(ruby.eval("'1'"), ruby.eval("Integer")));
    }

    SECTION("can correctly identify arrays") {
        REQUIRE(ruby.is_array(ruby.eval("[1, 2, 3]")));
        REQUIRE_FALSE(ruby.is_array(ruby.false_value()));
    }
}

TEST_CASE("api::equals", "[ruby-api]") {
    auto& ruby = api::instance();
    ruby.initialize();
    REQUIRE(ruby.initialized());
    SECTION("can correctly test boolean values for equality") {
        REQUIRE(ruby.equals(ruby.true_value(), ruby.true_value()));
        REQUIRE_FALSE(ruby.equals(ruby.true_value(), ruby.false_value()));
    }

    SECTION("can correctly test strings for equality") {
        REQUIRE(ruby.equals(ruby.utf8_value("foo"), ruby.utf8_value("foo")));
        REQUIRE_FALSE(ruby.equals(ruby.utf8_value("foo"), ruby.utf8_value("bar")));
    }

    SECTION("can correctly test numbers for equality") {
        REQUIRE(ruby.equals(ruby.eval("1"), ruby.eval("1")));
        REQUIRE_FALSE(ruby.equals(ruby.eval("1"), ruby.eval("3")));
        REQUIRE(ruby.equals(ruby.eval("1.5"), ruby.eval("1.5")));
        REQUIRE_FALSE(ruby.equals(ruby.eval("1.5"), ruby.eval("1")));
    }

    SECTION("can correctly test Ruby hashes for equality") {
        REQUIRE(ruby.equals(ruby.eval("{ 'red' => 'blue' }"), ruby.eval("{ 'red' => 'blue' }")));
        REQUIRE_FALSE(ruby.equals(ruby.eval("{ 'red' => 'blue' }"), ruby.eval("{ 'red' => 'green' }")));
    }

    SECTION("can correctly test symbols for equality") {
        REQUIRE(ruby.equals(ruby.to_symbol("mysymbol"), ruby.eval(":mysymbol")));
        REQUIRE_FALSE(ruby.equals(ruby.to_symbol("mysymbol"), ruby.to_symbol("notmysymbol")));
    }
}

TEST_CASE("api::case_equals", "[ruby-api]") {
    auto& ruby = api::instance();
    ruby.initialize();
    REQUIRE(ruby.initialized());

    SECTION("can detect class membership") {
        REQUIRE(ruby.case_equals(ruby.eval("Integer"), ruby.eval("1")));
        REQUIRE_FALSE(ruby.case_equals(ruby.eval("String"), ruby.eval("4")));
    }
}

VALUE test_func(VALUE self) {
    auto& ruby = api::instance();
    ruby.initialize();
    return ruby.utf8_value("test function");
}

TEST_CASE("api::rb_define_singleton_method", "[ruby-api]") {
    SECTION("can define a new module with a new method") {
        auto& ruby = api::instance();
        ruby.initialize();
        REQUIRE(ruby.initialized());

        auto module = ruby.rb_define_module("Test");
        REQUIRE(module);
        ruby.rb_define_singleton_method(module, "test_func", RUBY_METHOD_FUNC(test_func), 0);
        REQUIRE(ruby.to_string(ruby.eval("Test.test_func")) == "test function");
    }
}

TEST_CASE("api::exception_to_string", "[ruby-api]") {
    auto& ruby = api::instance();
    ruby.initialize();
    REQUIRE(ruby.initialized());

    SECTION("can print exception details") {
        try {
            ruby.eval("raise 'test_exception'");
        } catch (runtime_error exc) {
            REQUIRE(string(exc.what()) == "test_exception");
        }
    }

    SECTION("can print exception details with stack trace") {
        ruby.include_stack_trace(true);
        try {
            ruby.eval("raise 'test_exception'");
        } catch (runtime_error exc) {
            REQUIRE(string(exc.what()).find("backtrace") != string::npos);
        }
    }
}

TEST_CASE("api::lookup", "[ruby-api]") {
    auto& ruby = api::instance();
    ruby.initialize();
    REQUIRE(ruby.initialized());

    SECTION("can find module by name") {
        auto foo_module = ruby.rb_define_module("Foo");
        ruby.rb_define_module_under(foo_module, "Bar");
        REQUIRE(ruby.to_string(ruby.lookup({ "Foo", "Bar" })) == "Foo::Bar");
    }
}

TEST_CASE("api::to_string", "[ruby-api]") {
    auto& ruby = api::instance();
    ruby.initialize();
    REQUIRE(ruby.initialized());

    SECTION("can normalize encodings") {
        string john {"J\xc3\xb6hn"};
        auto obj = ruby.utf8_value(john);
        auto encoded = ruby.rb_funcall(obj, ruby.rb_intern("encode"), 1, ruby.utf8_value("Windows-1252"));
        REQUIRE(ruby.to_string(encoded) == john);
    }
}

TEST_CASE("api::num2size_t", "[ruby-api]") {
    auto& ruby = api::instance();
    ruby.initialize();
    REQUIRE(ruby.initialized());

    SECTION("can convert Ruby number to size_t") {
        auto fixednum = ruby.eval("1");
        auto num = ruby.num2size_t(fixednum);
        REQUIRE(1u == num);
    }

    SECTION("can convert large Ruby number to size_t") {
        auto expected = numeric_limits<size_t>::max();
        auto largenum = ruby.eval(to_string(expected));
        auto num = ruby.num2size_t(largenum);
        REQUIRE(expected == num);
    }

#if 0
    // Can't use this test yet, because Ruby SIGSEGVs on calling rb_num2ull.
    SECTION("throws exception on Ruby numbers exceeding size_t") {
        auto largenum = ruby.eval("184467440737095516150");
        REQUIRE_THROWS_AS(ruby.num2size_t(largenum), runtime_error);
    }
#endif
}