FilterChangeRequestListTask

class FilterAssetListTask(FilterListTask):
    config_path = FILTER_ASSET_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_asset"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_ASSET_LIST_COLUMNS_PATH,
            **kwargs,
        )

class FilterChangeRequestListTask(FilterListTask):
    config_path = FILTER_CHANGE_REQUEST_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["change_request"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
            **kwargs,
        )

class FilterHardwareListTask(FilterListTask):
    config_path = FILTER_HARDWARE_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_hardware"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
            **kwargs,
        )


FilterHardwareListTask

class FilterChangeRequestListTask(FilterListTask):
    config_path = FILTER_CHANGE_REQUEST_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["change_request"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
            **kwargs,
        )

class FilterHardwareListTask(FilterListTask):
    config_path = FILTER_HARDWARE_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_hardware"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
            **kwargs,
        )

class FilterIncidentListTask(FilterListTask):
    config_path = FILTER_INCIDENT_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["incident"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
            **kwargs,
        )


FilterIncidentListTask

class FilterHardwareListTask(FilterListTask):
    config_path = FILTER_HARDWARE_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_hardware"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
            **kwargs,
        )

class FilterIncidentListTask(FilterListTask):
    config_path = FILTER_INCIDENT_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["incident"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
            **kwargs,
        )

class FilterProblemListForWorkLoadBalancingTask(FilterListTask, CompositionalBuildingBlockTask):
    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url="/now/nav/ui/classic/params/target/problem_list.do",
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_PROBLEM_COLUMNS_PATH,
            **kwargs,
        )

    def get_pretty_printed_description(self, goal=False) -> str:


class FilterIncidentListTask(FilterListTask):
    config_path = FILTER_INCIDENT_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["incident"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
            **kwargs,
        )

class FilterProblemListForWorkLoadBalancingTask(FilterListTask, CompositionalBuildingBlockTask):
    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url="/now/nav/ui/classic/params/target/problem_list.do",
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_PROBLEM_COLUMNS_PATH,
            **kwargs,
        )

    def get_pretty_printed_description(self, goal=False) -> str:
        """Override the parent method to provide a more detailed description of the task"""

        return self.goal

class FilterServiceCatalogItemListTask(FilterListTask):
    config_path = FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sc_cat_item"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
            **kwargs,
        )


FilterServiceCatalogItemListTask

        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url="/now/nav/ui/classic/params/target/problem_list.do",
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_PROBLEM_COLUMNS_PATH,
            **kwargs,
        )

    def get_pretty_printed_description(self, goal=False) -> str:
        """Override the parent method to provide a more detailed description of the task"""

        return self.goal

class FilterServiceCatalogItemListTask(FilterListTask):
    config_path = FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sc_cat_item"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
            **kwargs,
        )

class FilterUserListTask(FilterListTask):
    config_path = FILTER_USER_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sys_user"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
            **kwargs,
        )


FilterUserListTask

class FilterServiceCatalogItemListTask(FilterListTask):
    config_path = FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sc_cat_item"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
            **kwargs,
        )

class FilterUserListTask(FilterListTask):
    config_path = FILTER_USER_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sys_user"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
            **kwargs,
        )

class SortAssetListTask(SortListTask):
    config_path = SORT_ASSET_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_asset"]["url"],
            forbidden_fields=LISTS["alm_asset"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_ASSET_LIST_COLUMNS_PATH,
            **kwargs,


SortAssetListTask

class FilterUserListTask(FilterListTask):
    config_path = FILTER_USER_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sys_user"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
            **kwargs,
        )

class SortAssetListTask(SortListTask):
    config_path = SORT_ASSET_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_asset"]["url"],
            forbidden_fields=LISTS["alm_asset"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_ASSET_LIST_COLUMNS_PATH,
            **kwargs,
        )

class SortChangeRequestListTask(SortListTask):
    config_path = SORT_CHANGE_REQUEST_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["change_request"]["url"],
            forbidden_fields=LISTS["change_request"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
            **kwargs,


SortChangeRequestListTask

    config_path = SORT_ASSET_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_asset"]["url"],
            forbidden_fields=LISTS["alm_asset"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_ASSET_LIST_COLUMNS_PATH,
            **kwargs,
        )

class SortChangeRequestListTask(SortListTask):
    config_path = SORT_CHANGE_REQUEST_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["change_request"]["url"],
            forbidden_fields=LISTS["change_request"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
            **kwargs,
        )

class SortHardwareListTask(SortListTask):
    config_path = SORT_HARDWARE_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_hardware"]["url"],
            forbidden_fields=LISTS["alm_hardware"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
            **kwargs,


SortHardwareListTask

    config_path = SORT_CHANGE_REQUEST_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["change_request"]["url"],
            forbidden_fields=LISTS["change_request"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_CHANGE_REQUEST_COLUMNS_PATH,
            **kwargs,
        )

class SortHardwareListTask(SortListTask):
    config_path = SORT_HARDWARE_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_hardware"]["url"],
            forbidden_fields=LISTS["alm_hardware"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
            **kwargs,
        )

class SortIncidentListTask(SortListTask):
    config_path = SORT_INCIDENT_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["incident"]["url"],
            forbidden_fields=LISTS["incident"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
            **kwargs,


SortIncidentListTask

    config_path = SORT_HARDWARE_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_hardware"]["url"],
            forbidden_fields=LISTS["alm_hardware"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_HARDWARE_COLUMNS_PATH,
            **kwargs,
        )

class SortIncidentListTask(SortListTask):
    config_path = SORT_INCIDENT_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["incident"]["url"],
            forbidden_fields=LISTS["incident"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
            **kwargs,
        )

class SortServiceCatalogItemListTask(SortListTask):
    config_path = SORT_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sc_cat_item"]["url"],
            forbidden_fields=LISTS["sc_cat_item"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
            **kwargs,


SortServiceCatalogItemListTask

    config_path = SORT_INCIDENT_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["incident"]["url"],
            forbidden_fields=LISTS["incident"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_INCIDENT_COLUMNS_PATH,
            **kwargs,
        )

class SortServiceCatalogItemListTask(SortListTask):
    config_path = SORT_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sc_cat_item"]["url"],
            forbidden_fields=LISTS["sc_cat_item"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
            **kwargs,
        )

class SortUserListTask(SortListTask):
    config_path = SORT_USER_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sys_user"]["url"],
            forbidden_fields=LISTS["sys_user"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
            **kwargs,


SortUserListTask

    config_path = SORT_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sc_cat_item"]["url"],
            forbidden_fields=LISTS["sc_cat_item"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
            **kwargs,
        )

class SortUserListTask(SortListTask):
    config_path = SORT_USER_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sys_user"]["url"],
            forbidden_fields=LISTS["sys_user"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
            **kwargs,
        )

class ExtractUserListInfoTask(ExtractListInfoTask, CompositionalBuildingBlockTask):
    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        config_path=EXTRACT_USER_LIST_INFO_CONFIG,
        list_name="User",
        unique_field_name="user_name",
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            config_path=config_path,
            list_name=list_name,
            unique_field_name=unique_field_name,


ExtractUserListInfoTask

    config_path = SORT_USER_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sys_user"]["url"],
            forbidden_fields=LISTS["sys_user"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
            **kwargs,
        )

class ExtractUserListInfoTask(ExtractListInfoTask, CompositionalBuildingBlockTask):
    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        config_path=EXTRACT_USER_LIST_INFO_CONFIG,
        list_name="User",
        unique_field_name="user_name",
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            config_path=config_path,
            list_name=list_name,
            unique_field_name=unique_field_name,
            table_name="sys_user",
            **kwargs,
        )

# Register all tasks
__TASKS__ = (
    [
        value
        for name, value in locals().items()
        if re.compile(r"^Filter\w+ListTask$").match(name)
        and not issubclass(value, CompositionalBuildingBlockTask)
    ]
    + [
        value
        for name, value in locals().items()
        if re.compile(r"^Sort\w+ListTask$").match(name)
        and not issubclass(value, CompositionalBuildingBlockTask)
    ]
    + [
        value
        for name, value in locals().items()
        if re.compile(r"^Extract\w+ListInfoTask$").match(name)


        "expected_values": [
            {
                "user_name": "lucius.bagnoli",
                "email": "lucius.bagnoli@example.com",
                "first_name": "Lucius",
                "last_name": "Bagnoli",
            }
        ],
    }
]

class ServiceNowListTask(AbstractServiceNowTask):
    OPERATOR_EQUALS = "="
    OPERATOR_NOT_EQUALS = "!="
    OPERATOR_STARTSWITH = "STARTSWITH"
    OPERATOR_ISEMPTY = "ISEMPTY"
    OPERATOR_EMPTYSTRING = "EMPTYSTRING"

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + ["registerGsftMainLoaded();"]

    def _get_visible_list(self, page: Page):
        self._wait_for_ready(page)

        iframe = page.frame("gsft_main")
        lst = iframe.locator("table.data_list_table")
        lst.wait_for()

        # Validate the number of lists on the page
        if lst.count() > 1:
            warn(
                "More than one list found on page. Using the first one.",
                category=RuntimeWarning,
            )
        elif lst.count() == 0:
            raise RuntimeError("No list found on page.")
        lst = lst.nth(0)


                "first_name": "Lucius",
                "last_name": "Bagnoli",
            }
        ],
    }
]

