import re
from time import sleep
from typing import List

from selenium.webdriver import Firefox
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select

from modules.browser_object import Navigation
from modules.classes.autofill_base import AutofillAddressBase
from modules.classes.credit_card import CreditCardBase
from modules.components.dropdown import Dropdown
from modules.page_base import BasePage
from modules.util import Utilities


class AboutPrefs(BasePage):
    """
    Page Object Model for about:preferences

    Attributes
    ----------
    driver: selenium.webdriver.Firefox
        WebDriver object under test
    """

    URL_TEMPLATE = "about:preferences#{category}"
    iframe = None

    def __init__(self, driver: Firefox, **kwargs):
        super().__init__(driver, **kwargs)
        self.driver = driver

    # number of tabs to reach the country tab
    TABS_TO_COUNTRY = 6
    TABS_TO_SAVE_CC = 5

    class HttpsOnlyStatus:
        """Fake enum: return a string based on a constant name"""

        def __init__(self):
            self.HTTPS_ONLY_ALL = "httpsonly-radio-enabled"
            self.HTTPS_ONLY_PRIVATE = "httpsonly-radio-enabled-pbm"
            self.HTTPS_ONLY_DISABLED = "httpsonly-radio-disabled"

    HTTPS_ONLY_STATUS = HttpsOnlyStatus()

    # Function Organization
    ## Search and Settings
    def search_engine_dropdown(self) -> Dropdown:
        """Returns the Dropdown region for search engine prefs"""
        return Dropdown(
            self, self.driver, root=self.get_element("search-engine-dropdown-root")
        )

    def find_in_settings(self, term: str) -> BasePage:
        """Search via the Find in Settings bar, return self."""
        search_input = self.get_element("find-in-settings-input")
        search_input.clear()
        search_input.send_keys(term)
        return self

    def set_alternative_language(self, lang_code: str) -> BasePage:
        """Changes the browser language"""
        self.get_element("language-set-alternative-button").click()
        self.driver.switch_to.frame(self.get_iframe())

        # Download the language options
        select_language = self.get_element("language-settings-select")
        select_language.click()
        search_languages = self.get_element("language-settings-search")
        search_languages.click()
        select_language.click()

        # Select the language, add, and make sure it appears
        select_language.click()
        self.get_element("language-option-by-code", labels=[lang_code]).click()
        select_language.click()
        self.get_element("language-settings-add-button").click()
        self.expect_element_attribute_contains(
            "language-added-list", "last-selected", f"locale-{lang_code}"
        )

        self.get_element("language-settings-ok").click()
        return self

    def select_https_only_setting(self, option_id: HttpsOnlyStatus) -> BasePage:
        """
        Click the HTTPS Only option given
        """
        self.find_in_settings("HTTPS")
        self.element_clickable(str(option_id))
        self.click_on(str(option_id))
        self.expect_element_attribute_contains(str(option_id), "checked", "")
        return self

    def set_default_zoom_level(self, zoom_percentage: int) -> BasePage:
        """
        Sets the Default Zoom level in about:preferences.
        """
        self.click_on("default-zoom-dropdown")
        with self.driver.context(self.driver.CONTEXT_CHROME):
            self.click_on("default-zoom-dropdown-value", labels=[f"{zoom_percentage}"])
        self.click_on("default-zoom-dropdown")
        return self

    def select_content_and_action(self, content_type: str, action: str) -> BasePage:
        """
        From the applications list that handles how downloaded media is used,
        select a content type and action
        """
        el = self.get_element("actions-menu", labels=[content_type])
        el.click()
        self.click_on("actions-menu-option", labels=[content_type, action])
        self.wait.until(lambda _: el.get_attribute("label") == action)
        return self

    def get_history_menulist(self) -> WebElement:
        """
        Gets the webelement for the list of history items that appear in about:preferences
        """
        return self.get_element("history_menulist")

    ## Payment and Address Management
    def verify_cc_json(
        self, cc_info_json: dict, credit_card_fill_obj: CreditCardBase
    ) -> BasePage:
        """
        Does the assertions that ensure all the extracted information (the cc_info_json) is the same as the generated fake credit_card_fill_obj data.

        ...

        Attributes
        ----------
        cc_info_json: dict
            The dictionary that is the json representation of the extracted information from a web page
        credit_card_fill_obj: CreditCardBase
            The object that contains all the generated information
        """
        assert cc_info_json["name"] == credit_card_fill_obj.name
        assert cc_info_json["number"][-4:] == credit_card_fill_obj.card_number[-4:]
        assert int(cc_info_json["month"]) == int(credit_card_fill_obj.expiration_month)
        return self

    def verify_cc_edit_saved_payments_profile(
        self, credit_card_fill_obj: CreditCardBase
    ):
        """
        Verify saved payment profile data is the same as the generated fake credit_card_fill_obj data.
        Make sure cvv is not displayed.

        Arguments:
            credit_card_fill_obj: CreditCardBase
                The object that contains all the generated information
        """
        self.switch_to_edit_saved_payments_popup_iframe()
        form_container = self.get_element("form-container")
        input_elements = form_container.find_elements(By.TAG_NAME, "input")
        expected_cc_data = [
            int(val) if val.isnumeric() else val
            for val in credit_card_fill_obj.__dict__.values()
        ]
        expected_cvv = int(credit_card_fill_obj.cvv)
        for element in input_elements:
            field_name = element.get_attribute("id")
            if field_name.startswith("cc"):
                field_value = element.get_attribute("value")
                if field_value.isnumeric():
                    field_value = int(field_value)
                assert field_value in expected_cc_data, (
                    f"{(field_name, field_value)} not found in generated data."
                )
                assert field_value != expected_cvv, "CVV is displayed."
        select_elements = form_container.find_elements(By.TAG_NAME, "select")
        for element in select_elements:
            field_name = element.get_attribute("id")
            if field_name.startswith("cc"):
                val = Select(element)
                # Only get the last two digits
                field_value = val.first_selected_option.get_attribute("value")[-2:]
                if field_value.isnumeric():
                    field_value = int(field_value)
                assert field_value in expected_cc_data, (
                    f"{(field_name, field_value)} not found in generated data."
                )
                assert field_value != expected_cvv, "CVV is displayed."
        return self

    def get_saved_payments_popup(self) -> WebElement:
        """
        Open saved payments dialog panel
        """
        return self.get_element("prefs-button", labels=["Saved payment methods"])

    def click_edit_on_dialog_element(self):
        """
        Click on edit button on dialog panel
        """
        edit_button = self.get_element(
            "panel-popup-button", labels=["autofill-manage-edit-button"]
        )
        self.expect(EC.element_to_be_clickable(edit_button))
        edit_button.click()
        return self

    def click_add_on_dialog_element(self):
        """
        Click on add button on dialog panel
        """
        add_button = self.get_element(
            "panel-popup-button", labels=["autofill-manage-add-button"]
        )
        self.expect(EC.element_to_be_clickable(add_button))
        add_button.click()
        return self

    def open_and_switch_to_saved_payments_popup(self) -> BasePage:
        """
        Open and Switch to saved payments popup frame.
        """
        saved_payments_iframe = self.get_saved_payments_popup_iframe()
        self.driver.switch_to.frame(saved_payments_iframe)
        return self

    def fill_and_save_cc_panel_information(
        self, credit_card_fill_information: CreditCardBase
    ):
        """
        Takes the sample cc object and fills it into the popup panel in the about:prefs section
        under saved payment methods.

        Arguments:
            credit_card_fill_information: The object containing all the sample data
        """
        fields = {
            "card_number": credit_card_fill_information.card_number,
            "expiration_month": credit_card_fill_information.expiration_month,
            "expiration_year": f"20{credit_card_fill_information.expiration_year}",
            "name": credit_card_fill_information.name,
        }

        for field in fields:
            self.actions.send_keys(fields[field] + Keys.TAB).perform()

        # Press tab again to navigate to the next field (this accounts for the second tab after the name field)
        self.actions.send_keys(Keys.TAB).perform()
        # Finally, press enter
        self.actions.send_keys(Keys.ENTER).perform()

    def add_entry_to_saved_payments(self, cc_data: CreditCardBase):
        """
        Takes the sample AutofillAddressBase object and adds an entry to the saved addresses list.
        Switches the appropriate frames to accommodate the operation.
        Exits after adding entry

        Arguments:
            cc_data: The object containing all the sample data
        """
        self.switch_to_saved_payments_popup_iframe()
        self.fill_and_save_cc_panel_information(cc_data)
        self.switch_to_default_frame()
        self.close_dialog_box()
        return self

    def close_dialog_box(self):
        """Close dialog box for saved addresses or payments."""
        self.element_clickable("panel-popup-button", labels=["close-button"])
        self.get_element("panel-popup-button", labels=["close-button"]).click()
        return self

    def update_cc_field_panel(self, field_name: str, value: str | int) -> BasePage:
        """
        Updates a field in the credit card popup panel in about:prefs
        Change value of the field_name given
        """

        fields = {
            "card_number": "cc-number",
            "expiration_month": "cc-exp-month",
            "expiration_year": "cc-exp-year",
            "name": "cc-name",
        }
        if field_name not in fields.keys():
            raise ValueError(
                f"{field_name} is not a valid field name for the cc dialog form."
            )
        self.switch_to_edit_saved_payments_popup_iframe()
        value_field = self.find_element(By.ID, fields[field_name])
        if value_field.tag_name != "select":
            value_field.clear()
        value_field.send_keys(value)
        self.get_element("save-button").click()
        return self

    def get_saved_addresses_popup(self) -> WebElement:
        """
        Returns saved addresses button element
        """
        return self.get_element("prefs-button", labels=["Saved addresses"])

    def open_and_switch_to_saved_addresses_popup(self) -> BasePage:
        """
        Open and Switch to saved addresses popup frame.
        """
        saved_address_iframe = self.get_saved_addresses_popup_iframe()
        self.driver.switch_to.frame(saved_address_iframe)
        return self

    def add_entry_to_saved_addresses(self, address_data: AutofillAddressBase):
        """
        Takes the sample AutofillAddressBase object and adds an entry to the saved addresses list.
        Switches the appropriate frames to accommodate the operation.
        Exits after adding entry

        Arguments:
            address_data: The object containing all the sample data
        """

        self.switch_to_edit_saved_addresses_popup_iframe()
        self.fill_and_save_address_panel_information(address_data)
        self.switch_to_default_frame()
        self.close_dialog_box()
        return self

    def get_all_saved_cc_profiles(self) -> List[WebElement]:
        """Gets the saved credit card profiles in the cc panel"""
        self.switch_to_saved_payments_popup_iframe()
        element = Select(self.get_element("cc-saved-options"))
        return element.options

    def get_all_saved_address_profiles(self) -> List[WebElement]:
        """Gets the saved credit card profiles in the cc panel"""
        self.switch_to_saved_addresses_popup_iframe()
        select_el = self.get_element("address-saved-options")
        if len(select_el.get_attribute("innerHTML")) > 1:
            return Select(select_el).options
        return []

    def extract_address_data_from_saved_addresses_entry(
        self, util: Utilities, region: str = "US"
    ) -> AutofillAddressBase:
        """
        Extracts the data from the saved addresses entry to a AutofillAddressBase object.

        Arguments:
            util: Utility instance
            region: country code in use
        """
        self.switch_to_edit_saved_addresses_popup_iframe()
        fields = {
            "name": "",
            "organization": "",
            "street-address": "",
            "address-level2": "",
            "address-level1": "",
            "postal-code": "",
            "tel": "",
            "country": "",
            "email": "",
        }

        for key in fields.keys():
            el = self.find_element(By.ID, key)
            if el.tag_name == "select":
                fields[key] = Select(el).first_selected_option.text
            else:
                fields[key] = el.get_attribute("value")

        return AutofillAddressBase(
            name=fields.get("name"),
            given_name=fields.get("name", "").split()[0],
            family_name=fields.get("name", "").split()[1],
            organization=fields.get("organization"),
            street_address=fields.get("street-address"),
            address_level_2=fields.get("address-level2"),
            address_level_1=fields.get("address-level1"),
            postal_code=fields.get("postal-code"),
            country=fields.get("country"),
            country_code=region,
            email=fields.get("email"),
            telephone=util.normalize_regional_phone_numbers(fields.get("tel"), region),
        )

    ## UI Navigation and Iframe Handling
    def get_saved_payments_popup_iframe(self) -> WebElement:
        """
        Returns the iframe object for the dialog panel in the popup
        """
        self.get_saved_payments_popup().click()
        iframe = self.get_element("browser-popup")
        return iframe

    def switch_to_edit_saved_payments_popup_iframe(self) -> BasePage:
        """
        Switch to form iframe to edit saved payments.
        """
        self.switch_to_default_frame()
        self.switch_to_iframe(2)
        return self

    def press_button_get_popup_dialog_iframe(self, button_label: str) -> WebElement:
        """
        Returns the iframe object for the dialog panel in the popup after pressing some button that triggers a popup
        """
        self.get_element("prefs-button", labels=[button_label]).click()
        iframe = self.get_element("browser-popup")
        return iframe

    def get_saved_addresses_popup_iframe(self) -> WebElement:
        """
        Returns the iframe object for the dialog panel in the popup
        """
        self.get_saved_addresses_popup().click()
        iframe = self.get_element("browser-popup")
        return iframe

    def switch_to_saved_addresses_popup_iframe(self) -> BasePage:
        """
        switch to save addresses popup frame.
        """
        self.switch_to_default_frame()
        self.switch_to_iframe(1)
        return self

    def switch_to_saved_payments_popup_iframe(self) -> BasePage:
        """
        switch to save payments popup frame.
        """
        self.switch_to_default_frame()
        self.switch_to_iframe(1)
        return self

    def switch_to_edit_saved_addresses_popup_iframe(self) -> BasePage:
        """
        Switch to form iframe to edit saved addresses.
        """
        self.switch_to_default_frame()
        self.switch_to_iframe(2)
        return self

    def get_iframe(self) -> WebElement:
        """
        Gets the webelement for the iframe that commonly appears in about:preferences
        """
        return self.get_element("browser-popup")

    def get_password_exceptions_popup_iframe(self) -> WebElement:
        """
        Returns the iframe object for the Password Exceptions dialog panel in the popup.
        """
        # Click on the "Password Exceptions" button
        self.get_element("logins-exceptions").click()
        # Get the iframe element for the popup
        iframe = self.get_element("browser-popup")
        return iframe

    ## Data Extraction and Processing
    def set_country_autofill_panel(self, country: str) -> BasePage:
        """Sets the country value in the autofill view"""
        select_country = Select(self.driver.find_element(By.ID, "country"))
        select_country.select_by_value(country)
        return self

    def fill_and_save_address_panel_information(
        self, address_data: AutofillAddressBase
    ) -> BasePage:
        """
        Takes the sample AutofillAddressBase object and fills it into the popup panel in the about:prefs section
        under saved addresses methods.

        Arguments:
            address_data: The object containing all the sample data
        """
        fields = {
            "name": address_data.name,
            "organization": address_data.organization,
            "street-address": address_data.street_address,
            "address-level2": address_data.address_level_2,
            "address-level1": address_data.address_level_1,
            "postal-code": address_data.postal_code,
            "tel": address_data.telephone,
            "email": address_data.email,
        }

        self.set_country_autofill_panel(address_data.country_code)
        form_element = self.get_element("form-container")
        children = [
            x.get_attribute("id")
            for x in form_element.find_elements(By.CSS_SELECTOR, "*")
        ]

        for key, val in fields.items():
            if key in children:
                form_element.find_element(By.ID, key).send_keys(val)
        self.get_element("save-button").click()
        return self

    def get_clear_cookie_data_value(self) -> int | None:
        """
        With the 'Clear browsing data and cookies' popup open,
        returns the <memory used> value of the option for 'Cookies and site data (<memory used>)'.
        The <memory used> value for no cookies is '0 bytes', otherwise values are '### MB', or '### KB'
        """
        # Find the dialog option elements containing the checkbox label
        options = self.get_elements("clear-data-dialog-options")

        # Extract the text from the label the second option
        second_option = options[1]
        label_text = second_option.text
        print(f"The text of the option is: {label_text}")

        # Use a regular expression to find the memory usage
        match = re.search(r"\d+", label_text)

        if match:
            number_str = match.group()  # Extract the matched number as a string
            number = int(number_str)  # Convert the number to an integer
            print(f"The extracted value is: {number}")
            return number
        else:
            print("No number found in the string")

    def get_manage_data_site_element(self, site: str) -> WebElement:
        """
        Returns the WebElement for the given site in the manage site data popup
        """
        element = self.get_element("manage-cookies-site", labels=[site])
        return element

    ## Utility Functions
    def import_bookmarks(self, browser_name: str) -> BasePage:
        """
        Press the import browser data button
        """
        MAX_TRIES = 16

        self.click_on("import-browser-data")
        sleep(2)
        tries = 0

        # Keep cycling through the options until you get it
        # Using keys for most of this because clicking elements is flaky for some reason
        while (
            browser_name.lower()
            not in self.get_element("browser-profile-selector").text.lower()
            and tries < MAX_TRIES
        ):
            self.actions.send_keys(" ").perform()
            for _ in range(tries):
                self.actions.send_keys(Keys.DOWN).perform()
            self.actions.send_keys(" ").perform()
            sleep(1)
            tries += 1

        self.click_on("migration-import-button")

        # There are two messages that indicate a successful migration
        self.wait.until(
            lambda _: self.get_element("migration-progress-header").text
            in ["Data Imported Successfully", "Data Import Complete"]
        )
        self.actions.send_keys(" ").perform()
        return self

    def click_popup_panel_button(self, field: str) -> BasePage:
        """Clicks the popup panel button for the specified field"""
        if self.iframe:
            with self.driver.switch_to.frame(self.iframe):
                self.get_element("panel-popup-button", labels=[field]).click()
        else:
            self.get_element("panel-popup-button", labels=[field]).click()
        return self


