import os
import subprocess
from unittest.mock import patch

import pytest

from osbuild.testutil import make_fake_tree
from osbuild.testutil.imports import import_module_from_path

osbuild_image_info = import_module_from_path("osbuild_image_info", "tools/osbuild-image-info")


@pytest.mark.parametrize("fake_tree,entries", (
    # no entries
    ({}, []),
    # one entry
    (
        {
            "/boot/loader/entries/0649288e52434223afde4c36460a375e-6.11.9-100.fc39.x86_64.conf": """title Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)
version 6.11.9-100.fc39.x86_64
linux /boot/vmlinuz-6.11.9-100.fc39.x86_64
initrd /boot/initramfs-6.11.9-100.fc39.x86_64.img
options root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8
grub_users $grub_users
grub_arg --unrestricted
grub_class fedora""",
        },
        [
            {
                "title": "Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)",
                "version": "6.11.9-100.fc39.x86_64",
                "linux": "/boot/vmlinuz-6.11.9-100.fc39.x86_64",
                "initrd": "/boot/initramfs-6.11.9-100.fc39.x86_64.img",
                "options": "root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8",
                "grub_users": "$grub_users",
                "grub_arg": "--unrestricted",
                "grub_class": "fedora",
            },
        ]
    ),
    # two entries
    (
        {
            "/boot/loader/entries/0649288e52434223afde4c36460a375e-6.11.9-100.fc39.x86_64.conf": """title Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)
version 6.11.9-100.fc39.x86_64
linux /boot/vmlinuz-6.11.9-100.fc39.x86_64
initrd /boot/initramfs-6.11.9-100.fc39.x86_64.img
options root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8
grub_users $grub_users
grub_arg --unrestricted
grub_class fedora""",
            "/boot/loader/entries/0649288e52434223afde4c36460a375e-6.11.9-101.fc39.x86_64.conf": """title Fedora Linux (6.11.9-101.fc39.x86_64) 39 (Thirty Nine)
version 6.11.9-101.fc39.x86_64
linux /boot/vmlinuz-6.11.9-101.fc39.x86_64
initrd /boot/initramfs-6.11.9-101.fc39.x86_64.img
options root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8
grub_users $grub_users
grub_arg --unrestricted
grub_class fedora""",
        },
        [
            {
                "title": "Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)",
                "version": "6.11.9-100.fc39.x86_64",
                "linux": "/boot/vmlinuz-6.11.9-100.fc39.x86_64",
                "initrd": "/boot/initramfs-6.11.9-100.fc39.x86_64.img",
                "options": "root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8",
                "grub_users": "$grub_users",
                "grub_arg": "--unrestricted",
                "grub_class": "fedora",
            },
            {
                "title": "Fedora Linux (6.11.9-101.fc39.x86_64) 39 (Thirty Nine)",
                "version": "6.11.9-101.fc39.x86_64",
                "linux": "/boot/vmlinuz-6.11.9-101.fc39.x86_64",
                "initrd": "/boot/initramfs-6.11.9-101.fc39.x86_64.img",
                "options": "root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8",
                "grub_users": "$grub_users",
                "grub_arg": "--unrestricted",
                "grub_class": "fedora",
            },
        ]
    ),
    # one entry with extra newlines
    (
        {
            "/boot/loader/entries/0649288e52434223afde4c36460a375e-6.11.9-100.fc39.x86_64.conf": """title Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)
version 6.11.9-100.fc39.x86_64
linux /boot/vmlinuz-6.11.9-100.fc39.x86_64
initrd /boot/initramfs-6.11.9-100.fc39.x86_64.img
options root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8
grub_users $grub_users
grub_arg --unrestricted
grub_class fedora

""",
        },
        [
            {
                "title": "Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)",
                "version": "6.11.9-100.fc39.x86_64",
                "linux": "/boot/vmlinuz-6.11.9-100.fc39.x86_64",
                "initrd": "/boot/initramfs-6.11.9-100.fc39.x86_64.img",
                "options": "root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8",
                "grub_users": "$grub_users",
                "grub_arg": "--unrestricted",
                "grub_class": "fedora",
            },
        ]
    ),
    # one entry with comments
    (
        {
            "/boot/loader/entries/0649288e52434223afde4c36460a375e-6.11.9-100.fc39.x86_64.conf": """title Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)
# this is a very useful comment
version 6.11.9-100.fc39.x86_64
linux /boot/vmlinuz-6.11.9-100.fc39.x86_64
initrd /boot/initramfs-6.11.9-100.fc39.x86_64.img
options root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8
# this is another very useful comment
grub_users $grub_users
grub_arg --unrestricted
grub_class fedora""",
        },
        [
            {
                "title": "Fedora Linux (6.11.9-100.fc39.x86_64) 39 (Thirty Nine)",
                "version": "6.11.9-100.fc39.x86_64",
                "linux": "/boot/vmlinuz-6.11.9-100.fc39.x86_64",
                "initrd": "/boot/initramfs-6.11.9-100.fc39.x86_64.img",
                "options": "root=UUID=a7e970a5-14fb-4a8a-ab09-603d1ac3fee9 ro crashkernel=auto net.ifnames=0 rhgb console=tty0 console=ttyS0,115200n8",
                "grub_users": "$grub_users",
                "grub_arg": "--unrestricted",
                "grub_class": "fedora",
            },
        ]
    ),
))
def test_read_boot_entries(tmp_path, fake_tree, entries):
    make_fake_tree(tmp_path, fake_tree)
    assert osbuild_image_info.read_boot_entries(tmp_path / "boot") == entries


