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

import json
import os
import shutil
import tempfile
import time
import unittest
from contextlib import contextmanager
from unittest import mock
from urllib.parse import urlencode

import cherrypy
import cherrypy.test.helper
import ldap3
import responses
from selenium import webdriver

from bfh_science_ssp.app import SspApplication
from bfh_science_ssp.core.user import User
from bfh_science_ssp.main import parse_args

BaseClass = cherrypy.test.helper.CPWebCase
del BaseClass.test_gc


class WebCase(BaseClass):
    """
    Helper class for the test suite.
    """

    interactive = False  # Disable interactive test

    login = True  # Login by default.
    username = 'test-user'
    password = 'my-password'

    default_config = {'debug': False}

    @classmethod
    def setup_class(cls):
        if cls is WebCase:
            raise unittest.SkipTest("%s is an abstract base class" % cls.__name__)
        super().setup_class()

    @classmethod
    def teardown_class(cls):
        cherrypy.ldap.uri = None
        # Release Ldap mock server.
        cls.patcher.stop()
        return super().teardown_class()

    @classmethod
    def setup_server(cls):
        # Configure Ldap Mock server early.
        cls.server = ldap3.Server('my_fake_server')
        cls.conn = ldap3.Connection(cls.server, client_strategy=ldap3.MOCK_ASYNC, raise_exceptions=True)
        cls.patcher = mock.patch('ldap3.Connection', return_value=cls.conn)
        cls.patcher.start()

        # Get defaultconfig from test class
        default_config = getattr(cls, 'default_config', {})
        default_config['ldap-uri'] = 'my_fake_server'
        default_config['ldap-base-dn'] = 'dc=bfh'
        if 'rate-limit' not in default_config:
            default_config['rate-limit'] = -1
        cfg = parse_args(args=[], config_file_contents='\n'.join('%s=%s' % (k, v) for k, v in default_config.items()))
        app = SspApplication(cfg)
        cherrypy.tree.mount(app)

    def setUp(self):
        super().setUp()
        # Clear LDAP entries before test.
        for entry in list(cherrypy.ldap._pool.strategy.connection.server.dit):
            if entry == 'cn=schema':
                continue
            cherrypy.ldap._pool.strategy.remove_entry(entry)
        # Create a default user
        self.conn.strategy.add_entry(
            f'cn={self.username},ou=Staff,ou=Accounts,dc=bfh',
            {
                'userPassword': self.password,
                'uid': [self.username],
                'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
                'mail': 'test@example.com',
                'o': 'MyDepartement',
                'bfhSection': 'MySection',
                'bfhAccountType': User.TYPE_STAFF,
            },
        )
        # Login if required
        if self.login:
            self._login()

        # Mock web server (Znuny)
        self.responses = responses.RequestsMock()
        self.responses.start()

        # Mock email listener
        self.listener = mock.MagicMock()
        cherrypy.engine.subscribe('send_mail', self.listener.send_mail, priority=50)
        return super().setUp()

    def tearDown(self):
        # Delete selenium download
        if getattr(self, '_selenium_download_dir', False):
            shutil.rmtree(self._selenium_download_dir)
            self._selenium_download_dir = None
        # Stop and reset mock server
        self.responses.stop()
        self.responses.reset()

        # Stop email listener
        cherrypy.engine.unsubscribe('send_mail', self.listener.send_mail)
        return super().tearDown()

    @property
    def session(self):
        return cherrypy.db.get_session()

    @property
    def baseurl(self):
        return 'http://%s:%s' % (self.HOST, self.PORT)

    def getPage(self, url, headers=None, method="GET", body=None, protocol=None):
        if headers is None:
            headers = []
        # When body is a dict, send the data as form data.
        if isinstance(body, dict) and method in ['POST', 'PUT']:
            data = []
            for k, v in body.items():
                if isinstance(v, list):
                    data.extend(
                        [
                            (
                                str(k).encode(encoding='latin1'),
                                str(item).encode(encoding='utf-8'),
                            )
                            for item in v
                        ]
                    )
                else:
                    data.append(
                        (
                            str(k).encode(encoding='latin1'),
                            str(v).encode(encoding='utf-8'),
                        )
                    )
            body = urlencode(data)
        # Send back cookies if any
        if hasattr(self, 'cookies') and self.cookies:
            headers.extend(self.cookies)
        # CherryPy ~8.9.1 is not handling absolute URL properly and web browser
        # are usually not sending absolute URL either. So trim the base.
        if url.startswith(self.baseurl):
            url = url[len(self.baseurl) : :]
        super().getPage(url, headers, method, body, protocol)

    def getJson(self, *args, **kwargs):
        self.getPage(*args, **kwargs)
        self.assertStatus(200)
        self.assertHeader('Content-Type', 'application/json')
        return json.loads(self.body.decode('utf8'))

    def _login(self, login=username, password=password, redirect='/'):
        # Authenticate
        self.getPage("/logout", method="POST")
        self.getPage("/login/", method='POST', body={'login': login, 'password': password})
        self.assertStatus('303 See Other')

    @property
    def session_id(self):
        if hasattr(self, 'cookies') and self.cookies:
            for unused, value in self.cookies:
                for part in value.split(';'):
                    key, unused, value = part.partition('=')
                    if key == 'session_id':
                        return value

    @contextmanager
    def selenium(self, headless=True, implicitly_wait=3):
        """
        Decorator to load selenium for a test.
        """
        # Skip selenium test is display is not available.
        if not os.environ.get('DISPLAY', False):
            raise unittest.SkipTest("selenium require a display")
        # Start selenium driver
        options = webdriver.ChromeOptions()
        if headless:
            options.add_argument('--headless')
        options.add_argument('--disable-gpu')
        options.add_argument('--window-size=1280,800')
        options.add_argument('--no-sandbox')
        options.add_argument('--disable-dev-shm-usage')
        # Debian python3-selenium doesn't include `selenium manager`.
        # So we need to find 'chromedriver' executable.
        chromedriver_bin = shutil.which('chromedriver')
        service = webdriver.ChromeService(executable_path=chromedriver_bin)
        driver = webdriver.Chrome(options=options, service=service)
        try:
            # If logged in, reuse the same session id.
            if self.session_id:
                driver.get(f'{self.baseurl}/login/')
                driver.add_cookie({"name": "session_id", "value": self.session_id})
            # Configure download folder
            download = os.path.join(os.path.expanduser('~'), 'Downloads')
            os.makedirs(download, exist_ok=True)
            self._selenium_download_dir = tempfile.mkdtemp(dir=download, prefix='udb-selenium-download-')
            driver.execute_cdp_cmd(
                'Page.setDownloadBehavior', {'behavior': 'allow', 'downloadPath': self._selenium_download_dir}
            )
            # Set default wait.
            driver.implicitly_wait(implicitly_wait)
            # Clear existing log.
            driver.get_log('browser')
            yield driver
        finally:
            # Code to release resource, e.g.:
            driver.close()
            driver = None

    def assertSeleniumDownload(self, timeout=10):
        """
        Check number of file in download folder
        """
        # Wait until file get downloaded.
        # Make sure to ignore '.crdownload'
        for unused in range(1, timeout):
            files = [f for f in os.listdir(self._selenium_download_dir) if not f.endswith('.crdownload')]
            if len(files) > 0:
                break
            time.sleep(1)
        files = os.listdir(self._selenium_download_dir)
        self.assertTrue(files)
        return files[0]
