import os
import re
import time

import pytest
import testinfra.utils.ansible_runner


testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')


@pytest.mark.parametrize("expected_group, expected_group_id", [
    ('web-parameters-mandatory', 1003),
    ('web-parameters-optional_local', 5001),
    ('web-parameters-paste-req', 5002),
])
def test_website_group(host, expected_group, expected_group_id):
    """
    Tests if website group has been created correctly.
    """

    group = host.group(expected_group)

    assert group.exists
    assert group.gid == expected_group_id


@pytest.mark.parametrize("fqdn, expected_group, expected_admin, expected_user_id", [
    ('parameters-mandatory', 'web-parameters-mandatory', 'admin-parameters-mandatory', 1003),
    ('parameters-optional.local', 'web-parameters-optional_local', 'admin-parameters-optional_local', 5000),
    ('parameters-paste-req', 'web-parameters-paste-req', 'admin-parameters-paste-req', 5002)
])
def test_website_admin_user(host, fqdn, expected_group, expected_admin, expected_user_id):

    user = host.user(expected_admin)

    assert user.exists
    assert user.uid == expected_user_id
    assert user.group == expected_group
    assert user.groups == [expected_group]
    assert user.shell == '/bin/bash'
    assert user.home == '/var/www/' + fqdn


@pytest.mark.parametrize("home, expected_owner, expected_group", [
    ('/var/www/parameters-mandatory', 'admin-parameters-mandatory', 'web-parameters-mandatory'),
    ('/var/www/parameters-optional.local', 'admin-parameters-optional_local', 'web-parameters-optional_local'),
    ('/var/www/parameters-paste-req', 'admin-parameters-paste-req', 'web-parameters-paste-req'),
])
def test_website_admin_home(host, home, expected_owner, expected_group):
    """
    Tests if permissions on website admin home directory are correct.
    """

    home = host.file(home)

    assert home.is_directory
    assert home.user == expected_owner
    assert home.group == expected_group
    assert home.mode == 0o750


@pytest.mark.parametrize("profile_dir, expected_owner, expected_group", [
    ('/var/www/parameters-mandatory/.profile.d', 'admin-parameters-mandatory', 'web-parameters-mandatory'),
    ('/var/www/parameters-optional.local/.profile.d', 'admin-parameters-optional_local', 'web-parameters-optional_local'),
    ('/var/www/parameters-paste-req/.profile.d', 'admin-parameters-paste-req', 'web-parameters-paste-req'),
])
def test_home_profile_directory(host, profile_dir, expected_owner, expected_group):
    """
    Tests if profile directory has been set-up correctly for the website
    administrator/application user.
    """

    with host.sudo():

        directory = host.file(profile_dir)
        assert directory.is_directory
        assert directory.user == expected_owner
        assert directory.group == expected_group
        assert directory.mode == 0o750


@pytest.mark.parametrize("profile_file, expected_group", [
    ('/var/www/parameters-mandatory/.profile.d/virtualenv.sh', 'web-parameters-mandatory'),
    ('/var/www/parameters-optional.local/.profile.d/virtualenv.sh', 'web-parameters-optional_local'),
    ('/var/www/parameters-paste-req/.profile.d/virtualenv.sh', 'web-parameters-paste-req'),
])
def test_virtualenv_profile_configuration(host, profile_file, expected_group):
    """
    Tests if profile configuration file for auto-activation of virtual
    environment has been deployed correctly.
    """

    with host.sudo():

        config = host.file(profile_file)
        assert config.is_file
        assert config.user == 'root'
        assert config.group == expected_group
        assert config.mode == 0o640


@pytest.mark.parametrize("profile_file, expected_group, expected_content", [
    ('/var/www/parameters-mandatory/.profile.d/environment.sh', 'web-parameters-mandatory', '\n'),
    ('/var/www/parameters-optional.local/.profile.d/environment.sh', 'web-parameters-optional_local', "export MY_ENV_VAR='My environment variable'\n"),
    ('/var/www/parameters-paste-req/.profile.d/environment.sh', 'web-parameters-paste-req', '\n'),
])
def test_environment_profile_configuration(host, profile_file, expected_group, expected_content):
    """
    Tests if profile configuration file for setting-up environment variables has
    been deployed correctly.
    """

    with host.sudo():

        config = host.file(profile_file)
        assert config.is_file
        assert config.user == 'root'
        assert config.group == expected_group
        assert config.mode == 0o640
        assert config.content_string == expected_content


