# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Functional, browser based tests for users app.
"""

# TODO Scenario: Add user to wiki group
# TODO Scenario: Remove user from wiki group

import subprocess
import urllib

import pytest

from plinth.tests import functional

_admin_password = functional.config['DEFAULT']['password']

pytestmark = [pytest.mark.system, pytest.mark.essential, pytest.mark.users]

_language_codes = {
    'None': '',
    'Deutsch': 'de',
    'Nederlands': 'nl',
    'Português': 'pt',
    'Türkçe': 'tr',
    'dansk': 'da',
    'español': 'es',
    'français': 'fr',
    'norsk (bokmål)': 'nb',
    'polski': 'pl',
    'svenska': 'sv',
    'Русский': 'ru',
    'తెలుగు': 'te',
    '简体中文': 'zh-hans'
}

_config_page_title_language_map = {
    '': 'General Configuration',
    'da': 'Generel Konfiguration',
    'de': 'Allgemeine Konfiguration',
    'es': 'Configuración general',
    'fr': 'Configuration générale',
    'nb': 'Generelt oppsett',
    'nl': 'Algemene Instellingen',
    'pl': 'Ustawienia główne',
    'pt': 'Configuração Geral',
    'ru': 'Общие настройки',
    'sv': 'Allmän Konfiguration',
    'te': 'సాధారణ ఆకృతీకరణ',
    'tr': 'Genel Yapılandırma',
    'zh-hans': '常规配置',
}


@pytest.fixture(scope='module', autouse=True)
def fixture_background(session_browser):
    """Unset language."""
    yield
    functional.login(session_browser)
    functional.user_set_language(session_browser, _language_codes['None'])


@pytest.fixture(scope='function', autouse=True)
def fixture_login(session_browser):
    """Login."""
    functional.login(session_browser)


@pytest.fixture(name='syncthing_installed')
def fixture_syncthing_installed(session_browser):
    """Login and install the app."""
    functional.login(session_browser)
    functional.install(session_browser, 'syncthing')
    functional.app_enable(session_browser, 'syncthing')
    yield
    functional.app_disable(session_browser, 'syncthing')


def test_app_access(session_browser, syncthing_installed):
    """Test that only logged-in users can access Syncthing web interface."""
    functional.logout(session_browser)
    functional.access_url(session_browser, 'syncthing')
    assert functional.is_login_prompt(session_browser)

    functional.login(session_browser)
    functional.access_url(session_browser, 'syncthing')
    assert functional.is_available(session_browser, 'syncthing')


def test_create_user(session_browser):
    """Test creating a user."""
    _delete_user(session_browser, 'alice')

    functional.create_user(session_browser, 'alice', email='alice@example.com')
    assert functional.user_exists(session_browser, 'alice')
    assert _get_email(session_browser, 'alice') == 'alice@example.com'


def test_rename_user(session_browser, host_sudo):
    """Test renaming a user."""
    _non_admin_user_exists(session_browser, 'alice')
    _delete_user(session_browser, 'bob')

    _rename_user(session_browser, 'alice', 'bob')
    assert not functional.user_exists(session_browser, 'alice')
    assert functional.user_exists(session_browser, 'bob')

    assert 'cn: bob' in host_sudo.check_output('ldapfinger bob')
    assert host_sudo.user('bob').gecos == 'bob'
    assert host_sudo.user('bob').home == '/home/bob'


def test_admin_users_can_change_own_ssh_keys(session_browser):
    """Test that admin users can change their own ssh keys."""
    _set_ssh_keys(session_browser, 'somekey123')
    assert _get_ssh_keys(session_browser) == 'somekey123'


def test_non_admin_users_can_change_own_ssh_keys(session_browser):
    """Test that non-admin users can change their own ssh keys."""
    _non_admin_user_exists(session_browser, 'alice')
    functional.login_with_account(session_browser, functional.base_url,
                                  'alice')
    _set_ssh_keys(session_browser, 'somekey456')
    assert _get_ssh_keys(session_browser) == 'somekey456'


def test_admin_users_can_change_other_users_ssh_keys(session_browser):
    """Test that admin users can change other user's ssh keys."""
    _non_admin_user_exists(session_browser, 'alice')
    _set_ssh_keys(session_browser, 'alicesomekey123', username='alice')
    assert _get_ssh_keys(session_browser,
                         username='alice') == 'alicesomekey123'


def test_users_can_remove_ssh_keys(session_browser):
    """Test that users can remove ssh keys."""
    _set_ssh_keys(session_browser, 'somekey123')
    _set_ssh_keys(session_browser, '')
    assert _get_ssh_keys(session_browser) == ''


def test_users_can_connect_passwordless_over_ssh(session_browser,
                                                 tmp_path_factory):
    """Test that users can connect passwordless over ssh if the keys are
    set."""
    functional.app_enable(session_browser, 'ssh')
    _configure_ssh_keys(session_browser, tmp_path_factory)
    _should_connect_passwordless_over_ssh(session_browser, tmp_path_factory)