class ServiceNowListTask(AbstractServiceNowTask):
    OPERATOR_EQUALS = "="
    OPERATOR_NOT_EQUALS = "!="
    OPERATOR_STARTSWITH = "STARTSWITH"
    OPERATOR_ISEMPTY = "ISEMPTY"
    OPERATOR_EMPTYSTRING = "EMPTYSTRING"

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + ["registerGsftMainLoaded();"]

    def _get_visible_list(self, page: Page):
        self._wait_for_ready(page)

        iframe = page.frame("gsft_main")
        lst = iframe.locator("table.data_list_table")
        lst.wait_for()

        # Validate the number of lists on the page
        if lst.count() > 1:
            warn(
                "More than one list found on page. Using the first one.",
                category=RuntimeWarning,
            )
        elif lst.count() == 0:
            raise RuntimeError("No list found on page.")
        lst = lst.nth(0)

        # A javascript command that gets the list object
        javascript_selector = f"gsft_main.GlideList2.get('{lst.get_attribute('data-list_id')}')"


        ],
    }
]

class ServiceNowListTask(AbstractServiceNowTask):
    OPERATOR_EQUALS = "="
    OPERATOR_NOT_EQUALS = "!="
    OPERATOR_STARTSWITH = "STARTSWITH"
    OPERATOR_ISEMPTY = "ISEMPTY"
    OPERATOR_EMPTYSTRING = "EMPTYSTRING"

    @classmethod
    def all_configs(cls) -> List[dict]:
        with open(cls.config_path, "r") as f:
            return json.load(f)

    def get_init_scripts(self) -> List[str]:
        return super().get_init_scripts() + ["registerGsftMainLoaded();"]

    def _get_visible_list(self, page: Page):
        self._wait_for_ready(page)

        iframe = page.frame("gsft_main")
        lst = iframe.locator("table.data_list_table")
        lst.wait_for()

        # Validate the number of lists on the page
        if lst.count() > 1:
            warn(
                "More than one list found on page. Using the first one.",
                category=RuntimeWarning,
            )
        elif lst.count() == 0:
            raise RuntimeError("No list found on page.")
        lst = lst.nth(0)

        # A javascript command that gets the list object
        javascript_selector = f"gsft_main.GlideList2.get('{lst.get_attribute('data-list_id')}')"

        return iframe, lst, javascript_selector

    @retry(
        stop=stop_after_delay(SNOW_BROWSER_TIMEOUT / 1000),
        retry=retry_if_exception_type(playwright.sync_api.Error),
        reraise=True,
        before_sleep=lambda _: logging.debug("Retrying due to a Playwright Error..."),
    )
    def _extract_list_info(self, page: Page, with_data=False):
        """
        Extract useful information about the list visible on the page

        """
        self._wait_for_ready(page)

        # Grab the list component
        _, _, js_selector = self._get_visible_list(page)

        # Load some basic info
        list_info = {
            "title": page.evaluate(f"{js_selector}.getTitle()").lower(),


        if lst.count() > 1:
            warn(
                "More than one list found on page. Using the first one.",
                category=RuntimeWarning,
            )
        elif lst.count() == 0:
            raise RuntimeError("No list found on page.")
        lst = lst.nth(0)

        # A javascript command that gets the list object
        javascript_selector = f"gsft_main.GlideList2.get('{lst.get_attribute('data-list_id')}')"

        return iframe, lst, javascript_selector

    @retry(
        stop=stop_after_delay(SNOW_BROWSER_TIMEOUT / 1000),
        retry=retry_if_exception_type(playwright.sync_api.Error),
        reraise=True,
        before_sleep=lambda _: logging.debug("Retrying due to a Playwright Error..."),
    )
    def _extract_list_info(self, page: Page, with_data=False):
        """
        Extract useful information about the list visible on the page

        """
        self._wait_for_ready(page)

        # Grab the list component
        _, _, js_selector = self._get_visible_list(page)

        # Load some basic info
        list_info = {
            "title": page.evaluate(f"{js_selector}.getTitle()").lower(),
            "glide_table": page.evaluate(f"{js_selector}.getTableName()").lower(),
            "query": page.evaluate(f"{js_selector}.getQuery()"),
            "fields": page.evaluate(f"{js_selector}.fields"),
            "js_selector": js_selector,
        }

        # Get column info
        list_info["columns"] = table_column_info(
            instance=self.instance,
            table=list_info["glide_table"],
        )

        # Get the list data
        if with_data:
            data = table_api_call(
                instance=self.instance,
                table=list_info["glide_table"],
                params={
                    "sysparm_query": list_info["query"],
                    "sysparm_fields": list_info["fields"],
                    "sysparm_display_value": "all",
                },
            )["result"]
            # Extract all display values (not raw values)
            data = [{k: v["display_value"] for k, v in x.items()} for x in data]
            list_info["data"] = data

        return list_info

    def _wait_for_ready(self, page: Page) -> None:
        """
        Waits for the main iframe to be fully loaded

        """
        logging.debug(f"Waiting for gsft_main to be fully loaded")
        page.wait_for_function(
            "typeof window.gsft_main !== 'undefined' && window.gsft_main.WORKARENA_LOAD_COMPLETE"
        )
        logging.debug("Detected gsft_main ready")

        logging.debug("Waiting for Glide list API to be available")
        page.wait_for_function("window.gsft_main.GlideList2 !== undefined")
        logging.debug("Detected Glide list API ready")

class SortListTask(ServiceNowListTask):
    """
    Sort a list according to a column. Works with any list.


        """
        print_field_names = list(self.fields.values())
        print_field_names.remove(
            self.fields[self.unique_field_name]
        )  # the unique fields are the keys in the dict
        if len(print_field_names) > 1:
            fields_str = (
                '"' + '", "'.join(print_field_names[:-1]) + f'" and "{print_field_names[-1]}"'
            )
            printed_unique_field_name = self.fields[self.unique_field_name]
            task_description = (
                f"- Extract information of field(s) {fields_str} "
                + f'from the "{self.list_name}" list. Return the result as a json where keys are the values of the "{printed_unique_field_name}" field and values are mappings between the fields and the extracted information. Please provide this information in the chat.'
            )
        else:
            fields_str = print_field_names[0]
            task_description = f'- Extract information of field "{fields_str}" from the "{self.list_name}" list. Please provide this information in the chat.'

        return task_description

    def _wait_for_ready(self, page: Page) -> bool:
        """
        Waits for the main iframe to be fully loaded; over-rides the parent method as the cheat
        can be called on a filtered list on which there is no gsft_main.

        Returns True if the gsft_main is present, False otherwise.
        """
        gsft_main_present = False
        logging.debug(f"Waiting up to 3 seconds for gsft_main to be ready")
        try:
            page.wait_for_function(
                "typeof window.gsft_main !== 'undefined' && window.gsft_main.WORKARENA_LOAD_COMPLETE",
                timeout=3000,
            )
            logging.debug("Detected gsft_main ready")
            gsft_main_present = True
        except TimeoutError:
            logging.debug(
                "Timed out waiting for gsft_main to be ready; searching for GlideList API directly"
            )
            pass

        logging.debug("Waiting for Glide list API to be available")
        if gsft_main_present:
            page.wait_for_function("window.gsft_main.GlideList2 !== undefined")
        else:
            page.wait_for_function("window.GlideList2 !== undefined")

        logging.debug("Detected Glide list API ready")

        return gsft_main_present

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
        if not right_url:
            return
        gft_main_present = self._wait_for_ready(page)
        if gft_main_present:
            main_element = page.wait_for_selector("iframe#gsft_main").content_frame()
        else:
            main_element = page

        main_element.wait_for_selector(
            f"#hdr_{self.table_name}"
        )  # Selector for the name of the columns
        # system name mapped to their order in the table
        all_column_elements = main_element.query_selector_all(f"#hdr_{self.table_name} th")
        required_fields_order = {}
        for i, element in enumerate(all_column_elements):
            if element.get_attribute("name") in self.fields:


    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sys_user"]["url"],
            forbidden_fields=LISTS["sys_user"]["forbidden_fields"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_USER_COLUMNS_PATH,
            **kwargs,
        )

class ExtractUserListInfoTask(ExtractListInfoTask, CompositionalBuildingBlockTask):
    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        config_path=EXTRACT_USER_LIST_INFO_CONFIG,
        list_name="User",
        unique_field_name="user_name",
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            fixed_config=fixed_config,
            config_path=config_path,
            list_name=list_name,
            unique_field_name=unique_field_name,
            table_name="sys_user",
            **kwargs,
        )

