/*
* Author:          Patrick-Christopher Mattulat
* Company:         Lynar Studios
* E-Mail:          webmaster@lynarstudios.com
* Created:         2023-04-07
* Changed:         2023-04-11
*
* */

#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <ls-std-core-jni-test.hpp>
#include <ls-std/ls-std-core-jni.hpp>
#include <ls-std/ls-std-core.hpp>
#include <memory>

using ls::std::core::IllegalArgumentException;
using ls::std::core::NullPointerException;
using ls::std::core::experimental::JniClass;
using ls::std::core::experimental::JniClassParameter;
using std::make_shared;
using std::shared_ptr;
using std::string;
using test::core::jni::MockJniApi;
using testing::AtLeast;
using testing::Return;
using testing::Test;

namespace
{
  class JniClassTest : public Test
  {
    public:

      JniClassTest() = default;
      ~JniClassTest() override = default;

      JniClass createJniClass(const string &_classPath)
      {
        shared_ptr<JniClassParameter> parameter = make_shared<JniClassParameter>();
        this->jniApi = make_shared<MockJniApi>();
        parameter->setJniApi(this->jniApi);
        shared_ptr<JNIEnv> environment = make_shared<JNIEnv>();
        parameter->setJavaEnvironment(environment.get());

        return JniClass{parameter, _classPath};
      }

      shared_ptr<MockJniApi> jniApi{};
  };

  TEST_F(JniClassTest, constructor_no_parameter_reference)
  {
    EXPECT_THROW(
        {
          try
          {
            JniClass javaClass(nullptr, "java.utils.String");
          }
          catch (const IllegalArgumentException &_exception)
          {
            string expected = _exception.getName() + " thrown - no provided reference to JNI class parameter!";
            string actual = _exception.what();

            ASSERT_STREQ(expected.c_str(), actual.c_str());
            throw;
          }
        },
        IllegalArgumentException);
  }

  TEST_F(JniClassTest, constructor_empty_path)
  {
    EXPECT_THROW(
        {
          try
          {
            JniClass javaClass(make_shared<JniClassParameter>(), "");
          }
          catch (const IllegalArgumentException &_exception)
          {
            string expected = _exception.getName() + " thrown - path to associated Java class is empty!";
            string actual = _exception.what();

            ASSERT_STREQ(expected.c_str(), actual.c_str());
            throw;
          }
        },
        IllegalArgumentException);
  }

  TEST_F(JniClassTest, constructor_no_java_environment_reference)
  {
    EXPECT_THROW(
        {
          try
          {
            JniClass javaClass(make_shared<JniClassParameter>(), "java.utils.String");
          }
          catch (const IllegalArgumentException &_exception)
          {
            string expected = _exception.getName() + " thrown - Java environment is not being provided!";
            string actual = _exception.what();

            ASSERT_STREQ(expected.c_str(), actual.c_str());
            throw;
          }
        },
        IllegalArgumentException);
  }

  TEST_F(JniClassTest, callMethod_boolean_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "isBlue";
    string methodSignature = "()Z";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callBooleanMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callBooleanMethod(testing::_, methodId)).WillByDefault(Return(true));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_TRUE(javaClass.callMethod(methodIdentifier).getBooleanValue());
  }

  TEST_F(JniClassTest, callMethod_byte_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getDay";
    string methodSignature = "()B";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callByteMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callByteMethod(testing::_, methodId)).WillByDefault(Return(22));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_EQ(22, javaClass.callMethod(methodIdentifier).getByteValue());
  }

  TEST_F(JniClassTest, callMethod_char_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getLetter";
    string methodSignature = "()C";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callCharMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callCharMethod(testing::_, methodId)).WillByDefault(Return('P'));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_EQ('P', javaClass.callMethod(methodIdentifier).getCharValue());
  }

  TEST_F(JniClassTest, callMethod_double_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getPi";
    string methodSignature = "()D";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callDoubleMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callDoubleMethod(testing::_, methodId)).WillByDefault(Return(3.14159265l));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_DOUBLE_EQ(3.14159265l, javaClass.callMethod(methodIdentifier).getDoubleValue());
  }

  TEST_F(JniClassTest, callMethod_float_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getPi";
    string methodSignature = "()F";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callFloatMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callFloatMethod(testing::_, methodId)).WillByDefault(Return(3.14159f));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_FLOAT_EQ(3.14159f, javaClass.callMethod(methodIdentifier).getFloatValue());
  }

  TEST_F(JniClassTest, callMethod_integer_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getYear";
    string methodSignature = "()I";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callIntMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callIntMethod(testing::_, methodId)).WillByDefault(Return(1989));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_EQ(1989, javaClass.callMethod(methodIdentifier).getIntegerValue());
  }

  TEST_F(JniClassTest, callMethod_long_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getAmountOfMoney";
    string methodSignature = "()J";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callLongMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callLongMethod(testing::_, methodId)).WillByDefault(Return(18837828981));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_EQ(18837828981, javaClass.callMethod(methodIdentifier).getLongValue());
  }

  TEST_F(JniClassTest, callMethod_short_return_value)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getDay";
    string methodSignature = "()S";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    EXPECT_CALL(*this->jniApi, callShortMethod(testing::_, methodId)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, callShortMethod(testing::_, methodId)).WillByDefault(Return(55));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_EQ(55, javaClass.callMethod(methodIdentifier).getShortValue());
  }

  TEST_F(JniClassTest, hasMethod)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    ASSERT_FALSE(javaClass.hasMethod("getDay"));
  }

  TEST_F(JniClassTest, load)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    ASSERT_TRUE(javaClass.load());
  }

  TEST_F(JniClassTest, loadMethod)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getDay";
    string methodSignature = "()B";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
  }

  TEST_F(JniClassTest, loadMethod_repeat)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    EXPECT_CALL(*this->jniApi, findClass(classPath)).Times(AtLeast(1));
    ON_CALL(*this->jniApi, findClass(classPath)).WillByDefault(Return(make_shared<_jclass>().get()));

    string methodIdentifier = "getDay";
    string methodSignature = "()B";
    EXPECT_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).Times(AtLeast(1));
    jmethodID methodId = (jmethodID) make_shared<int>().get();
    ON_CALL(*this->jniApi, getMethodId(testing::_, methodIdentifier.c_str(), methodSignature.c_str())).WillByDefault(Return(methodId));

    ASSERT_TRUE(javaClass.load());
    ASSERT_TRUE(javaClass.loadMethod(methodIdentifier, methodSignature));
    ASSERT_FALSE(javaClass.loadMethod(methodIdentifier, methodSignature));
  }

  TEST_F(JniClassTest, loadMethod_without_previous_class_load)
  {
    string classPath = "java.utils.String";
    JniClass javaClass = this->createJniClass(classPath);

    string methodIdentifier = "getDay";
    string methodSignature = "()B";

    EXPECT_THROW(
        {
          try
          {
            javaClass.loadMethod(methodIdentifier, methodSignature);
          }
          catch (const NullPointerException &_exception)
          {
            string expected = _exception.getName() + " thrown - no Java class reference available for loading class method!";
            string actual = _exception.what();

            ASSERT_STREQ(expected.c_str(), actual.c_str());
            throw;
          }
        },
        NullPointerException);
  }
}