@pytest.mark.parametrize("admin_user, expected_virtualenv_path", [
    ('admin-parameters-mandatory', '/var/www/parameters-mandatory/virtualenv\n'),
    ('admin-parameters-optional_local', '/var/www/parameters-optional.local/virtualenv\nMy environment variable\n'),
    ('admin-parameters-paste-req', '/var/www/parameters-paste-req/virtualenv\n'),
])
def test_profile_configuration(host, admin_user, expected_virtualenv_path):
    """
    Tests if profile configuration is behaving correctly (setting appropriate
    vars via login shell).
    """

    env = host.run("sudo -i -u " + admin_user + " printenv VIRTUAL_ENV MY_ENV_VAR")
    assert env.stdout == expected_virtualenv_path


@pytest.mark.parametrize("expected_group", [
    'web-parameters-mandatory',
    'web-parameters-optional_local',
    'web-parameters-paste-req'
])
def test_nginx_user(host, expected_group):
    """
    Tests if web server user has been added to website group.
    """

    user = host.user('www-data')
    assert expected_group in user.groups


@pytest.mark.parametrize("forward_file, expected_group, expected_content", [
    ('/var/www/parameters-mandatory/.forward', 'web-parameters-mandatory', 'root\n'),
    ('/var/www/parameters-optional.local/.forward', 'web-parameters-optional_local', 'user\n'),
    ('/var/www/parameters-paste-req/.forward', 'web-parameters-paste-req', 'root\n'),
])
def test_forward_file(host, forward_file, expected_group, expected_content):
    """
    Tests if the forward file has correct permissions and content.
    """

    with host.sudo():

        config = host.file(forward_file)

        assert config.is_file
        assert config.user == 'root'
        assert config.group == expected_group
        assert config.mode == 0o640
        assert config.content_string == expected_content


@pytest.mark.parametrize("original_destination, expected_destination_user", [
    ('web-parameters-mandatory@localhost', 'vagrant'),
    ('web-parameters-optional_local@localhost', 'user'),
    ('web-parameters-paste-req@localhost', 'vagrant'),
])
def test_mail_forwarding(host, original_destination, expected_destination_user):
    """
    Tests if mail forwarding works as expected.
    """

    hostname = host.run('hostname').stdout.strip()

    send = host.run('swaks --suppress-data --to ' + original_destination)
    assert send.rc == 0
    original_queue_id = re.search('Ok: queued as (.*)', send.stdout).group(1)

    # Sleep for a couple of seconds so the mail can get delivered.
    time.sleep(5)

    with host.sudo():
        mail_log = host.file('/var/log/mail.log')

        # First extract message ID of forwarded mail.
        pattern = r"%s: to=<%s>.*status=sent \(forwarded as ([^)]*)\)" % (original_queue_id, original_destination)
        forward_queue_id = re.search(pattern, mail_log.content_string).group(1)

        # Now try to determine where the forward ended-up at.
        pattern = "%s: to=<%s@%s>, orig_to=<%s>.*status=sent" % (forward_queue_id, expected_destination_user, hostname, original_destination)
        assert re.search(pattern, mail_log.content_string) is not None