def test_read_default_target_ok(tmp_path):
    """
    Test the happy case when determinig the systemd default target
    """
    make_fake_tree(tmp_path, {
        "/usr/lib/systemd/system/multi-user.target": """#  SPDX-License-Identifier: LGPL-2.1-or-later
#
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

[Unit]
Description=Multi-User System
Documentation=man:systemd.special(7)
Requires=basic.target
Conflicts=rescue.service rescue.target
After=basic.target rescue.service rescue.target
AllowIsolate=yes
"""
    })
    etc_systemd_system_dir = tmp_path / "etc/systemd/system"
    etc_systemd_system_dir.mkdir(parents=True)
    default_target_link = etc_systemd_system_dir / "default.target"
    default_target_link.symlink_to("/usr/lib/systemd/system/multi-user.target")

    assert osbuild_image_info.read_default_target(tmp_path) == "multi-user.target"


def test_read_default_target_none(tmp_path):
    """
    Test the case when when there is no default target set on the system
    """
    assert osbuild_image_info.read_default_target(tmp_path) == ""


# root is needed, because the script will bind mount the dir as read-only
@pytest.mark.skipif(os.getuid() != 0, reason="root only")
def test_empty_report_fail(tmp_path):
    """
    Test that the main() exits with a non-zero exit code if the report is empty.
    """
    with pytest.raises(SystemExit) as e, patch("sys.argv", ["osbuild-image-info", str(tmp_path)]):
        osbuild_image_info.main()
    assert e.value.code == 1


def make_fake_iso(iso_tree, output_dir) -> str:
    iso_path = os.path.join(output_dir, "image.iso")
    subprocess.run(["mkisofs", "-o", iso_path, "-R", "-J", iso_tree], check=True)
    return iso_path


@pytest.mark.skipif(os.getuid() != 0, reason="root only")
def test_analyse_iso_fail_mount(tmp_path):
    # fake ISO that can't be mounted
    image_path = tmp_path / "image.iso"
    image_path.touch()

    with pytest.raises(
            subprocess.CalledProcessError,
            match=fr"^Command '\['mount', '-o', 'ro,loop', PosixPath\('{image_path}'\)"):
        osbuild_image_info.analyse_iso(image_path)


@pytest.mark.skipif(os.getuid() != 0, reason="root only")
def test_analyse_iso_fail_no_tarball(tmp_path):
    # ISO that can be mounted, but doesn't contain the liveimg.tar.gz
    iso_tree = tmp_path / "iso_tree"
    iso_tree.mkdir()
    # NB: The random file is added to the ISO, because in GH actions, the produced
    # ISO was not valid and was consistently failing to be mounted.
    random_file = iso_tree / "random_file"
    random_file.write_text("random content")

    image_path = make_fake_iso(iso_tree, tmp_path)

    with pytest.raises(
            subprocess.CalledProcessError,
            match=r"^Command '\['tar', '--selinux', '--xattrs', '--acls', '-x', '--auto-compress', '-f', '/tmp/\w+/liveimg.tar.gz"):
        osbuild_image_info.analyse_iso(image_path)