# Register all tasks
__TASKS__ = (
    [
        value
        for name, value in locals().items()
        if re.compile(r"^Filter\w+ListTask$").match(name)
        and not issubclass(value, CompositionalBuildingBlockTask)
    ]
    + [
        value
        for name, value in locals().items()
        if re.compile(r"^Sort\w+ListTask$").match(name)
        and not issubclass(value, CompositionalBuildingBlockTask)
    ]
    + [
        value
        for name, value in locals().items()
        if re.compile(r"^Extract\w+ListInfoTask$").match(name)


        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        configs: str = "",
        list_name: str = "",
        list_url: str = "",
        unique_field_name: str = "",
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed, instance=instance, start_rel_url=list_url
        )  # For these tasks, the start URL is defined in the setup method, as the URL depends on the configuration
        self.fixed_config = fixed_config
        self.config = None
        self.all_configs = configs
        self.list_name = list_name
        self.table_name = ""
        self.unique_field_name = unique_field_name
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get the task configuration
        config = self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        self.fields = config["fields"]  # mapping between fields and their display names
        self.printed_field_names = {
            v: k for k, v in self.fields.items()
        }  # mapping between fields and their system names
        self.expected_values = config[
            "expected_values"
        ]  # mapping between fields and their expected values
        # This is setup here because the start_url depends on the config
        assert (
            self.unique_field_name in self.fields.keys()
        ), f"Unique field name {self.unique_field_name} not in fields."
        assert all(
            [self.unique_field_name in expected_value for expected_value in self.expected_values]
        ), f"Unique field name {self.unique_field_name} not in expected values."

        if not self.start_url or self.start_url == self.instance.snow_url:
            self.start_rel_url = config["start_rel_url"]
            self.start_url = self.instance.snow_url + self.start_rel_url
        # table_name can be passed in the constructor or extracted from the start_rel_url, located in the config
        if self.table_name is None:
            self.table_name = self.start_rel_url.split("/")[-1].split("_list.do")[0]

        goal = self.get_pretty_printed_description()
        info = {}

        return goal, info

    def start(self, page: Page) -> None:
        super().start(page)
        # TODO: We should add a check to make sure the required columns are present in the list

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks and used as goal in L1 tasks.
        called by subclasses
        """
        print_field_names = list(self.fields.values())
        print_field_names.remove(
            self.fields[self.unique_field_name]
        )  # the unique fields are the keys in the dict
        if len(print_field_names) > 1:
            fields_str = (
                '"' + '", "'.join(print_field_names[:-1]) + f'" and "{print_field_names[-1]}"'
            )
            printed_unique_field_name = self.fields[self.unique_field_name]
            task_description = (


start

        # This is setup here because the start_url depends on the config
        assert (
            self.unique_field_name in self.fields.keys()
        ), f"Unique field name {self.unique_field_name} not in fields."
        assert all(
            [self.unique_field_name in expected_value for expected_value in self.expected_values]
        ), f"Unique field name {self.unique_field_name} not in expected values."

        if not self.start_url or self.start_url == self.instance.snow_url:
            self.start_rel_url = config["start_rel_url"]
            self.start_url = self.instance.snow_url + self.start_rel_url
        # table_name can be passed in the constructor or extracted from the start_rel_url, located in the config
        if self.table_name is None:
            self.table_name = self.start_rel_url.split("/")[-1].split("_list.do")[0]

        goal = self.get_pretty_printed_description()
        info = {}

        return goal, info

    def start(self, page: Page) -> None:
        super().start(page)
        # TODO: We should add a check to make sure the required columns are present in the list

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks and used as goal in L1 tasks.
        called by subclasses
        """
        print_field_names = list(self.fields.values())
        print_field_names.remove(
            self.fields[self.unique_field_name]
        )  # the unique fields are the keys in the dict
        if len(print_field_names) > 1:
            fields_str = (
                '"' + '", "'.join(print_field_names[:-1]) + f'" and "{print_field_names[-1]}"'
            )
            printed_unique_field_name = self.fields[self.unique_field_name]
            task_description = (
                f"- Extract information of field(s) {fields_str} "
                + f'from the "{self.list_name}" list. Return the result as a json where keys are the values of the "{printed_unique_field_name}" field and values are mappings between the fields and the extracted information. Please provide this information in the chat.'
            )


        )

class FilterProblemListForWorkLoadBalancingTask(FilterListTask, CompositionalBuildingBlockTask):
    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url="/now/nav/ui/classic/params/target/problem_list.do",
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_PROBLEM_COLUMNS_PATH,
            **kwargs,
        )

    def get_pretty_printed_description(self, goal=False) -> str:
        """Override the parent method to provide a more detailed description of the task"""

        return self.goal

