#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import os
import signal
import socket
import subprocess
from threading import Thread
import time

import pytest
from selenium.webdriver.support.ui import Select
import xvfbwrapper

from horizon.test import webdriver
from openstack_dashboard.test.integration_tests import config as horizon_config
from openstack_dashboard.test.selenium import widgets


STASH_FAILED = pytest.StashKey[bool]()


class Session:
    def __init__(self, driver, config):
        self.current_user = None
        self.current_project = None
        self.driver = driver
        self.config = config
        self.credentials = {
            'user': (
                config.identity.username,
                config.identity.password,
                config.identity.home_project,
            ),
            'admin': (
                config.identity.admin_username,
                config.identity.admin_password,
                config.identity.admin_home_project,
            ),
        }
        self.oidc_credentials = {
            'user': (
                config.OIDC.keycloak_test_user1_username,
                config.OIDC.keycloak_test_user1_password,
                config.OIDC.keycloak_test_user_home_project,
            ),
        }
        self.project_name_xpath = config.theme.project_name_xpath
        self.logout_url = '/'.join((
            config.dashboard.dashboard_url,
            'auth',
            'logout',
        ))

    def login(self, user, project=None):
        if project is None:
            project = self.credentials[user][2]
        if self.current_user != user:
            username, password, home_project = self.credentials[user]
            self.driver.get(self.logout_url)
            user_field = self.driver.find_element_by_id('id_username')
            user_field.send_keys(username)
            pass_field = self.driver.find_element_by_id('id_password')
            pass_field.send_keys(password)
            button = self.driver.find_element_by_css_selector(
                '.btn-primary')
            button.click()
            self.current_user = user
            project_element = self.driver.find_element_by_xpath(
                self.project_name_xpath)
            self.current_project = project_element.text
        if self.current_project != project:
            project_element = self.driver.find_element_by_xpath(
                self.project_name_xpath)
            project_element.click()
            selection = project_element.find_element_by_xpath(
                f'.//*[normalize-space()="{project}"]')
            selection.click()
            widgets.get_and_dismiss_messages(self.driver, self.config)
            self.current_project = self.driver.find_element_by_xpath(
                self.project_name_xpath).text

    def login_oidc(self, user, project=None):
        # Keycloak/OIDC login
        username, password, home_project = self.oidc_credentials[user]
        if project is None:
            project = home_project
        self.driver.get(self.logout_url)
        select_auth = self.driver.find_element_by_id('id_auth_type')
        select_auth.click()
        select_opt = Select(select_auth)
        select_opt.select_by_visible_text('OpenID Connect')
        button = self.driver.find_element_by_css_selector(
            '.btn-primary')
        button.click()
        keycloak_user_field = self.driver.find_element_by_id('username')
        keycloak_user_field.send_keys(username)
        keycloak_pass_field = self.driver.find_element_by_id('password')
        keycloak_pass_field.send_keys(password)
        kc_login_button = self.driver.find_element_by_id('kc-login')
        kc_login_button.click()


@pytest.fixture(scope='session')
def login(driver, config):
    session = Session(driver, config)
    return session.login


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """A hook to save the failure state of a test."""
    # execute all other hooks to obtain the report object
    outcome = yield
    rep = outcome.get_result()

    item.stash[STASH_FAILED] = item.stash.get(STASH_FAILED, False) or rep.failed


@pytest.fixture(scope='function', autouse=True)
def save_screenshot(request, report_dir, driver):
    yield None
    if not request.node.stash.get(STASH_FAILED, False):
        return
    screen_path = os.path.join(report_dir, 'screenshot.png')
    driver.get_screenshot_as_file(screen_path)


@pytest.fixture(scope='function', autouse=True)
def save_page_source(request, report_dir, driver):
    yield None
    if not request.node.stash.get(STASH_FAILED, False):
        return
    source_path = os.path.join(report_dir, 'page.html')
    html_elem = driver.find_element_by_tag_name("html")
    page_source = html_elem.get_property("innerHTML")
    with open(source_path, 'w') as f:
        f.write(page_source)