@pytest.mark.parametrize("virtualenv_dir, expected_owner, expected_group", [
    ('/var/www/parameters-mandatory/virtualenv', 'admin-parameters-mandatory', 'web-parameters-mandatory'),
    ('/var/www/parameters-optional.local/virtualenv', 'admin-parameters-optional_local', 'web-parameters-optional_local'),
    ('/var/www/parameters-paste-req/virtualenv', 'admin-parameters-paste-req', 'web-parameters-paste-req'),
])
def test_python_virtualenv_created(host, virtualenv_dir, expected_owner, expected_group):
    """
    Tests if Python virtual environment has been created correctly.
    """

    with host.sudo():

        virtualenv = host.file(virtualenv_dir)
        assert virtualenv.is_directory
        assert virtualenv.user == expected_owner
        assert virtualenv.group == expected_group
        assert virtualenv.mode == 0o2750

        virtualenv_activate = host.file(virtualenv_dir + "/bin/activate")
        assert virtualenv_activate.is_file
        assert virtualenv_activate.user == expected_owner
        assert virtualenv_activate.group == expected_group
        assert virtualenv_activate.mode == 0o640


@pytest.mark.parametrize("project_file, expected_owner, expected_group", [
    ('/var/www/parameters-mandatory/virtualenv/.project', 'admin-parameters-mandatory', 'web-parameters-mandatory'),
    ('/var/www/parameters-optional.local/virtualenv/.project', 'admin-parameters-optional_local', 'web-parameters-optional_local'),
    ('/var/www/parameters-paste-req/virtualenv/.project', 'admin-parameters-paste-req', 'web-parameters-paste-req'),
])
def test_python_virtualenv_project_directory_config(host, project_file, expected_owner, expected_group):
    """
    Tests if project directory configuration within virtualenv is set-up
    correctly.
    """

    with host.sudo():

        project = host.file(project_file)

        assert project.is_file
        assert project.user == expected_owner
        assert project.group == expected_group
        assert project.mode == 0o640


@pytest.mark.parametrize("virtualenv_dir, admin_user, expected_prompt", [
    ('/var/www/parameters-mandatory/virtualenv', 'admin-parameters-mandatory', '(parameters-mandatory) '),
    ('/var/www/parameters-optional.local/virtualenv', 'admin-parameters-optional_local', '(parameters-optional.local) '),
    ('/var/www/parameters-paste-req/virtualenv', 'admin-parameters-paste-req', '(parameters-paste-req) '),
])
def test_python_virtualenv_prompt(host, virtualenv_dir, admin_user, expected_prompt):
    """
    Tests if Python virtual environment prompt has been set-up
    correctly.
    """

    with host.sudo(admin_user):
        prompt = host.run('bash -c "source %s/bin/activate; printenv PS1"', virtualenv_dir)

        # Chop off trailing newline if present (this is from the
        # host.run itself).
        if prompt.stdout.endswith("\n"):
            prompt_stdout = prompt.stdout[:-1]
        else:
            prompt_stdout = prompt.stdout

        assert prompt_stdout == expected_prompt


@pytest.mark.parametrize("wrapper_script, expected_owner, expected_group", [
    ('/var/www/parameters-mandatory/virtualenv/bin/exec', 'admin-parameters-mandatory', 'web-parameters-mandatory'),
    ('/var/www/parameters-optional.local/virtualenv/bin/exec', 'admin-parameters-optional_local', 'web-parameters-optional_local'),
    ('/var/www/parameters-paste-req/virtualenv/bin/exec', 'admin-parameters-paste-req', 'web-parameters-paste-req'),
])
def test_python_virtualenv_wrapper_script(host, wrapper_script, expected_owner, expected_group):
    """
    Tests if Python virtualenv wrapper script is functioning correctly.
    """

    with host.sudo():

        wrapper = host.file(wrapper_script)
        assert wrapper.is_file
        assert wrapper.user == expected_owner
        assert wrapper.group == expected_group
        assert wrapper.mode == 0o750

        command = host.run("sudo -u %s %s python -c 'import gunicorn'", expected_owner, wrapper_script)
        assert command.rc == 0


