#! /usr/bin/env python3 #
# BitBake Toaster UI tests implementation
#
# Copyright (C) 2023 Savoir-faire Linux
#
# SPDX-License-Identifier: GPL-2.0-only
#

import string
import random
import pytest
from django.urls import reverse
from selenium.webdriver import Keys
from selenium.webdriver.support.select import Select
from selenium.common.exceptions import TimeoutException
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
from selenium.webdriver.common.by import By

from .utils import get_projectId_from_url


@pytest.mark.django_db
@pytest.mark.order("last")
class TestProjectConfig(SeleniumFunctionalTestCase):
    project_id = None
    PROJECT_NAME = 'TestProjectConfig'
    INVALID_PATH_START_TEXT = 'The directory path should either start with a /'
    INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \
        'any of these characters'

    def _create_project(self, project_name):
        """ Create/Test new project using:
          - Project Name: Any string
          - Release: Any string
          - Merge Toaster settings: True or False
        """
        self.get(reverse('newproject'))
        self.wait_until_visible('#new-project-name', poll=2)
        self.find("#new-project-name").send_keys(project_name)
        select = Select(self.find("#projectversion"))
        select.select_by_value('3')

        # check merge toaster settings
        checkbox = self.find('.checkbox-mergeattr')
        if not checkbox.is_selected():
            checkbox.click()

        if self.PROJECT_NAME != 'TestProjectConfig':
            # Reset project name if it's not the default one
            self.PROJECT_NAME = 'TestProjectConfig'

        self.find("#create-project-button").click()

        try:
            self.wait_until_visible('#hint-error-project-name', poll=2)
            url = reverse('project', args=(TestProjectConfig.project_id, ))
            self.get(url)
            self.wait_until_visible('#config-nav', poll=3)
        except TimeoutException:
            self.wait_until_visible('#config-nav', poll=3)

    def _random_string(self, length):
        return ''.join(
            random.choice(string.ascii_letters) for _ in range(length)
        )

    def _get_config_nav_item(self, index):
        config_nav = self.find('#config-nav')
        return config_nav.find_elements(By.TAG_NAME, 'li')[index]

    def _navigate_bbv_page(self):
        """ Navigate to project BitBake variables page """
        # check if the menu is displayed
        if TestProjectConfig.project_id is None:
            self._create_project(project_name=self._random_string(10))
            current_url = self.driver.current_url
            TestProjectConfig.project_id = get_projectId_from_url(current_url)
        else:
            url = reverse('projectconf', args=(TestProjectConfig.project_id,))
            self.get(url)
        self.wait_until_visible('#config-nav', poll=3)
        bbv_page_link = self._get_config_nav_item(9)
        bbv_page_link.click()
        self.wait_until_visible('#config-nav', poll=3)

    def test_no_underscore_iamgefs_type(self):
        """
        Should not accept IMAGEFS_TYPE with an underscore
        """
        self._navigate_bbv_page()
        imagefs_type = "foo_bar"

        self.wait_until_visible('#change-image_fstypes-icon', poll=2)

        self.click('#change-image_fstypes-icon')

        self.enter_text('#new-imagefs_types', imagefs_type)

        element = self.wait_until_visible('#hintError-image-fs_type', poll=2)

        self.assertTrue(("A valid image type cannot include underscores" in element.text),
                        "Did not find underscore error message")

    def test_checkbox_verification(self):
        """
        Should automatically check the checkbox if user enters value
        text box, if value is there in the checkbox.
        """
        self._navigate_bbv_page()

        imagefs_type = "btrfs"

        self.wait_until_visible('#change-image_fstypes-icon', poll=2)

        self.click('#change-image_fstypes-icon')

        self.enter_text('#new-imagefs_types', imagefs_type)

        checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']")

        for checkbox in checkboxes:
            if checkbox.get_attribute("value") == "btrfs":
               self.assertEqual(checkbox.is_selected(), True)

    def test_textbox_with_checkbox_verification(self):
        """
        Should automatically add or remove value in textbox, if user checks
        or unchecks checkboxes.
        """
        self._navigate_bbv_page()

        self.wait_until_visible('#change-image_fstypes-icon', poll=2)

        self.click('#change-image_fstypes-icon')

        checkboxes_selector = '.fs-checkbox-fstypes'

        self.wait_until_visible(checkboxes_selector, poll=2)
        checkboxes = self.find_all(checkboxes_selector)

        for checkbox in checkboxes:
            if checkbox.get_attribute("value") == "cpio":
               checkbox.click()
               element = self.driver.find_element(By.ID, 'new-imagefs_types')

               self.wait_until_visible('#new-imagefs_types', poll=2)

               self.assertTrue(("cpio" in element.get_attribute('value'),
                               "Imagefs not added into the textbox"))
               checkbox.click()
               self.assertTrue(("cpio" not in element.text),
                               "Image still present in the textbox")

    def test_set_download_dir(self):
        """
        Validate the allowed and disallowed types in the directory field for
        DL_DIR
        """
        self._navigate_bbv_page()

        # activate the input to edit download dir
        try:
            change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
        except TimeoutException:
            # If download dir is not displayed, test is skipped
            change_dl_dir_btn = None

        if change_dl_dir_btn:
            change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
            change_dl_dir_btn.click()

            # downloads dir path doesn't start with / or ${...}
            input_field = self.wait_until_visible('#new-dl_dir', poll=2)
            input_field.clear()
            self.enter_text('#new-dl_dir', 'home/foo')
            element = self.wait_until_visible('#hintError-initialChar-dl_dir', poll=2)

            msg = 'downloads directory path starts with invalid character but ' \
                'treated as valid'
            self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)

            # downloads dir path has a space
            self.driver.find_element(By.ID, 'new-dl_dir').clear()
            self.enter_text('#new-dl_dir', '/foo/bar a')

            element = self.wait_until_visible('#hintError-dl_dir', poll=2)
            msg = 'downloads directory path characters invalid but treated as valid'
            self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)

            # downloads dir path starts with ${...} but has a space
            self.driver.find_element(By.ID,'new-dl_dir').clear()
            self.enter_text('#new-dl_dir', '${TOPDIR}/down foo')

            element = self.wait_until_visible('#hintError-dl_dir', poll=2)
            msg = 'downloads directory path characters invalid but treated as valid'
            self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)

            # downloads dir path starts with /
            self.driver.find_element(By.ID,'new-dl_dir').clear()
            self.enter_text('#new-dl_dir', '/bar/foo')

            hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
            self.assertEqual(hidden_element.is_displayed(), False,
                'downloads directory path valid but treated as invalid')

            # downloads dir path starts with ${...}
            self.driver.find_element(By.ID,'new-dl_dir').clear()
            self.enter_text('#new-dl_dir', '${TOPDIR}/down')

            hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
            self.assertEqual(hidden_element.is_displayed(), False,
                'downloads directory path valid but treated as invalid')

    def test_set_sstate_dir(self):
        """
        Validate the allowed and disallowed types in the directory field for
        SSTATE_DIR
        """
        self._navigate_bbv_page()

        try:
            btn_chg_sstate_dir = self.wait_until_visible(
                '#change-sstate_dir-icon',
                poll=2
            )
            self.click('#change-sstate_dir-icon')
        except TimeoutException:
            # If sstate_dir is not displayed, test is skipped
            btn_chg_sstate_dir = None

        if btn_chg_sstate_dir:  # Skip continuation if sstate_dir is not displayed
            # path doesn't start with / or ${...}
            input_field = self.wait_until_visible('#new-sstate_dir', poll=2)
            input_field.clear()
            self.enter_text('#new-sstate_dir', 'home/foo')
            element = self.wait_until_visible('#hintError-initialChar-sstate_dir', poll=2)

            msg = 'sstate directory path starts with invalid character but ' \
                'treated as valid'
            self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)

            # path has a space
            self.driver.find_element(By.ID, 'new-sstate_dir').clear()
            self.enter_text('#new-sstate_dir', '/foo/bar a')

            element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
            msg = 'sstate directory path characters invalid but treated as valid'
            self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)

            # path starts with ${...} but has a space
            self.driver.find_element(By.ID,'new-sstate_dir').clear()
            self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo')

            element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
            msg = 'sstate directory path characters invalid but treated as valid'
            self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)

            # path starts with /
            self.driver.find_element(By.ID,'new-sstate_dir').clear()
            self.enter_text('#new-sstate_dir', '/bar/foo')

            hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
            self.assertEqual(hidden_element.is_displayed(), False,
                'sstate directory path valid but treated as invalid')

            # paths starts with ${...}
            self.driver.find_element(By.ID, 'new-sstate_dir').clear()
            self.enter_text('#new-sstate_dir', '${TOPDIR}/down')

            hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
            self.assertEqual(hidden_element.is_displayed(), False,
                'sstate directory path valid but treated as invalid')

    def _change_bbv_value(self, **kwargs):
        var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values()
        """ Change bitbake variable value """
        self._navigate_bbv_page()
        self.wait_until_visible(f'#{btn_id}', poll=2)
        if kwargs.get('new_variable'):
            self.find(f"#{btn_id}").clear()
            self.enter_text(f"#{btn_id}", f"{var_name}")
        else:
            self.click(f'#{btn_id}')
            self.wait_until_visible(f'#{input_id}', poll=2)

        if kwargs.get('is_select'):
            select = Select(self.find(f'#{input_id}'))
            select.select_by_visible_text(value)
        else:
            self.find(f"#{input_id}").clear()
            self.enter_text(f'#{input_id}', f'{value}')
        self.click(f'#{save_btn}')
        value_displayed = str(self.wait_until_visible(f'#{field}').text).lower()
        msg = f'{var_name} variable not changed'
        self.assertTrue(str(value).lower() in value_displayed, msg)

    def test_change_distro_var(self):
        """ Test changing distro variable """
        self._change_bbv_value(
            var_name='DISTRO',
            field='distro',
            btn_id='change-distro-icon',
            input_id='new-distro',
            value='poky-changed',
            save_btn="apply-change-distro",
        )

    def test_set_image_install_append_var(self):
        """ Test setting IMAGE_INSTALL:append variable """
        self._change_bbv_value(
            var_name='IMAGE_INSTALL:append',
            field='image_install',
            btn_id='change-image_install-icon',
            input_id='new-image_install',
            value='bash, apt, busybox',
            save_btn="apply-change-image_install",
        )

    def test_set_package_classes_var(self):
        """ Test setting PACKAGE_CLASSES variable """
        self._change_bbv_value(
            var_name='PACKAGE_CLASSES',
            field='package_classes',
            btn_id='change-package_classes-icon',
            input_id='package_classes-select',
            value='package_deb',
            save_btn="apply-change-package_classes",
            is_select=True,
        )

    def test_create_new_bbv(self):
        """ Test creating new bitbake variable """
        self._change_bbv_value(
            var_name='New_Custom_Variable',
            field='configvar-list',
            btn_id='variable',
            input_id='value',
            value='new variable value',
            save_btn="add-configvar-button",
            new_variable=True
        )