class FilterServiceCatalogItemListTask(FilterListTask):
    config_path = FILTER_SERVICE_CATALOG_ITEM_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["sc_cat_item"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_SERVICE_CATALOG_COLUMNS_PATH,
            **kwargs,
        )


        # Get the task goal
        goal = self.config["goal"]
        info = {}

        return goal, info

    def start(self, page: Page) -> None:
        super().start(page)
        self._wait_for_ready(page)

        # Ensure that the fields that need to be sorted are visible (task feasibility check)
        self.list_info = self._extract_list_info(page)

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        return self.config["goal"] + "\n"

    def _generate_all_configs(self, seed: int, page: Page, n_fields_to_sort: int):
        self.setup(seed=seed, page=page)
        self._wait_for_ready(page)
        list_info = self._extract_list_info(page)

        # Get available fields
        available_fields = list(list_info["columns"].keys())
        # ... remove forbidden fields
        available_fields = [f for f in available_fields if f not in self.forbidden_fields]

        field_txt = {k: x["label"] for k, x in list_info["columns"].items()}
        dir_txt = {"asc": "ascending", "desc": "descending"}

        # compute all field combinations
        all_sort_fields = list(itertools.combinations(available_fields, n_fields_to_sort))
        # compute all direction combinations
        all_sort_dirs = list(itertools.product(*[["asc", "desc"] for _ in range(n_fields_to_sort)]))

        # product of field combinations x direction combinations
        all_configs = list(itertools.product(all_sort_fields, all_sort_dirs))

        all_configs = [
            {
                "sort_fields": sort_fields,
                "sort_dirs": sort_dirs,
                "goal": f'Sort the "{list_info["title"]}" list by the following fields:\n'
                + "\n".join(
                    [
                        f" - {field_txt[field]} ({dir_txt[dir]})"
                        for field, dir in zip(sort_fields, sort_dirs)
                    ]
                ),
            }
            for sort_fields, sort_dirs in all_configs
        ]

        return all_configs

    def _generate_random_config(self, page: Page):
        self.setup(page=page)
        self._wait_for_ready(page)
        self.list_info = self._extract_list_info(page)
        # Get available fields
        available_fields = list(self.list_info["columns"].keys())
        # ... remove forbidden fields
        available_fields = [f for f in available_fields if f not in self.forbidden_fields]

        sort_len = self.random.randint(
            self.min_sort_len, min(self.max_sort_len, len(available_fields)) + 1
        )

        # Pick random fields and directions to sort
        # Retry until the task is not initially solved
        try_again = True
        while try_again:
            try_again = False


        args:
        goal: bool
            If True, return as the goal of the task (without the starting dash)
        """
        task_info = "" if goal else "- "
        task_info += (
            f"Create a filter for the list to extract all entries where:"
            + f" {'and' if self.filter_kind == 'AND' else 'or'} ".join(
                [
                    f'\n    - "{self.list_info["columns"][col]["label"]}" {filter_operator} "{val}"'
                    for col, filter_operator, val in zip(
                        self.filter_columns, self.filter_operators, self.filter_values
                    )
                ]
            )
        )

        return task_info

    def _generate_random_config(self, page: Page):
        self.setup(page=page)
        self._wait_for_ready(page)

        # Extract the list from the page
        self.list_info = self._extract_list_info(page)

        # Choose the columns to filter on
        allowed_types = ["string", "choice", "reference", "translated_text", "boolean"]
        valid_filter_columns = [
            k for k, v in self.list_info["columns"].items() if v["type"] in allowed_types
        ]
        assert len(valid_filter_columns) > 0, "Not enough columns to filter on."
        self.filter_len = self.random.randint(
            self.min_filter_len, min(self.max_filter_len, len(valid_filter_columns)) + 1
        )
        self.filter_columns = list(
            self.random.choice(valid_filter_columns, self.filter_len, replace=False)
        )

        # Choose the filter kind
        if self.filter_len == 1:
            # If there is only one column to filter on, then the kind is always AND
            self.filter_kind = "AND"
        else:
            self.filter_kind = self.random.choice(["AND", "OR"])

        # Choose the values to filter on
        # We do this by loading a single record at random and using its values
        # This is significantly faster than loading all records and then filtering
        offset = self.random.randint(
            0, page.evaluate(f'{self.list_info["js_selector"]}.grandTotalRows')
        )
        data = table_api_call(
            instance=self.instance,
            table=self.list_info["glide_table"],
            params={
                "sysparm_query": self.list_info["query"],
                "sysparm_fields": ",".join(self.filter_columns),
                "sysparm_display_value": "all",
                "sysparm_limit": "1",
                "sysparm_offset": f"{offset}",
            },
        )["result"][0]
        # XXX: The use of "" as default display value is a hack, but it seems to work for now.
        self.filter_values = [
            data.get(c, {"display_value": ""})["display_value"] for c in self.filter_columns
        ]

        # Make sure we expand empty strings to their expected display value (the API fails to do this)
        for i, (col, val) in enumerate(zip(self.filter_columns, self.filter_values)):
            if self.list_info["columns"][col]["type"] == "choice" and val in [None, ""]:
                self.filter_values[i] = self.list_info["columns"][col]["choices"].get(
                    "", "-- None --"
                )
                # XXX: The use of -- None -- as default is a hack, but it seems to work for now.

        # Make sure there are no trailing spaces in the values
        self.filter_values = [v.strip() for v in self.filter_values]

        # Make sure we use only values that are available in the UI
        # (some database entries have old values that are no longer available)
        for i, (c, v) in enumerate(zip(self.filter_columns, self.filter_values)):
            if "choices" in self.list_info["columns"][c]:
                if v not in self.list_info["columns"][c]["choices"]:
                    # replace with a random choice
                    self.filter_values[i] = self.random.choice(
                        list(self.list_info["columns"][c]["choices"].values())
                    )
        goal = (
            f"Create a filter for the list to extract all entries where "
            + f" {'and' if self.filter_kind == 'AND' else 'or'} ".join(
                [
                    f'"{self.list_info["columns"][col]["label"]}" is "{val}"'
                    for col, val in zip(self.filter_columns, self.filter_values)
                ]
            )
            + "."
        )

        return goal, {}

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        self._wait_for_ready(page)

        iframe, _, _ = self._get_visible_list(page)

        iframe.locator(".list_filter_toggle").click()

        # Wait for the filter to be visible
        iframe.wait_for_function(
            "typeof document.querySelectorAll('.list_filter')[0] !== 'undefined' && document.querySelectorAll('.list_filter')[0].offsetParent !== null"
        )

        # Clear any existing filters
        # Use a while loop and click the first button until there are no more buttons
        while iframe.locator(".filerTableAction.deleteButton:visible").count() > 0:
            logging.debug("Clearing existing filter condition")
            iframe.locator(".filerTableAction.deleteButton:visible").nth(0).click()


cheat

                timeout=3000,
            )
            logging.debug("Detected gsft_main ready")
            gsft_main_present = True
        except TimeoutError:
            logging.debug(
                "Timed out waiting for gsft_main to be ready; searching for GlideList API directly"
            )
            pass

        logging.debug("Waiting for Glide list API to be available")
        if gsft_main_present:
            page.wait_for_function("window.gsft_main.GlideList2 !== undefined")
        else:
            page.wait_for_function("window.GlideList2 !== undefined")

        logging.debug("Detected Glide list API ready")

        return gsft_main_present

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        right_url = check_url_suffix_match(page, expected_url=self.start_url, task=self)
        if not right_url:
            return
        gft_main_present = self._wait_for_ready(page)
        if gft_main_present:
            main_element = page.wait_for_selector("iframe#gsft_main").content_frame()
        else:
            main_element = page

        main_element.wait_for_selector(
            f"#hdr_{self.table_name}"
        )  # Selector for the name of the columns
        # system name mapped to their order in the table
        all_column_elements = main_element.query_selector_all(f"#hdr_{self.table_name} th")
        required_fields_order = {}
        for i, element in enumerate(all_column_elements):
            if element.get_attribute("name") in self.fields:
                required_fields_order[element.get_attribute("name")] = i

        # Lines of the table
        table_lines = main_element.query_selector_all(
            f".list2_body [record_class={self.table_name}]"
        )

        # will hold the values to extract
        table_values = {}

        # Extract the values of the required fields
        for line_element in table_lines:
            line_fields = line_element.query_selector_all("td")
            line_values = {}
            for field, order in required_fields_order.items():
                printed_field_name = self.fields[field]
                line_values[printed_field_name] = line_fields[order].inner_text()
            printed_unique_value_name = self.fields[self.unique_field_name]
            unique_field_value = line_values[printed_unique_value_name]
            line_values.pop(printed_unique_value_name)
            table_values[unique_field_value] = line_values

        # Add the "extracted" answer to the chat messages
        if len(self.fields) > 2:
            chat_messages.append({"role": "assistant", "message": json.dumps(table_values)})
        # In this case, we expect only one field to be extracted
        else:
            expected_field = list(self.fields.keys() - {self.unique_field_name})[0]
            pretty_field_name = self.fields[expected_field]
            # Here we assume that unique_field_value is unique in the table_values
            chat_messages.append(
                {
                    "role": "assistant",
                    "message": str(table_values[unique_field_value][pretty_field_name]),
                }
            )

    def validate(
        self, page: playwright.sync_api.Page, chat_messages: list[str]
    ) -> Tuple[float, bool, str, dict]:
        """
        Validate the solution

        Note: current implementation is limited to AND and OR filters (single type per filter) with equality operators

        """
        if (
            len(chat_messages) == 0
            or chat_messages[-1]["role"] != "assistant"
            or not chat_messages[-1]["message"]
        ):
            return 0, False, "", {"message": "No extracted values found."}

        # When 2 or more fields (unique field is always present so at least 2 fields are present), we expect a dict
        # Otherwise, we only look for the presence of the expected value in the message sent by the agent
        if len(self.fields) > 2:


validate

            printed_unique_value_name = self.fields[self.unique_field_name]
            unique_field_value = line_values[printed_unique_value_name]
            line_values.pop(printed_unique_value_name)
            table_values[unique_field_value] = line_values

        # Add the "extracted" answer to the chat messages
        if len(self.fields) > 2:
            chat_messages.append({"role": "assistant", "message": json.dumps(table_values)})
        # In this case, we expect only one field to be extracted
        else:
            expected_field = list(self.fields.keys() - {self.unique_field_name})[0]
            pretty_field_name = self.fields[expected_field]
            # Here we assume that unique_field_value is unique in the table_values
            chat_messages.append(
                {
                    "role": "assistant",
                    "message": str(table_values[unique_field_value][pretty_field_name]),
                }
            )

    def validate(
        self, page: playwright.sync_api.Page, chat_messages: list[str]
    ) -> Tuple[float, bool, str, dict]:
        """
        Validate the solution

        Note: current implementation is limited to AND and OR filters (single type per filter) with equality operators

        """
        if (
            len(chat_messages) == 0
            or chat_messages[-1]["role"] != "assistant"
            or not chat_messages[-1]["message"]
        ):
            return 0, False, "", {"message": "No extracted values found."}

        # When 2 or more fields (unique field is always present so at least 2 fields are present), we expect a dict
        # Otherwise, we only look for the presence of the expected value in the message sent by the agent
        if len(self.fields) > 2:
            answer = json.loads(chat_messages[-1]["message"])
            for expected_line in self.expected_values:
                # Check if the line is in the visible lines
                if expected_line[self.unique_field_name] not in answer:
                    return (
                        0,
                        False,
                        "",
                        {
                            "message": f"Value {expected_line[self.unique_field_name]} for unique field {self.unique_field_name} not found in the list."
                        },
                    )
                # Check if the values are correct
                unique_value = expected_line[self.unique_field_name]
                # This checks all fields inside the dict for the unique value
                for field, value in expected_line.items():
                    # The unique field's presence is implicitly validated by the above check
                    if field == self.unique_field_name:
                        continue
                    printed_field_name = self.fields[field]
                    if answer[unique_value][printed_field_name] != value:
                        return 0, False, "", {"message": "Incorrect value."}
        # In this case, we expect only one field to be extracted
        else:
            # get the field that is not the unique field
            field = list(self.fields.keys() - {self.unique_field_name})[0]
            expected_value = str(self.expected_values[0][field])
            if expected_value not in chat_messages[-1]["message"]:
                return 0, False, "", {"message": "Incorrect value."}

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Correct information extracted."},
        )

class FilterAssetListTask(FilterListTask):
    config_path = FILTER_ASSET_LIST_CONFIG_PATH

    def __init__(
        self,
        seed: int = None,
        instance=None,
        fixed_config: dict = None,
        **kwargs,
    ) -> None:
        super().__init__(
            seed=seed,
            instance=instance,
            list_url=LISTS["alm_asset"]["url"],
            fixed_config=fixed_config,
            expected_fields_path=EXPECTED_ASSET_LIST_COLUMNS_PATH,
            **kwargs,
        )


            # newly added row should be the last one
            row_index = filter.locator(".filter_row").count() - 1

            # Refresh since new rows are added at each iteration
            row = iframe.locator(".filter_row").nth(row_index)
            row_selectors = row.locator("select.filerTableSelect")
            field_selector = row_selectors.nth(0)
            dir_selector = row_selectors.nth(1)

            # Choose field
            logging.debug(f"Choosing sorting field {field_txt}")
            field_selector.select_option(field_txt)

            # Choose sort order
            logging.debug(f"Choosing sorting direction {dir_txt}")
            dir_selector.select_option(dir_txt)

        # hack to wait for two events
        n_events_to_wait = 2

        def n_events_passed(event_info):
            nonlocal n_events_to_wait
            n_events_to_wait -= 1
            return n_events_to_wait == 0

        # click and wait for two navigations to happen (the iframe will navigate first, the page after)
        with page.expect_event("framenavigated", predicate=n_events_passed):
            filter.get_by_label("Run filter").click()

    def validate(
        self, page: playwright.sync_api.Page, chat_messages: list[str]
    ) -> Tuple[float, bool, str, dict]:
        right_url = check_url_suffix_match(
            page, expected_url=self.start_url[: self.start_url.find("%3F")], task=self
        )
        if not right_url:
            return (
                0,
                False,
                "",
                {
                    "message": f"The page is not in the right URL to validate task {self.__class__.__name__}."
                },
            )


import json

from playwright.sync_api import Page
from typing import Tuple

from .base import AbstractServiceNowTask
from .comp_building_block import CompositionalBuildingBlockTask

from ..api.utils import table_api_call

class SetProblemAsDuplicateTask(AbstractServiceNowTask, CompositionalBuildingBlockTask):
    """
    Set a problem as duplicate, assuming we start on the problems list view.

    Parameters:
    -----------
    instance: SNowInstance
        The instance to use.
    start_rel_url: str
        The relative URL of the task list.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/filter_change_request_list_task.json
        for an example of a configuration file.
    respect_problem_ordering: bool
        Whether to respect the ordering of the problems in the list. If True, the task will pick the first problem in the
        list as the target problem. If False, the task validation will check if any problem is a duplicate of the other.
    add_comment: bool
        Whether or not to add comment to the duplicated task. If set to True, will add "Duplicate" as the problem description
    goal_version: str
        choice of "base", "priority", "high_priority". Adjusts the goal to the task setting for L2
    """

    def __init__(
        self,
        seed: int = None,
        instance=None,
        start_rel_url="/now/nav/ui/classic/params/target/problem_list.do",
        fixed_config: dict = None,
        respect_problem_ordering: bool = False,
        add_comment: bool = False,
        goal_version: str = "base",
        level: int = None,
        **kwargs,
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url=start_rel_url)
        self.fixed_config = fixed_config
        self.config = fixed_config

        self.problem_sys_id = None
        self.respect_problem_ordering = respect_problem_ordering
        self.add_comment = add_comment
        self.goal_version = goal_version
        self.level = level
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        self.target_problem = self.fixed_config["target_problem"]
        self.source_problem = self.fixed_config["source_problem"]

        goal = self.get_pretty_printed_description()

        return goal, {}

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L2 compositional tasks.
        called by subclasses
        """

        if self.level == 3:
            task_info = " "
        elif self.goal_version == "base":
            task_info = "Mark problems with duplicated problem statements as such. You can mark any as duplicate of the other."
        elif self.goal_version == "priority":
            task_info = "Among the problems with duplicated problem statements, mark the lower priority one as duplicate of the higher priority one"
        elif self.goal_version == "high priority":
            task_info = "Among the problems with duplicated problem statements, mark any as duplicate of the other. Change the description of the problem marked as duplicate to 'duplicate'."

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page, chat_messages)
        target_problem_number = self.target_problem["number"]

        frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
        # Search for the private task by search for the number
        frame.wait_for_selector(f"[aria-label='Preview record: {target_problem_number}']").click()
        page.wait_for_timeout(1500)
        # Click on the private task to open it
        frame.get_by_text("Open Record").click()
        page.wait_for_timeout(2000)
        page.wait_for_load_state("networkidle")
        frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
        page.wait_for_timeout(1500)
        # Open the duplicate mode
        frame.get_by_text("Mark Duplicate").first.click()
        page.wait_for_timeout(1000)
        # Close the pop-up to edit the duplicate problem in the same window
        frame.get_by_text("Close").last.click()
        frame.locator('[aria-labelledby="label.problem.duplicate_of"]').fill(
            self.source_problem["number"]
        )
        page.keyboard.press("Enter")
        page.wait_for_timeout(1000)
        if self.add_comment:
            frame.locator('[id="problem.description"]').fill("Duplicate")

        frame.get_by_text("update").first.click()

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
        """
        Validate the solution
        """
        target_problem_record = table_api_call(
            instance=self.instance,
            table="problem",
            params={"sysparm_query": f"number={self.target_problem['number']}"},
        )["result"]
        source_problem_record = table_api_call(
            instance=self.instance,
            table="problem",
            params={"sysparm_query": f"number={self.source_problem['number']}"},
        )["result"]
        # If the ordering can be anything, we check both problems
        problem_found = source_problem_record and target_problem_record

        if not problem_found:
            return 0, False, "", {"message": "Problem not found in DB."}

        # if the duplicate value is not set, the field will be an empty string; otherwise it will be a dict
        target_duplicate_value = target_problem_record[0]["duplicate_of"]
        if target_duplicate_value:
            target_duplicate_value = target_duplicate_value["value"]

        target_is_duplicate = target_duplicate_value == source_problem_record[0]["sys_id"]
        if self.respect_problem_ordering:
            problem_marked_as_duplicate = target_is_duplicate
        else:
            source_duplicate_value = source_problem_record[0]["duplicate_of"]
            if source_duplicate_value:
                source_duplicate_value = source_duplicate_value["value"]
            source_is_duplicate = source_duplicate_value == target_problem_record[0]["sys_id"]
            problem_marked_as_duplicate = target_is_duplicate or source_is_duplicate

        if self.add_comment:
            comment_added = (
                target_problem_record[0]["description"].lower() == "duplicate"
                and target_is_duplicate
            )
            if not self.respect_problem_ordering:
                comment_added = comment_added or (
                    source_problem_record[0]["description"].lower() == "duplicate"
                    and source_is_duplicate
                )
            if not comment_added:
                return 0, False, "", {"message": "Comment not added."}

        if not problem_marked_as_duplicate:
            return 0, False, "", {"message": "Problem not marked as duplicate."}

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Problem task was closed as duplicate."},
        )