@pytest.mark.parametrize("admin_user, pip_path, expected_packages",  [
    ('admin-parameters-mandatory', '/var/www/parameters-mandatory/virtualenv/bin/pip', [
        "gunicorn==21.2.0",
        "packaging==23.2",
    ]),
    ('admin-parameters-optional_local', '/var/www/parameters-optional.local/virtualenv/bin/pip', [
        "appdirs==1.4.4",
        "dnspython==2.6.1",
        "gunicorn==21.1.0",
        "jedi==0.19.1",
        "packaging==23.2",
        "parso==0.8.3",
        "prompt-toolkit==3.0.43",
        "ptpython==3.0.26",
        "pygments==2.17.2",
        "wcwidth==0.2.13",
    ]),
    ('admin-parameters-paste-req', '/var/www/parameters-paste-req/virtualenv/bin/pip', [
        "Flask==3.0.2",
        "Jinja2==3.1.3",
        "MarkupSafe==2.1.5",
        "Paste==3.7.1",
        "PasteDeploy==3.1.0",
        "Werkzeug==3.0.1",
        "blinker==1.7.0",
        "click==8.1.7",
        "gunicorn==21.1.0",
        "importlib-metadata==7.0.1",
        "itsdangerous==2.1.2",
        "packaging==23.2",
        "six==1.16.0",
        "zipp==3.17.0",
    ]),
])
def test_virtualenv_packages(host, admin_user, pip_path, expected_packages):
    """
    Tests if correct packages are installed in virtualenv.
    """

    packages = host.run("sudo -u %s %s freeze", admin_user, pip_path)

    # Normalise package names and order.
    expected_packages = sorted([p.lower() for p in expected_packages])
    actual_packages = sorted(packages.stdout.lower().strip().split("\n"))

    assert actual_packages == expected_packages


@pytest.mark.parametrize("config_file, expected_website_name, expected_socket_file_path", [
    ('/etc/systemd/system/parameters-mandatory.socket', 'parameters-mandatory', '/run/wsgi/parameters-mandatory.sock'),
    ('/etc/systemd/system/parameters-optional.local.socket', 'parameters-optional.local', '/run/wsgi/parameters-optional.local.sock'),
    ('/etc/systemd/system/parameters-paste-req.socket', 'parameters-paste-req', '/run/wsgi/parameters-paste-req.sock'),
])
def test_systemd_socket_configuration_file(host, config_file, expected_website_name, expected_socket_file_path):
    """
    Tests if systemd socket configuration file has been set-up correctly.
    """

    config = host.file(config_file)
    assert config.is_file
    assert config.user == 'root'
    assert config.group == 'root'
    assert config.mode == 0o644
    assert "Socket for website %s" % expected_website_name in config.content_string
    assert "ListenStream=%s" % expected_socket_file_path in config.content_string


@pytest.mark.parametrize("socket_file_path", [
    '/run/wsgi/parameters-mandatory.sock',
    '/run/wsgi/parameters-optional.local.sock',
    '/run/wsgi/parameters-paste-req.sock',
])
def test_systemd_socket(host, socket_file_path):
    """
    Tests if systemd socket has correct permissions and is available.
    """

    with host.sudo():

        socket_file = host.file(socket_file_path)
        assert socket_file.is_socket
        assert socket_file.user == 'www-data'
        assert socket_file.group == 'www-data'
        assert socket_file.mode == 0o660

        socket = host.socket("unix://%s" % socket_file_path)
        assert socket.is_listening


@pytest.mark.parametrize("service_file, expected_fqdn", [
    ('/etc/systemd/system/parameters-mandatory.service', 'parameters-mandatory'),
    ('/etc/systemd/system/parameters-optional.local.service', 'parameters-optional.local'),
    ('/etc/systemd/system/parameters-paste-req.service', 'parameters-paste-req'),
])
def test_systemd_service_configuration_file(host, service_file, expected_fqdn):
    """
    Tests if systemd service configuration file has been set-up correctly.
    """

    config = host.file(service_file)
    assert config.is_file
    assert config.user == 'root'
    assert config.group == 'root'
    assert config.mode == 0o644
    assert expected_fqdn in config.content_string


@pytest.mark.parametrize("service_name", [
    'parameters-mandatory',
    'parameters-optional.local',
    'parameters-paste-req',
])
def test_systemd_service(host, service_name):
    """
    Tests if the systemd service is enabled at boot and running.
    """

    service = host.service(service_name)

    assert service.is_enabled
    assert service.is_running


