/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

#include "browser_element.h"
#include "browser_error.h"
#include "duktape.h"
#include "embed.h"
#include "webdriver.h"
#include "zbxalgo.h"
#include "zbxcommon.h"
#include "zbxembed.h"
#include "zbxtypes.h"

#ifdef HAVE_LIBCURL

/******************************************************************************
 *                                                                            *
 * Purpose: return backing C structure embedded in element object             *
 *                                                                            *
 ******************************************************************************/
static zbx_wd_element_t *wd_element(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	zbx_es_env_t		*env;
	void			*objptr;

	if (NULL == (env = zbx_es_get_env(ctx)))
	{
		(void)duk_push_error_object(ctx, DUK_RET_EVAL_ERROR, "cannot access internal environment");

		return NULL;
	}

	duk_push_this(ctx);
	objptr = duk_require_heapptr(ctx, -1);
	duk_pop(ctx);

	if (NULL == (el = (zbx_wd_element_t *)es_obj_get_data(env, objptr, ES_OBJ_ELEMENT)))
		(void)duk_push_error_object(ctx, DUK_RET_EVAL_ERROR, "cannot find native data attached to object");

	return el;
}

void	wd_element_free(zbx_wd_element_t *el)
{
	webdriver_release(el->wd);
	zbx_free(el->id);
	zbx_free(el);
}

/******************************************************************************
 *                                                                            *
 * Purpose: element destructor                                                *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_dtor(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	zbx_es_env_t		*env;

	zabbix_log(LOG_LEVEL_TRACE, "Element::~Element()");

	if (NULL == (env = zbx_es_get_env(ctx)))
		return duk_error(ctx, DUK_RET_EVAL_ERROR, "cannot access internal environment");

	if (NULL != (el = (zbx_wd_element_t *)es_obj_detach_data(env, duk_require_heapptr(ctx, -1), ES_OBJ_ELEMENT)))
		wd_element_free(el);

	return 0;
}

/******************************************************************************
 *                                                                            *
 * Purpose: element constructor                                               *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_ctor(duk_context *ctx, zbx_webdriver_t *wd, const char *elementid)
{
	zbx_wd_element_t	*el;
	zbx_es_env_t		*env;

	zabbix_log(LOG_LEVEL_TRACE, "Element::Element()");

	if (NULL == (env = zbx_es_get_env(ctx)))
		return duk_error(ctx, DUK_RET_TYPE_ERROR, "cannot access internal environment");

	el = (zbx_wd_element_t *)zbx_malloc(NULL, sizeof(zbx_wd_element_t));
	el->wd = webdriver_addref(wd);
	el->id = zbx_strdup(NULL, elementid);

	duk_push_object(ctx);
	es_obj_attach_data(env, duk_require_heapptr(ctx, -1), el, ES_OBJ_ELEMENT);

	duk_push_string(ctx, "browser");
	duk_push_heapptr(ctx, wd->browser);
	duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_CLEAR_WRITABLE | DUK_DEFPROP_HAVE_ENUMERABLE |
			DUK_DEFPROP_HAVE_CONFIGURABLE);

	duk_push_c_function(ctx, wd_element_dtor, 1);
	duk_set_finalizer(ctx, -2);

	return 0;
}

/******************************************************************************
 *                                                                            *
 * Purpose: input keys into element                                           *
 *                                                                            *
 * Stack: 0 - keys to send (string)                                           *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_send_keys(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	char			*error = NULL, *keys = NULL;
	int			err_index = -1;
	const char		*keys_cesu = NULL;

	if (!duk_is_null(ctx, 0) && !duk_is_undefined(ctx, 0))
		keys_cesu = duk_safe_to_string(ctx, 0);

	if (NULL == (el = wd_element(ctx)))
		return duk_throw(ctx);

	if (NULL == keys_cesu)
	{
		err_index = browser_push_error(ctx,  el->wd, "missing keys parameter");

		goto out;
	}

	if (SUCCEED != es_duktape_string_decode(keys_cesu, &keys))
	{
		err_index = browser_push_error(ctx,  el->wd, "cannot convert keys parameter to utf8");

		goto out;
	}

	if (SUCCEED != webdriver_send_keys_to_element(el->wd, el->id, keys, &error))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot send keys to element: %s", error);
		zbx_free(error);
	}
out:
	zbx_free(keys);

	if (-1 != err_index)
		return duk_throw(ctx);

	return 0;
}

/******************************************************************************
 *                                                                            *
 * Purpose: click on element                                                  *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_click(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	char			*error = NULL;
	int			err_index = -1;

	if (NULL == (el = wd_element(ctx)))
		return duk_throw(ctx);

	if (SUCCEED != webdriver_click_element(el->wd, el->id, &error))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot click element: %s", error);
		zbx_free(error);
	}

	if (-1 != err_index)
		return duk_throw(ctx);

	return 0;
}

/******************************************************************************
 *                                                                            *
 * Purpose: clear element                                                     *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_clear(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	char			*error = NULL;
	int			err_index = -1;

	if (NULL == (el = wd_element(ctx)))
		return duk_throw(ctx);

	if (SUCCEED != webdriver_clear_element(el->wd, el->id, &error))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot clear element: %s", error);
		zbx_free(error);
	}

	if (-1 != err_index)
		return duk_throw(ctx);

	return 0;
}

/******************************************************************************
 *                                                                            *
 * Purpose: get element attribute value                                       *
 *                                                                            *
 * Stack: 0 - attribute name (string)                                         *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_get_attribute(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	char			*error = NULL, *name = NULL, *value = NULL;
	int			err_index = -1;
	const char		*name_cesu = NULL;

	if (!duk_is_null(ctx, 0) && !duk_is_undefined(ctx, 0))
		name_cesu = duk_safe_to_string(ctx, 0);

	if (NULL == (el = wd_element(ctx)))
		return duk_throw(ctx);

	if (NULL == name_cesu)
	{
		err_index = browser_push_error(ctx,  el->wd, "missing name parameter");

		goto out;
	}

	if (SUCCEED != es_duktape_string_decode(name_cesu, &name))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot convert name parameter to utf8");

		goto out;
	}

	if (SUCCEED != webdriver_get_element_info(el->wd, el->id, "attribute", name, &value, &error))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot get element attribute: %s", error);
		zbx_free(error);

		goto out;
	}

	es_push_result_string(ctx, value, strlen(value));
out:
	zbx_free(value);
	zbx_free(name);

	if (-1 != err_index)
		return duk_throw(ctx);

	return 1;
}

/******************************************************************************
 *                                                                            *
 * Purpose: get element property value                                        *
 *                                                                            *
 * Stack: 0 - property value (string)                                         *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_get_property(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	char			*error = NULL, *name = NULL, *value = NULL;
	int			err_index = -1;
	const char		*name_cesu = NULL;

	if (!duk_is_null(ctx, 0) && !duk_is_undefined(ctx, 0))
		name_cesu = duk_safe_to_string(ctx, 0);

	if (NULL == (el = wd_element(ctx)))
		return duk_throw(ctx);

	if (NULL == name_cesu)
	{
		err_index = browser_push_error(ctx,  el->wd, "missing name parameter");

		goto out;
	}

	if (SUCCEED != es_duktape_string_decode(name_cesu, &name))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot convert name parameter to utf8");

		goto out;
	}

	if (SUCCEED != webdriver_get_element_info(el->wd, el->id, "property", name, &value, &error))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot get element property: %s", error);
		zbx_free(error);

		goto out;
	}

	es_push_result_string(ctx, value, strlen(value));
out:
	zbx_free(value);
	zbx_free(name);

	if (-1 != err_index)
		return duk_throw(ctx);

	return 1;
}

/******************************************************************************
 *                                                                            *
 * Purpose: get element text                                                  *
 *                                                                            *
 ******************************************************************************/