__TASKS__ = [SetProblemAsDuplicateTask]


    Parameters:
    -----------
    instance: SNowInstance
        The instance to use.
    start_rel_url: str
        The relative URL of the task list.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/filter_change_request_list_task.json
        for an example of a configuration file.
    respect_problem_ordering: bool
        Whether to respect the ordering of the problems in the list. If True, the task will pick the first problem in the
        list as the target problem. If False, the task validation will check if any problem is a duplicate of the other.
    add_comment: bool
        Whether or not to add comment to the duplicated task. If set to True, will add "Duplicate" as the problem description
    goal_version: str
        choice of "base", "priority", "high_priority". Adjusts the goal to the task setting for L2
    """

    def __init__(
        self,
        seed: int = None,
        instance=None,
        start_rel_url="/now/nav/ui/classic/params/target/problem_list.do",
        fixed_config: dict = None,
        respect_problem_ordering: bool = False,
        add_comment: bool = False,
        goal_version: str = "base",
        level: int = None,
        **kwargs,
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url=start_rel_url)
        self.fixed_config = fixed_config
        self.config = fixed_config

        self.problem_sys_id = None
        self.respect_problem_ordering = respect_problem_ordering
        self.add_comment = add_comment
        self.goal_version = goal_version
        self.level = level
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        self.target_problem = self.fixed_config["target_problem"]
        self.source_problem = self.fixed_config["source_problem"]

        goal = self.get_pretty_printed_description()

        return goal, {}

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L2 compositional tasks.
        called by subclasses
        """

        if self.level == 3:
            task_info = " "
        elif self.goal_version == "base":
            task_info = "Mark problems with duplicated problem statements as such. You can mark any as duplicate of the other."
        elif self.goal_version == "priority":


        instance=None,
        start_rel_url="/now/nav/ui/classic/params/target/problem_list.do",
        fixed_config: dict = None,
        respect_problem_ordering: bool = False,
        add_comment: bool = False,
        goal_version: str = "base",
        level: int = None,
        **kwargs,
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url=start_rel_url)
        self.fixed_config = fixed_config
        self.config = fixed_config

        self.problem_sys_id = None
        self.respect_problem_ordering = respect_problem_ordering
        self.add_comment = add_comment
        self.goal_version = goal_version
        self.level = level
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        self.target_problem = self.fixed_config["target_problem"]
        self.source_problem = self.fixed_config["source_problem"]

        goal = self.get_pretty_printed_description()

        return goal, {}

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L2 compositional tasks.
        called by subclasses
        """

        if self.level == 3:
            task_info = " "
        elif self.goal_version == "base":
            task_info = "Mark problems with duplicated problem statements as such. You can mark any as duplicate of the other."
        elif self.goal_version == "priority":
            task_info = "Among the problems with duplicated problem statements, mark the lower priority one as duplicate of the higher priority one"
        elif self.goal_version == "high priority":
            task_info = "Among the problems with duplicated problem statements, mark any as duplicate of the other. Change the description of the problem marked as duplicate to 'duplicate'."

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page, chat_messages)


    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url=start_rel_url)
        self.fixed_config = fixed_config
        self.config = fixed_config

        self.problem_sys_id = None
        self.respect_problem_ordering = respect_problem_ordering
        self.add_comment = add_comment
        self.goal_version = goal_version
        self.level = level
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        self.target_problem = self.fixed_config["target_problem"]
        self.source_problem = self.fixed_config["source_problem"]

        goal = self.get_pretty_printed_description()

        return goal, {}

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L2 compositional tasks.
        called by subclasses
        """

        if self.level == 3:
            task_info = " "
        elif self.goal_version == "base":
            task_info = "Mark problems with duplicated problem statements as such. You can mark any as duplicate of the other."
        elif self.goal_version == "priority":
            task_info = "Among the problems with duplicated problem statements, mark the lower priority one as duplicate of the higher priority one"
        elif self.goal_version == "high priority":
            task_info = "Among the problems with duplicated problem statements, mark any as duplicate of the other. Change the description of the problem marked as duplicate to 'duplicate'."

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page, chat_messages)
        target_problem_number = self.target_problem["number"]

        frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
        # Search for the private task by search for the number
        frame.wait_for_selector(f"[aria-label='Preview record: {target_problem_number}']").click()
        page.wait_for_timeout(1500)
        # Click on the private task to open it
        frame.get_by_text("Open Record").click()
        page.wait_for_timeout(2000)
        page.wait_for_load_state("networkidle")
        frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
        page.wait_for_timeout(1500)
        # Open the duplicate mode
        frame.get_by_text("Mark Duplicate").first.click()
        page.wait_for_timeout(1000)
        # Close the pop-up to edit the duplicate problem in the same window
        frame.get_by_text("Close").last.click()