@pytest.mark.parametrize("directory_path, expected_owner, expected_group", [
    ('/var/www/parameters-mandatory/htdocs', 'admin-parameters-mandatory', 'web-parameters-mandatory'),
    ('/var/www/parameters-optional.local/htdocs', 'admin-parameters-optional_local', 'web-parameters-optional_local'),
    ('/var/www/parameters-paste-req/htdocs', 'admin-parameters-paste-req', 'web-parameters-paste-req'),
])
def test_static_file_directory(host, directory_path, expected_owner, expected_group):
    """
    Tests if directory for serving static files has been created correctly.
    """

    with host.sudo():

        directory = host.file(directory_path)
        assert directory.is_directory
        assert directory.user == expected_owner
        assert directory.group == expected_group
        assert directory.mode == 0o2750


@pytest.mark.parametrize("private_key_path, certificate_path, expected_private_key, expected_certificate", [
    ('/etc/ssl/private/parameters-mandatory_https.key', '/etc/ssl/certs/parameters-mandatory_https.pem',
     'tests/data/x509/server/parameters-mandatory_https.key.pem', 'tests/data/x509/server/parameters-mandatory_https.cert.pem'),
    ('/etc/ssl/private/parameters-optional.local_https.key', '/etc/ssl/certs/parameters-optional.local_https.pem',
     'tests/data/x509/server/parameters-optional.local_https.key.pem', 'tests/data/x509/server/parameters-optional.local_https.cert.pem'),
    ('/etc/ssl/private/parameters-paste-req_https.key', '/etc/ssl/certs/parameters-paste-req_https.pem',
     'tests/data/x509/server/parameters-paste-req_https.key.pem', 'tests/data/x509/server/parameters-paste-req_https.cert.pem'),
])
def test_nginx_tls_files(host, private_key_path, certificate_path, expected_private_key, expected_certificate):
    """
    Tests if TLS private key and certificate have been deployed correctly.
    """

    with host.sudo():

        tls_file = host.file(private_key_path)
        assert tls_file.is_file
        assert tls_file.user == 'root'
        assert tls_file.group == 'root'
        assert tls_file.mode == 0o640
        assert tls_file.content_string == open(expected_private_key, "r").read().rstrip()

        tls_file = host.file(certificate_path)
        assert tls_file.is_file
        assert tls_file.user == 'root'
        assert tls_file.group == 'root'
        assert tls_file.mode == 0o644
        assert tls_file.content_string == open(expected_certificate, "r").read().rstrip()


@pytest.mark.parametrize("config_file_path, expected_content", [
    ('/etc/check_certificate/parameters-mandatory_https.conf', '/etc/ssl/certs/parameters-mandatory_https.pem'),
    ('/etc/check_certificate/parameters-optional.local_https.conf', '/etc/ssl/certs/parameters-optional.local_https.pem'),
    ('/etc/check_certificate/parameters-paste-req_https.conf', '/etc/ssl/certs/parameters-paste-req_https.pem'),
])
def test_certificate_validity_check_configuration(host, config_file_path, expected_content):
    """
    Tests if certificate validity check configuration file has been deployed
    correctly.
    """

    config = host.file(config_file_path)
    assert config.is_file
    assert config.user == 'root'
    assert config.group == 'root'
    assert config.mode == 0o644
    assert config.content_string == expected_content


@pytest.mark.parametrize("config_file", [
    '/etc/nginx/sites-available/parameters-mandatory',
    '/etc/nginx/sites-available/parameters-optional.local',
    '/etc/nginx/sites-available/parameters-paste-req',
])
def test_vhost_file(host, config_file):
    """
    Tests permissions of vhost configuration file.
    """

    config = host.file(config_file)

    assert config.is_file
    assert config.user == 'root'
    assert config.group == 'root'
    assert config.mode == 0o640