@pytest.fixture(scope='function', autouse=True)
def record_video(request, report_dir, xdisplay):
    if not os.environ.get('FFMPEG_INSTALLED', False):
        yield None
        return
    filepath = os.path.join(report_dir, 'video.mp4')
    frame_rate = 15
    display, width, height = xdisplay
    command = [
        'ffmpeg',
        '-video_size', f'{width}x{height}',
        '-framerate', str(frame_rate),
        '-f', 'x11grab',
        '-i', f':{display}',  # noqa: E231
        filepath,
    ]
    fnull = open(os.devnull, 'w')
    popen = subprocess.Popen(command, stdout=fnull, stderr=fnull)
    yield None
    popen.send_signal(signal.SIGINT)

    def terminate_process():
        limit = time.time() + 10
        while time.time() < limit:
            time.sleep(0.1)
            if popen.poll() is not None:
                return
        os.kill(popen.pid, signal.SIGTERM)

    thread = Thread(target=terminate_process)
    thread.start()
    popen.communicate()
    thread.join()
    if not request.node.stash.get(STASH_FAILED, False):
        try:
            os.remove(filepath)
        except OSError:
            pass


@pytest.fixture(scope='session')
def xdisplay():
    IS_SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', "True")
    if IS_SELENIUM_HEADLESS.lower() == "true":
        width, height = 1920, 1080
        vdisplay = xvfbwrapper.Xvfb(width=width, height=height)
        args = []

        # workaround for memory leak in Xvfb taken from:
        # http://blog.jeffterrace.com/2012/07/xvfb-memory-leak-workaround.html
        args.append("-noreset")

        # disables X access control
        args.append("-ac")

        if hasattr(vdisplay, 'extra_xvfb_args'):
            # xvfbwrapper 0.2.8 or newer
            vdisplay.extra_xvfb_args.extend(args)
        else:
            vdisplay.xvfb_cmd.extend(args)
        vdisplay.start()
        display = vdisplay.new_display
    else:
        width, height = subprocess.check_output(
            'xdpyinfo | grep "dimensions:"', shell=True
        ).decode().split(':', 1)[1].split()[0].strip().split('x')
        vdisplay = None
        display = subprocess.check_output(
            'xdpyinfo | grep "name of display:"', shell=True
        ).decode().split(':', 1)[1].strip()
    yield display, width, height
    if vdisplay:
        vdisplay.stop()


@pytest.fixture(scope='session')
def config():
    return horizon_config.get_config()


@pytest.fixture(scope='function')
def report_dir(request, config):
    root_path = os.path.dirname(os.path.abspath(horizon_config.__file__))
    test_name = request.node.nodeid.rsplit('/', 1)[1]
    report_dir = os.path.join(
        root_path, config.selenium.screenshots_directory, test_name)
    if not os.path.isdir(report_dir):
        os.makedirs(report_dir)
    yield report_dir
    try:
        os.rmdir(report_dir)  # delete if empty
    except OSError:
        pass


@pytest.fixture(scope='session')
def driver(config, xdisplay):
    # Start a virtual display server for running the tests headless.
    IS_SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', False)
    # Increase the default Python socket timeout from nothing
    # to something that will cope with slow webdriver startup times.
    # This *just* affects the communication between this test process
    # and the webdriver.
    socket.setdefaulttimeout(60)
    # Start the Selenium webdriver and setup configuration.
    desired_capabilities = dict(webdriver.desired_capabilities)
    desired_capabilities['loggingPrefs'] = {'browser': 'ALL'}
    driver = webdriver.WebDriver(
        desired_capabilities=desired_capabilities
    )
    if config.selenium.maximize_browser:
        driver.maximize_window()
        if IS_SELENIUM_HEADLESS:  # force full screen in xvfb
            display, width, height = xdisplay
            driver.set_window_size(width, height)

    driver.implicitly_wait(config.selenium.implicit_wait)
    driver.set_page_load_timeout(config.selenium.page_timeout)
    yield driver
    driver.quit()