cheat

        return goal, {}

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L2 compositional tasks.
        called by subclasses
        """

        if self.level == 3:
            task_info = " "
        elif self.goal_version == "base":
            task_info = "Mark problems with duplicated problem statements as such. You can mark any as duplicate of the other."
        elif self.goal_version == "priority":
            task_info = "Among the problems with duplicated problem statements, mark the lower priority one as duplicate of the higher priority one"
        elif self.goal_version == "high priority":
            task_info = "Among the problems with duplicated problem statements, mark any as duplicate of the other. Change the description of the problem marked as duplicate to 'duplicate'."

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page, chat_messages)
        target_problem_number = self.target_problem["number"]

        frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
        # Search for the private task by search for the number
        frame.wait_for_selector(f"[aria-label='Preview record: {target_problem_number}']").click()
        page.wait_for_timeout(1500)
        # Click on the private task to open it
        frame.get_by_text("Open Record").click()
        page.wait_for_timeout(2000)
        page.wait_for_load_state("networkidle")
        frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
        page.wait_for_timeout(1500)
        # Open the duplicate mode
        frame.get_by_text("Mark Duplicate").first.click()
        page.wait_for_timeout(1000)
        # Close the pop-up to edit the duplicate problem in the same window
        frame.get_by_text("Close").last.click()
        frame.locator('[aria-labelledby="label.problem.duplicate_of"]').fill(
            self.source_problem["number"]
        )
        page.keyboard.press("Enter")
        page.wait_for_timeout(1000)
        if self.add_comment:
            frame.locator('[id="problem.description"]').fill("Duplicate")

        frame.get_by_text("update").first.click()

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
        """
        Validate the solution
        """
        target_problem_record = table_api_call(
            instance=self.instance,
            table="problem",
            params={"sysparm_query": f"number={self.target_problem['number']}"},
        )["result"]
        source_problem_record = table_api_call(
            instance=self.instance,
            table="problem",
            params={"sysparm_query": f"number={self.source_problem['number']}"},
        )["result"]
        # If the ordering can be anything, we check both problems
        problem_found = source_problem_record and target_problem_record

        if not problem_found:
            return 0, False, "", {"message": "Problem not found in DB."}


validate

        frame.get_by_text("Open Record").click()
        page.wait_for_timeout(2000)
        page.wait_for_load_state("networkidle")
        frame = page.wait_for_selector('iframe[name="gsft_main"]').content_frame()
        page.wait_for_timeout(1500)
        # Open the duplicate mode
        frame.get_by_text("Mark Duplicate").first.click()
        page.wait_for_timeout(1000)
        # Close the pop-up to edit the duplicate problem in the same window
        frame.get_by_text("Close").last.click()
        frame.locator('[aria-labelledby="label.problem.duplicate_of"]').fill(
            self.source_problem["number"]
        )
        page.keyboard.press("Enter")
        page.wait_for_timeout(1000)
        if self.add_comment:
            frame.locator('[id="problem.description"]').fill("Duplicate")

        frame.get_by_text("update").first.click()

    def validate(self, page: Page, chat_messages: list[str]) -> Tuple[float, bool, str, dict]:
        """
        Validate the solution
        """
        target_problem_record = table_api_call(
            instance=self.instance,
            table="problem",
            params={"sysparm_query": f"number={self.target_problem['number']}"},
        )["result"]
        source_problem_record = table_api_call(
            instance=self.instance,
            table="problem",
            params={"sysparm_query": f"number={self.source_problem['number']}"},
        )["result"]
        # If the ordering can be anything, we check both problems
        problem_found = source_problem_record and target_problem_record

        if not problem_found:
            return 0, False, "", {"message": "Problem not found in DB."}

        # if the duplicate value is not set, the field will be an empty string; otherwise it will be a dict
        target_duplicate_value = target_problem_record[0]["duplicate_of"]
        if target_duplicate_value:
            target_duplicate_value = target_duplicate_value["value"]

        target_is_duplicate = target_duplicate_value == source_problem_record[0]["sys_id"]
        if self.respect_problem_ordering:
            problem_marked_as_duplicate = target_is_duplicate
        else:
            source_duplicate_value = source_problem_record[0]["duplicate_of"]
            if source_duplicate_value:
                source_duplicate_value = source_duplicate_value["value"]
            source_is_duplicate = source_duplicate_value == target_problem_record[0]["sys_id"]
            problem_marked_as_duplicate = target_is_duplicate or source_is_duplicate

        if self.add_comment:
            comment_added = (
                target_problem_record[0]["description"].lower() == "duplicate"
                and target_is_duplicate
            )
            if not self.respect_problem_ordering:
                comment_added = comment_added or (
                    source_problem_record[0]["description"].lower() == "duplicate"
                    and source_is_duplicate
                )
            if not comment_added:
                return 0, False, "", {"message": "Comment not added."}

        if not problem_marked_as_duplicate:
            return 0, False, "", {"message": "Problem not marked as duplicate."}

        return (
            1,
            True,
            "Nice work, thank you!",
            {"message": "Problem task was closed as duplicate."},
        )

__TASKS__ = [SetProblemAsDuplicateTask]


"""
Tasks related to basic menu navigation.