static duk_ret_t	wd_element_get_text(duk_context *ctx)
{
	zbx_wd_element_t	*el;
	char			*error = NULL, *value = NULL;
	int			err_index = -1;

	if (NULL == (el = wd_element(ctx)))
		return duk_throw(ctx);

	if (SUCCEED != webdriver_get_element_info(el->wd, el->id, "text", NULL, &value, &error))
	{
		err_index = browser_push_error(ctx, el->wd, "cannot get element text: %s", error);
		zbx_free(error);

		goto out;
	}

	es_push_result_string(ctx, value, strlen(value));
out:
	zbx_free(value);

	if (-1 != err_index)
		return duk_throw(ctx);

	return 1;
}

static const duk_function_list_entry	element_methods[] = {
	{"sendKeys", wd_element_send_keys, 1},
	{"click", wd_element_click, 0},
	{"clear", wd_element_clear, 0},
	{"getAttribute", wd_element_get_attribute, 1},
	{"getProperty", wd_element_get_property, 1},
	{"getText", wd_element_get_text, 0},
	{0}
};

/******************************************************************************
 *                                                                            *
 * Purpose: create element and push it on stack                               *
 *                                                                            *
 * Parameters: ctx       - [IN] duktape context                               *
 *             wd        - [IN] webdriver object                              *
 *             elementid - [IN] element identifier returned by webdriver      *
 *                              find element(s) requests                      *
 *                                                                            *
 ******************************************************************************/
void	wd_element_create(duk_context *ctx, zbx_webdriver_t *wd, const char *elementid)
{
	wd_element_ctor(ctx, wd, elementid);
	duk_put_function_list(ctx, -1, element_methods);
}

/******************************************************************************
 *                                                                            *
 * Purpose: create element array and push it on stack                         *
 *                                                                            *
 * Parameters: ctx      - [IN] duktape context                                *
 *             wd       - [IN] webdriver object                               *
 *             elements - [IN] vector with element identifiers returned       *
 *                             by webdriver find elements request             *
 *                                                                            *
 ******************************************************************************/
void	wd_element_create_array(duk_context *ctx, zbx_webdriver_t *wd, const zbx_vector_str_t *elements)
{
	duk_idx_t	arr;

	arr = duk_push_array(ctx);

	for (int i = 0; i < elements->values_num; i++)
	{
		wd_element_create(ctx, wd, elements->values[i]);
		duk_put_prop_index(ctx, arr, (duk_uarridx_t)i);
	}
}

/******************************************************************************
 *                                                                            *
 * Purpose: get element id                                                    *
 *                                                                            *
 ******************************************************************************/
const char	*wd_element_get_id(void *wd)
{
	return ((zbx_wd_element_t *)wd)->id;
}

#endif