@pytest.mark.parametrize("subprocess_output,expected_report", [
    pytest.param(
        """Would relabel {tmp_path}/etc/shells from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
Would relabel {tmp_path}/etc/ld.so.cache from unconfined_u:object_r:ld_so_cache_t:s0 to system_u:object_r:ld_so_cache_t:s0
Would relabel {tmp_path}/etc/alternatives/roff.7.gz from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
Would relabel {tmp_path}/var/lib/selinux/targeted/active from unconfined_u:object_r:semanage_store_t:s0 to system_u:object_r:semanage_store_t:s0
Would relabel {tmp_path}/var/lib/alternatives/roff.7.gz from unconfined_u:object_r:rpm_var_lib_t:s0 to system_u:object_r:rpm_var_lib_t:s0
""",
        [
            {
                "filename": "/etc/alternatives/roff.7.gz",
                "actual": "unconfined_u:object_r:etc_t:s0",
                "expected": "system_u:object_r:etc_t:s0",
            },
            {
                "filename": "/etc/ld.so.cache",
                "actual": "unconfined_u:object_r:ld_so_cache_t:s0",
                "expected": "system_u:object_r:ld_so_cache_t:s0",
            },
            {
                "filename": "/etc/shells",
                "actual": "unconfined_u:object_r:etc_t:s0",
                "expected": "system_u:object_r:etc_t:s0",
            },
            {
                "filename": "/var/lib/alternatives/roff.7.gz",
                "actual": "unconfined_u:object_r:rpm_var_lib_t:s0",
                "expected": "system_u:object_r:rpm_var_lib_t:s0",
            },
            {
                "filename": "/var/lib/selinux/targeted/active",
                "actual": "unconfined_u:object_r:semanage_store_t:s0",
                "expected": "system_u:object_r:semanage_store_t:s0",
            },
        ],
        id="happy case",
    ),
    pytest.param(
        "",
        [],
        id="empty",
    ),
    pytest.param(
        """{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.bin:  Old compiled fcontext format, skipping
{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.homedirs.bin:  Old compiled fcontext format, skipping
""",
        [],
        id="only lines to skip",
    ),
    pytest.param(
        """{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.bin:  Old compiled fcontext format, skipping
{tmp_path}/etc/selinux/targeted/contexts/files/file_contexts.homedirs.bin:  Old compiled fcontext format, skipping

Would relabel {tmp_path}/etc/shells from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
Would relabel {tmp_path}/etc/ld.so.cache from unconfined_u:object_r:ld_so_cache_t:s0 to system_u:object_r:ld_so_cache_t:s0
Would relabel {tmp_path}/etc/alternatives/roff.7.gz from unconfined_u:object_r:etc_t:s0 to system_u:object_r:etc_t:s0
Would relabel {tmp_path}/var/lib/selinux/targeted/active from unconfined_u:object_r:semanage_store_t:s0 to system_u:object_r:semanage_store_t:s0
Would relabel {tmp_path}/var/lib/alternatives/roff.7.gz from unconfined_u:object_r:rpm_var_lib_t:s0 to system_u:object_r:rpm_var_lib_t:s0
""",
        [
            {
                "filename": "/etc/alternatives/roff.7.gz",
                "actual": "unconfined_u:object_r:etc_t:s0",
                "expected": "system_u:object_r:etc_t:s0",
            },
            {
                "filename": "/etc/ld.so.cache",
                "actual": "unconfined_u:object_r:ld_so_cache_t:s0",
                "expected": "system_u:object_r:ld_so_cache_t:s0",
            },
            {
                "filename": "/etc/shells",
                "actual": "unconfined_u:object_r:etc_t:s0",
                "expected": "system_u:object_r:etc_t:s0",
            },
            {
                "filename": "/var/lib/alternatives/roff.7.gz",
                "actual": "unconfined_u:object_r:rpm_var_lib_t:s0",
                "expected": "system_u:object_r:rpm_var_lib_t:s0",
            },
            {
                "filename": "/var/lib/selinux/targeted/active",
                "actual": "unconfined_u:object_r:semanage_store_t:s0",
                "expected": "system_u:object_r:semanage_store_t:s0",
            },
        ],
        id="valid lines mixed with lines to skip",
    )
])
def test_read_selinux_ctx_mismatch(tmp_path, subprocess_output, expected_report):
    """
    Test the read_selinux_ctx_mismatch function
    """
    policy_dir = tmp_path / "etc/selinux/targeted/policy"
    policy_dir.mkdir(parents=True)
    policy_file = policy_dir / "policy.33"
    policy_file.touch()

    with patch("subprocess.check_output") as subprocess_check_output:
        subprocess_check_output.return_value = subprocess_output.format(tmp_path=tmp_path)
        report = osbuild_image_info.read_selinux_ctx_mismatch(tmp_path.as_posix(), False)

        assert subprocess_check_output.call_count == 1
        assert subprocess_check_output.call_args[0][0] == [
            "setfiles", "-r", tmp_path.as_posix(),
            "-nvF",
            "-c", os.fspath(tmp_path / "etc/selinux/targeted/policy/policy.33"),
            os.fspath(tmp_path / "etc/selinux/targeted/contexts/files/file_contexts"),
            tmp_path.as_posix(),
        ]
        assert report == expected_report