def test_ssh_passwordless_after_user_rename(session_browser, tmp_path_factory):
    """Test that users can connect passwordless after user is renamed."""
    username_old = 'bob'
    username_new = 'bob2'
    functional.app_enable(session_browser, 'ssh')
    _non_admin_user_exists(session_browser, username_old,
                           groups=['freedombox-ssh'])
    _delete_user(session_browser, username_new)
    _configure_ssh_keys(session_browser, tmp_path_factory,
                        username=username_old)
    _should_connect_passwordless_over_ssh(session_browser, tmp_path_factory,
                                          username=username_old)

    _rename_user(session_browser, username_old, username_new)

    assert functional.user_exists(session_browser, username_new)
    _should_connect_passwordless_over_ssh(session_browser, tmp_path_factory,
                                          username=username_new)


def test_users_cannot_connect_passwordless_over_ssh(session_browser,
                                                    tmp_path_factory):
    """Test that users cannot connect passwordless over ssh if the keys aren't
    set."""
    functional.app_enable(session_browser, 'ssh')
    _configure_ssh_keys(session_browser, tmp_path_factory)
    _set_ssh_keys(session_browser, '')
    _should_not_connect_passwordless_over_ssh(session_browser,
                                              tmp_path_factory)


def test_update_user(session_browser):
    """Test changing properties of a user."""
    functional.create_user(session_browser, 'alice', email='alice@example.com')

    # Update email
    _set_email(session_browser, 'alice', 'alice1@example.com')
    assert _get_email(session_browser, 'alice') == 'alice1@example.com'
    _set_email(session_browser, 'alice', 'alice2@example.com')
    assert _get_email(session_browser, 'alice') == 'alice2@example.com'


@pytest.mark.parametrize('language_code', _language_codes.values())
def test_change_language(session_browser, language_code):
    """Test changing the language."""
    functional.user_set_language(session_browser, language_code)
    assert _check_language(session_browser, language_code)


def test_user_states(session_browser, tmp_path_factory):
    """Test that admin users can set other users as inactive/active."""
    username = 'bob2'
    _non_admin_user_exists(session_browser, username,
                           groups=['freedombox-ssh'])
    _configure_ssh_keys(session_browser, tmp_path_factory, username=username)

    # Test set user inactive
    _set_user_status(session_browser, username, 'inactive')
    # Test Django login
    _cannot_log_in(session_browser, username)
    # Test PAM/nslcd authorization
    _should_not_connect_passwordless_over_ssh(session_browser,
                                              tmp_path_factory,
                                              username=username)

    # Test set user active
    functional.login(session_browser)
    _set_user_status(session_browser, username, 'active')
    _can_log_in(session_browser, username)
    _should_connect_passwordless_over_ssh(session_browser, tmp_path_factory,
                                          username=username)


def test_admin_users_can_change_own_password(session_browser):
    """Test that admin users can change their own password."""
    _admin_user_exists(session_browser, 'testadmin')
    functional.login_with_account(session_browser, functional.base_url,
                                  'testadmin')
    _change_password(session_browser, 'newpassword456')
    _can_log_in_with_password(session_browser, 'testadmin', 'newpassword456')


def test_admin_users_can_change_others_password(session_browser):
    """Test that admin users can change other user's password."""
    _non_admin_user_exists(session_browser, 'alice')
    _change_password(session_browser, 'secretsecret567', username='alice')
    _can_log_in_with_password(session_browser, 'alice', 'secretsecret567')


def test_non_admin_users_can_change_own_password(session_browser):
    """Test that non-admin users can change their own password."""
    _non_admin_user_exists(session_browser, 'alice')
    functional.login_with_account(session_browser, functional.base_url,
                                  'alice')
    _change_password(session_browser, 'newpassword123')
    _can_log_in_with_password(session_browser, 'alice', 'newpassword123')


def test_delete_user(session_browser):
    """Test deleting a user."""
    _non_admin_user_exists(session_browser, 'alice')
    functional.delete_user(session_browser, 'alice')
    assert not functional.user_exists(session_browser, 'alice')


def _delete_user(session_browser, name):
    """Delete a user."""
    if functional.user_exists(session_browser, name):
        functional.delete_user(session_browser, name)


def _admin_user_exists(session_browser, name):
    _delete_user(session_browser, name)
    functional.create_user(session_browser, name, groups=['admin'])


def _non_admin_user_exists(session_browser, name, groups=[]):
    _delete_user(session_browser, name)
    functional.create_user(session_browser, name, groups=groups)


def _generate_ssh_keys(session_browser, key_file):
    try:
        key_file.unlink()
    except FileNotFoundError:
        pass

    subprocess.check_call(
        ['ssh-keygen', '-t', 'ed25519', '-N', '', '-q', '-f',
         str(key_file)])


def _configure_ssh_keys(session_browser, tmp_path_factory, username=None):
    key_file = tmp_path_factory.getbasetemp() / 'users-ssh.key'
    _generate_ssh_keys(session_browser, key_file)
    public_key_file = key_file.with_suffix(key_file.suffix + '.pub')
    public_key = public_key_file.read_text()
    _set_ssh_keys(session_browser, public_key, username=username)