class AboutAddons(BasePage):
    """
    The POM for the about:addons page

    Attributes
    ----------
    driver: selenium.webdriver.Firefox
        WebDriver object under test
    """

    URL_TEMPLATE = "about:addons"

    def choose_sidebar_option(self, option: str):
        """
        Clicks the corresponding sidebar option from the about:addons page.
        """
        self.get_element("sidebar-options", labels=[option]).click()

    def activate_theme(
        self, nav: Navigation, theme_name: str, intended_color: str, perform_assert=True
    ):
        """
        Clicks the theme card and presses enable. Then verifies that the theme is the correct color.

        Attributes
        ----------
        nav: Navigation
            The navgiation object
        theme_name: str
            The name of the theme to press
        intended_color: str
            The RGB string that is the intended color of the element
        """
        self.get_element("theme-card", labels=[theme_name]).click()
        self.get_element("enable-theme").click()

        self.expect(
            EC.text_to_be_present_in_element_attribute(
                self.get_selector("enable-theme"), "innerText", "Disable"
            )
        )

        with self.driver.context(self.driver.CONTEXT_CHROME):
            navigation_component = nav.get_element("navigation-background-component")
            background_color = navigation_component.value_of_css_property(
                "background-color"
            )
            if perform_assert:
                assert background_color == intended_color
            else:
                return background_color

    def is_devedition(self):
        active_theme_el = self.driver.find_element(
            By.CSS_SELECTOR, ".card.addon[active] h3.addon-name"
        )
        active_theme_name = active_theme_el.text.lower()
        return "dark" in active_theme_name or "developer edition" in active_theme_name

    def enabled_theme_matches(self, expected_theme: str) -> bool:
        """
        Check the enabled theme name against any string.
        """

        enabled_theme = self.get_element("enabled-theme-title").get_attribute(
            "innerText"
        )
        return enabled_theme == expected_theme

    def check_theme_has_changed(self, original_theme: str) -> BasePage:
        """
        Ensure that the theme has changed.
        """
        assert not self.enabled_theme_matches(original_theme)
        return self