"""

import json
import playwright.sync_api
import re

from importlib import resources
from playwright.sync_api import Page
from urllib import parse
from typing import List, Tuple

from ..api.utils import table_api_call
from .base import AbstractServiceNowTask
from ..config import ALL_MENU_PATH, IMPERSONATION_CONFIG_PATH
from ..instance import SNowInstance
from ..utils import impersonate_user

class AllMenuTask(AbstractServiceNowTask):
    """
    Navigate to some application module using the All menu.

    Parameters:
    -----------

    instance: SNowInstance
        The instance on which to create the record.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/all_menu.json
        for an example of a configuration file.

    """

    def __init__(
        self, seed: int = None, instance: SNowInstance = None, fixed_config: dict = None, **kwargs
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
        self.fixed_config = fixed_config
        with open(ALL_MENU_PATH, "r") as f:
            self.all_configs = json.load(f)
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get task configuration
        self.module = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )

        # When menu tasks do not need to be validated, the URL can be omitted from their config
        self.final_url = self.instance.snow_url + self.module.get("url", "")

        # Generate goal
        goal = f'Navigate to the "{self.module["module"]}" module of the "{self.module["application"]}" application.'
        info = {}

        return goal, info

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        task_info = f'- Navigate to the "{self.module["module"]}" module of the "{self.module["application"]}" application.'

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        # gsft_main remains undefined on the landing page; we have to wait for the network to be idle instead.
        page.wait_for_load_state("networkidle")
        menu_button = page.locator('div[aria-label="All"]')
        if menu_button.get_attribute("aria-expanded").lower() != "true":
            menu_button.click()

        # Select the menu's main div
        menu = page.locator('div[aria-label="All menu"]')

        # Filter the menu using the application's name
        menu.get_by_placeholder("Filter").fill(self.module["application"])

        # Avoids issues due to list not being fully filtered yet
        # We could certainly do something more fancy, but it's not
        # worth spending time on right now.
        page.wait_for_timeout(1000)

        path = [m.strip() for m in self.module["module"].split(">")]
        # Navigate to the application's location in the menu and select its parent
        locator = menu.get_by_label(self.module["application"], exact=True).and_(
            menu.get_by_role("button")
        )
        locator = locator.locator("xpath=ancestor::div[contains(@class, 'snf-collapsible-list')]")
        for module in path[:-1]:
            # Expand menu if necessary (this is mostly for visual satisfaction, cheat func would still work without it)
            # XXX: Double selector here is due to discrepancies in the UI for various ServiceNow releases
            button = locator.locator(
                f'button[aria-label="{module}"], div[role="button"][aria-label="{module}"]'
            ).first
            button.scroll_into_view_if_needed()
            if button.get_attribute("aria-expanded").lower() != "true":
                button.click()

            # Get the button's parent "collapsible list" container
            parent_div_locator = button.locator(
                "xpath=ancestor::div[contains(@class, 'snf-collapsible-list')]"
            )
            locator = parent_div_locator

        # Click the final menu item
        menu_item = locator.get_by_label(path[-1], exact=True)
        # In some cases, like System Scheduler > Scheduled Jobs > Scheduled Jobs, modules are repeated in the path
        # This causes problems when clicking. Therefore, we pick the last item
        if menu_item.count() > 1:
            menu_item = menu_item.first
        with page.expect_navigation():
            menu_item.click()
        page.wait_for_timeout(2000)

    def validate(
        self, page: playwright.sync_api.Page, chat_messages: list[str]
    ) -> Tuple[float, bool, str, dict]:
        page.wait_for_load_state("domcontentloaded")

        # Get the current URL and the final URL
        current_url = parse.urlunparse(
            parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        )
        final_url = parse.urlunparse(parse.urlparse(parse.unquote(self.final_url)))

        if final_url == current_url:
            return (
                1,
                True,
                "Nice work, thank you!",
                {"message": "Correct module reached."},
            )

        return 0, False, "", {"message": "Not at expected URL."}

    def teardown(self) -> None:
        pass

class ImpersonationTask(AbstractServiceNowTask):
    """
    Task to impersonate a user.

    Parameters:
    -----------

    instance: SNowInstance
        The instance on which to create the record.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/impersonation_users.json
        for an example of a configuration file.

    """

    def __init__(
        self, seed: int = None, instance=None, fixed_config: dict = None, **kwargs
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
        self.fixed_config = fixed_config
        with open(IMPERSONATION_CONFIG_PATH, "r") as f:
            self.all_configs = json.load(f)
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get task configuration
        self.user_full_name = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )
        assert self.user_full_name in self.all_configs

        # Generate goal
        goal = f"Impersonate the user {self.user_full_name}."
        info = {}

        return goal, info

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        task_info = f"- Impersonate the user {self.user_full_name} \n"

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        impersonate_user(self.user_full_name, page)

    def validate(
        self, page: playwright.sync_api.Page, chat_messages: list[str]
    ) -> Tuple[float, bool, str, dict]:
        page.wait_for_function("window.NOW && window.NOW.user")

        user_info = page.evaluate("window.NOW")["user"]

        # If the current user is not being impersonated, fail.
        if not user_info["isImpersonating"]:
            return 0, False, "", {"message": "Not currently impersonating a user."}

        # Fetch user's full name from database
        user_fullname = table_api_call(
            instance=self.instance,
            table="sys_user",
            params={
                "sysparm_query": f"sys_id={user_info['userID']}",
                "sysparm_fields": "name",
            },
        )["result"][0]["name"]

        # If the name matches, success.
        if user_fullname == self.user_full_name:
            return (
                1,
                True,
                "Nice work, thank you!",
                {"message": "Correct user impersonated."},
            )

        # Otherwise, fail.
        return 0, False, "", {"message": "Currently impersonating the wrong user."}

    def teardown(self) -> None:
        pass

__TASKS__ = [AllMenuTask, ImpersonationTask]


AllMenuTask

Tasks related to basic menu navigation.

"""