def _can_log_in(session_browser, username):
    functional.login_with_account(session_browser, functional.base_url,
                                  username)
    assert len(session_browser.find_by_id('id_user_menu')) > 0


def _can_log_in_with_password(session_browser, username, password):
    functional.logout(session_browser)
    functional.login_with_account(session_browser, functional.base_url,
                                  username, password)
    assert len(session_browser.find_by_id('id_user_menu')) > 0


def _cannot_log_in(session_browser, username):
    functional.login_with_account(session_browser, functional.base_url,
                                  username)
    assert len(session_browser.find_by_id('id_user_menu')) == 0


def _should_connect_passwordless_over_ssh(session_browser, tmp_path_factory,
                                          username=None):
    key_file = tmp_path_factory.getbasetemp() / 'users-ssh.key'
    _try_login_to_ssh(key_file=key_file, username=username)


def _should_not_connect_passwordless_over_ssh(session_browser,
                                              tmp_path_factory, username=None):
    key_file = tmp_path_factory.getbasetemp() / 'users-ssh.key'
    with pytest.raises(subprocess.CalledProcessError):
        _try_login_to_ssh(key_file=key_file, username=username)


def _rename_user(browser, old_name, new_name):
    functional.nav_to_module(browser, 'users')
    functional.click_link_by_href(browser,
                                  f'/freedombox/sys/users/{old_name}/edit/')
    browser.find_by_id('id_username').fill(new_name)
    browser.find_by_id('id_confirm_password').fill(_admin_password)
    functional.submit(browser, form_class='form-update')


def _set_email(browser, username, email):
    """Set the email field value for a user."""
    functional.visit(browser,
                     '/freedombox/sys/users/{}/edit/'.format(username))
    browser.find_by_id('id_email').fill(email)
    browser.find_by_id('id_confirm_password').fill(_admin_password)
    functional.submit(browser, form_class='form-update')


def _get_email(browser, username):
    """Return the email field value for a user."""
    functional.visit(browser,
                     '/freedombox/sys/users/{}/edit/'.format(username))
    return browser.find_by_id('id_email').value


def _check_language(browser, language_code):
    functional.nav_to_module(browser, 'config')
    return browser.find_by_css('.app-titles').first.find_by_tag(
        'h2').first.value == _config_page_title_language_map[language_code]


def _get_ssh_keys(browser, username=None):
    functional.visit(browser, '/freedombox/')
    if username is None:
        browser.find_by_id('id_user_menu').click()
        functional.click_and_wait(browser,
                                  browser.find_by_id('id_user_edit_menu'))
    else:
        functional.visit(browser, f'/freedombox/sys/users/{username}/edit/')
    return browser.find_by_id('id_ssh_keys').text


def _set_ssh_keys(browser, ssh_keys, username=None):
    if username is None:
        browser.find_by_id('id_user_menu').click()
        functional.click_and_wait(browser,
                                  browser.find_by_id('id_user_edit_menu'))
    else:
        functional.visit(browser, f'/freedombox/sys/users/{username}/edit/')

    current_user = browser.find_by_id('id_user_menu_link').text
    auth_password = functional.get_password(current_user)

    browser.find_by_id('id_ssh_keys').fill(ssh_keys)
    browser.find_by_id('id_confirm_password').fill(auth_password)

    functional.submit(browser, form_class='form-update')


def _set_user_status(browser, username, status):
    functional.visit(browser,
                     '/freedombox/sys/users/{}/edit/'.format(username))
    if status == 'inactive':
        browser.find_by_id('id_is_active').uncheck()
    elif status == 'active':
        browser.find_by_id('id_is_active').check()

    browser.find_by_id('id_confirm_password').fill(_admin_password)
    functional.submit(browser, form_class='form-update')


def _change_password(browser, new_password, current_password=None,
                     username=None):
    if username is None:
        browser.find_by_id('id_user_menu').click()
        functional.click_and_wait(
            browser, browser.find_by_id('id_change_password_menu'))
    else:
        functional.visit(
            browser,
            '/freedombox/sys/users/{}/change_password/'.format(username))

    current_user = browser.find_by_id('id_user_menu_link').text
    auth_password = current_password or functional.get_password(current_user)

    browser.find_by_id('id_new_password1').fill(new_password)
    browser.find_by_id('id_new_password2').fill(new_password)
    browser.find_by_id('id_confirm_password').fill(auth_password)
    functional.submit(browser, form_class='form-change-password')


def _try_login_to_ssh(key_file=None, username=None):
    user = username if username else functional.config['DEFAULT']['username']
    hostname = urllib.parse.urlparse(
        functional.config['DEFAULT']['url']).hostname
    port = functional.config['DEFAULT']['ssh_port']

    subprocess.check_call([
        'ssh', '-p', port, '-i', key_file, '-q', '-o',
        'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o',
        'BatchMode=yes', '{0}@{1}'.format(user, hostname), '/bin/true'
    ])