@pytest.mark.parametrize("config_file, expected_content", [
    ('/etc/nginx/sites-enabled/parameters-mandatory', '/etc/nginx/sites-available/parameters-mandatory'),
    ('/etc/nginx/sites-enabled/parameters-optional.local', '/etc/nginx/sites-available/parameters-optional.local'),
    ('/etc/nginx/sites-enabled/parameters-paste-req', '/etc/nginx/sites-available/parameters-paste-req'),
])
def test_website_enabled(host, config_file, expected_content):
    """
    Tests if website has been enabled.
    """

    config = host.file(config_file)

    assert config.is_symlink
    assert config.linked_to == expected_content


@pytest.mark.parametrize("python_path", [
    ("/var/www/parameters-mandatory/virtualenv/bin/python"),
    ("/var/www/parameters-optional.local/virtualenv/bin/python"),
    ("/var/www/parameters-paste-req/virtualenv/bin/python"),
])
def test_python_version_in_virtualenv(host, python_info, python_path,):
    """
    Tests if correct Python version is used inside of virtual
    environment.
    """

    expected_version = "Python %s" % python_info.python_version

    with host.sudo():

        python_version = host.run(python_path + " --version")

        assert python_version.rc == 0

        assert expected_version in python_version.stdout


@pytest.mark.parametrize("fqdn, pip_check_requirements_upgrades_directory, expected_requirements_in, expected_requirements", [
    ("parameters-mandatory", "/etc/pip_check_requirements_upgrades",  ["gunicorn"], ["gunicorn==21.2.0", "packaging==23.2"]),
    ("parameters-optional.local", "/etc/pip_check_requirements_upgrades", ["gunicorn"], ["gunicorn==21.1.0", "packaging==23.2"]),
    ("parameters-paste-req", "/etc/pip_check_requirements_upgrades", ["gunicorn"], ["gunicorn==21.1.0", "packaging==23.2"]),
])
def test_wsgi_requirements(host, fqdn, pip_check_requirements_upgrades_directory, expected_requirements_in, expected_requirements):
    """
    Tests if Python requirements files are set-up correctly (for both
    installation and upgrade checks).
    """

    with host.sudo():
        directory = host.file('%s/%s' % (pip_check_requirements_upgrades_directory, fqdn))
        assert directory.is_directory
        assert directory.user == 'root'
        assert directory.group == 'pipreqcheck'
        assert directory.mode == 0o750

        config = host.file('%s/%s/wsgi_requirements.in' % (pip_check_requirements_upgrades_directory, fqdn))
        assert config.is_file
        assert config.user == 'root'
        assert config.group == 'pipreqcheck'
        assert config.mode == 0o640

        check_requirements_in = config.content_string.strip().split("\n")
        assert check_requirements_in == expected_requirements_in

        config = host.file('%s/%s/wsgi_requirements.txt' % (pip_check_requirements_upgrades_directory, fqdn))
        assert config.is_file
        assert config.user == 'root'
        assert config.group == 'pipreqcheck'
        assert config.mode == 0o640

        check_requirements = config.content_string.strip().split("\n")
        assert check_requirements == expected_requirements

        requirements = host.file('/var/www/%s/.wsgi_requirements.txt' % fqdn)
        assert requirements.is_file
        assert requirements.user == 'admin-%s' % fqdn.replace(".", "_")
        assert requirements.group == 'web-%s' % fqdn.replace(".", "_")
        assert requirements.mode == 0o640

        install_requirements = requirements.content_string.strip().split("\n")
        assert install_requirements == expected_requirements


@pytest.mark.parametrize('fqdn', [
    'parameters-mandatory',
    'parameters-optional.local',
    'parameters-paste-req'
])
def test_https_enforcement(host, fqdn):
    """
    Tests if HTTPS is being enforced.
    """

    https_enforcement = host.run('curl -I http://%s/', fqdn)

    assert https_enforcement.rc == 0
    assert 'HTTP/1.1 301 Moved Permanently' in https_enforcement.stdout
    assert 'Location: https://%s/' % fqdn in https_enforcement.stdout

    https_enforcement = host.run('curl -I https://%s/', fqdn)

    assert https_enforcement.rc == 0
    assert 'Strict-Transport-Security: max-age=31536000; includeSubDomains' in https_enforcement.stdout