import json
import playwright.sync_api
import re

from importlib import resources
from playwright.sync_api import Page
from urllib import parse
from typing import List, Tuple

from ..api.utils import table_api_call
from .base import AbstractServiceNowTask
from ..config import ALL_MENU_PATH, IMPERSONATION_CONFIG_PATH
from ..instance import SNowInstance
from ..utils import impersonate_user

class AllMenuTask(AbstractServiceNowTask):
    """
    Navigate to some application module using the All menu.

    Parameters:
    -----------

    instance: SNowInstance
        The instance on which to create the record.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/all_menu.json
        for an example of a configuration file.

    """

    def __init__(
        self, seed: int = None, instance: SNowInstance = None, fixed_config: dict = None, **kwargs
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
        self.fixed_config = fixed_config
        with open(ALL_MENU_PATH, "r") as f:
            self.all_configs = json.load(f)
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get task configuration
        self.module = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )

        # When menu tasks do not need to be validated, the URL can be omitted from their config
        self.final_url = self.instance.snow_url + self.module.get("url", "")

        # Generate goal
        goal = f'Navigate to the "{self.module["module"]}" module of the "{self.module["application"]}" application.'
        info = {}

        return goal, info

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        task_info = f'- Navigate to the "{self.module["module"]}" module of the "{self.module["application"]}" application.'

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        # gsft_main remains undefined on the landing page; we have to wait for the network to be idle instead.
        page.wait_for_load_state("networkidle")
        menu_button = page.locator('div[aria-label="All"]')
        if menu_button.get_attribute("aria-expanded").lower() != "true":
            menu_button.click()

        # Select the menu's main div
        menu = page.locator('div[aria-label="All menu"]')

        # Filter the menu using the application's name
        menu.get_by_placeholder("Filter").fill(self.module["application"])

        # Avoids issues due to list not being fully filtered yet
        # We could certainly do something more fancy, but it's not
        # worth spending time on right now.
        page.wait_for_timeout(1000)

        path = [m.strip() for m in self.module["module"].split(">")]
        # Navigate to the application's location in the menu and select its parent
        locator = menu.get_by_label(self.module["application"], exact=True).and_(
            menu.get_by_role("button")
        )
        locator = locator.locator("xpath=ancestor::div[contains(@class, 'snf-collapsible-list')]")
        for module in path[:-1]:
            # Expand menu if necessary (this is mostly for visual satisfaction, cheat func would still work without it)
            # XXX: Double selector here is due to discrepancies in the UI for various ServiceNow releases
            button = locator.locator(
                f'button[aria-label="{module}"], div[role="button"][aria-label="{module}"]'
            ).first
            button.scroll_into_view_if_needed()
            if button.get_attribute("aria-expanded").lower() != "true":
                button.click()

            # Get the button's parent "collapsible list" container
            parent_div_locator = button.locator(
                "xpath=ancestor::div[contains(@class, 'snf-collapsible-list')]"
            )
            locator = parent_div_locator

        # Click the final menu item
        menu_item = locator.get_by_label(path[-1], exact=True)
        # In some cases, like System Scheduler > Scheduled Jobs > Scheduled Jobs, modules are repeated in the path
        # This causes problems when clicking. Therefore, we pick the last item
        if menu_item.count() > 1:
            menu_item = menu_item.first
        with page.expect_navigation():
            menu_item.click()
        page.wait_for_timeout(2000)

    def validate(
        self, page: playwright.sync_api.Page, chat_messages: list[str]
    ) -> Tuple[float, bool, str, dict]:
        page.wait_for_load_state("domcontentloaded")

        # Get the current URL and the final URL
        current_url = parse.urlunparse(
            parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        )
        final_url = parse.urlunparse(parse.urlparse(parse.unquote(self.final_url)))

        if final_url == current_url:
            return (
                1,
                True,
                "Nice work, thank you!",
                {"message": "Correct module reached."},
            )

        return 0, False, "", {"message": "Not at expected URL."}

    def teardown(self) -> None:
        pass

class ImpersonationTask(AbstractServiceNowTask):
    """
    Task to impersonate a user.

    Parameters:
    -----------

    instance: SNowInstance
        The instance on which to create the record.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/impersonation_users.json
        for an example of a configuration file.

    """

    def __init__(
        self, seed: int = None, instance=None, fixed_config: dict = None, **kwargs


ImpersonationTask

        # Get the current URL and the final URL
        current_url = parse.urlunparse(
            parse.urlparse(parse.unquote(page.evaluate("() => window.location.href")))
        )
        final_url = parse.urlunparse(parse.urlparse(parse.unquote(self.final_url)))

        if final_url == current_url:
            return (
                1,
                True,
                "Nice work, thank you!",
                {"message": "Correct module reached."},
            )

        return 0, False, "", {"message": "Not at expected URL."}

    def teardown(self) -> None:
        pass

class ImpersonationTask(AbstractServiceNowTask):
    """
    Task to impersonate a user.

    Parameters:
    -----------

    instance: SNowInstance
        The instance on which to create the record.
    fixed_config: dict
        Configuration to use for the task. If provided, the task will use the provided configuration instead of
        selecting a random one. See browsergym/workarena/data_files/task_configs/impersonation_users.json
        for an example of a configuration file.

    """

    def __init__(
        self, seed: int = None, instance=None, fixed_config: dict = None, **kwargs
    ) -> None:
        super().__init__(seed=seed, instance=instance, start_rel_url="/now/nav/ui/home")
        self.fixed_config = fixed_config
        with open(IMPERSONATION_CONFIG_PATH, "r") as f:
            self.all_configs = json.load(f)
        self.__dict__.update(kwargs)

    def setup_goal(self, page: Page) -> tuple[str, dict]:
        super().setup_goal(page=page)

        # Get task configuration
        self.user_full_name = (
            self.fixed_config if self.fixed_config else self.random.choice(self.all_configs)
        )
        assert self.user_full_name in self.all_configs

        # Generate goal
        goal = f"Impersonate the user {self.user_full_name}."
        info = {}

        return goal, info

    def get_pretty_printed_description(self) -> str:
        """
        Get the task info for this task when used in a private task; Used in L3 compositional tasks.
        called by subclasses
        """
        task_info = f"- Impersonate the user {self.user_full_name} \n"

        return task_info

    def cheat(self, page: Page, chat_messages: list[str]) -> None:
        super().cheat(page=page, chat_messages=chat_messages)
        impersonate_user(self.user_full_name, page)

    def validate(
        self, page: playwright.sync_api.Page, chat_messages: list[str]
    ) -> Tuple[float, bool, str, dict]:
        page.wait_for_function("window.NOW && window.NOW.user")

        user_info = page.evaluate("window.NOW")["user"]

        # If the current user is not being impersonated, fail.
        if not user_info["isImpersonating"]:
            return 0, False, "", {"message": "Not currently impersonating a user."}

        # Fetch user's full name from database
        user_fullname = table_api_call(
            instance=self.instance,
            table="sys_user",
            params={
                "sysparm_query": f"sys_id={user_info['userID']}",
                "sysparm_fields": "name",
            },
        )["result"][0]["name"]

        # If the name matches, success.
        if user_fullname == self.user_full_name:
            return (
                1,
                True,
                "Nice work, thank you!",
                {"message": "Correct user impersonated."},
            )

        # Otherwise, fail.
        return 0, False, "", {"message": "Currently impersonating the wrong user."}

    def teardown(self) -> None:
        pass

__TASKS__ = [AllMenuTask, ImpersonationTask]