#
# Test for the build root
#

import os
import pathlib
import sys
from tempfile import TemporaryDirectory

import pytest

import osbuild.meta
from osbuild.buildroot import BuildRoot
from osbuild.monitor import LogMonitor, NullMonitor
from osbuild.util import linux

from ..test import TestBase


@pytest.fixture(name="tempdir")
def tempdir_fixture():
    with TemporaryDirectory(prefix="lvm2-") as tmp:
        yield tmp


@pytest.fixture(name="runner")
def runner_fixture():
    meta = osbuild.meta.Index(os.curdir)
    runner = meta.detect_host_runner()
    return runner.path


@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_basic(tempdir, runner):
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    monitor = NullMonitor(sys.stderr.fileno())
    with BuildRoot("/", runner, libdir, var) as root:

        r = root.run(["/usr/bin/true"], monitor)
        assert r.returncode == 0

        # Test we can use `.run` multiple times
        r = root.run(["/usr/bin/true"], monitor)
        assert r.returncode == 0, f"{r.stdout} {r.stderr}"

        r = root.run(["/usr/bin/false"], monitor)
        assert r.returncode != 0

        # Test that fs setup looks correct
        r = root.run(["test", "-d", "/var/tmp"], monitor)
        assert r.returncode == 0
        r = root.run(["stat", "--format=%a", "/var/tmp"], monitor)
        assert r.returncode == 0
        assert "1777" in r.stdout.strip().split("\n")


@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_runner_fail(tempdir):
    runner = "org.osbuild.nonexistantrunner"
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    logfile = os.path.join(tempdir, "log.txt")

    with BuildRoot("/", runner, libdir, var) as root, \
            open(logfile, "w", encoding="utf8") as log:

        monitor = LogMonitor(log.fileno())

        r = root.run(["/usr/bin/true"], monitor)

    assert r.returncode == 1
    with open(logfile, encoding="utf8") as f:
        log = f.read()
    assert log
    assert r.output
    assert log == r.output


@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_output(tempdir, runner):
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    data = "42. cats are superior to dogs"

    monitor = NullMonitor(sys.stderr.fileno())
    with BuildRoot("/", runner, libdir, var) as root:

        r = root.run(["/usr/bin/echo", data], monitor)
        assert r.returncode == 0

    assert data in r.output.strip()


@pytest.mark.skipif(not TestBase.have_test_data(), reason="no test-data access")
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_bind_mounts(tempdir, runner):
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    rw_data = pathlib.Path(tempdir, "data")
    rw_data.mkdir()

    scripts = os.path.join(TestBase.locate_test_data(), "scripts")

    monitor = NullMonitor(sys.stderr.fileno())
    with BuildRoot("/", runner, libdir, var) as root:

        ro_binds = [f"{scripts}:/scripts"]

        cmd = ["/scripts/mount_flags.py",
               "/scripts",
               "ro"]

        r = root.run(cmd, monitor, readonly_binds=ro_binds)
        assert r.returncode == 0

        cmd = ["/scripts/mount_flags.py",
               "/rw-data",
               "ro"]

        binds = [f"{rw_data}:/rw-data"]
        r = root.run(cmd, monitor, binds=binds, readonly_binds=ro_binds)
        assert r.returncode == 1


@pytest.mark.skipif(not TestBase.have_test_data(), reason="no test-data access")
@pytest.mark.skipif(not os.path.exists("/sys/fs/selinux"), reason="no SELinux")
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_selinuxfs_ro(tempdir, runner):
    # /sys/fs/selinux must never be writable in the container
    # because RPM and other tools must not assume the policy
    # of the host is the valid policy

    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    scripts = os.path.join(TestBase.locate_test_data(), "scripts")

    monitor = NullMonitor(sys.stderr.fileno())
    with BuildRoot("/", runner, libdir, var) as root:

        ro_binds = [f"{scripts}:/scripts"]

        cmd = ["/scripts/mount_flags.py",
               "/sys/fs/selinux",
               "ro"]

        r = root.run(cmd, monitor, readonly_binds=ro_binds)
        assert r.returncode == 0


@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_proc_overrides(tempdir, runner):
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    cmdline = "is-this-the-real-world"

    monitor = NullMonitor(sys.stderr.fileno())
    with BuildRoot("/", runner, libdir, var) as root:

        root.proc.cmdline = cmdline

        r = root.run(["cat", "/proc/cmdline"], monitor)
        assert r.returncode == 0
        assert cmdline in r.output.strip()


@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_timeout(tempdir, runner):
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    monitor = NullMonitor(sys.stderr.fileno())

    with BuildRoot("/", runner, libdir, var) as root:

        root.run(["/bin/sleep", "1"], monitor, timeout=2)

        with pytest.raises(TimeoutError):
            root.run(["/bin/sleep", "1"], monitor, timeout=0.1)

        with pytest.raises(TimeoutError):
            root.run(["/bin/sleep", "1"], monitor, timeout=0.1)


@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_env_isolation(tempdir, runner):
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    monitor = NullMonitor(sys.stderr.fileno())

    ipc = pathlib.Path(tempdir, "ipc")
    ipc.mkdir()

    # Set some env variable to make sure it is not leaked into
    # the container
    os.environ["OSBUILD_TEST_ENV_ISOLATION"] = "42"

    with BuildRoot("/", runner, libdir, var) as root:
        cmd = ["/bin/sh", "-c", "/usr/bin/env > /ipc/env.txt"]
        r = root.run(cmd, monitor, binds=[f"{ipc}:/ipc"])

    assert r.returncode == 0
    with open(os.path.join(ipc, "env.txt"), encoding="utf8") as f:
        data = f.read().strip()
    assert data
    have = dict(map(lambda x: x.split("=", 1), data.split("\n")))

    allowed = [
        "_",      # added by `env` itself
        "container",
        "LC_CTYPE",
        "PATH",
        "PWD",
        "PYTHONPATH",
        "PYTHONUNBUFFERED",
        "SHLVL",  # added by the shell wrapper
        "TERM",
    ]

    for k in have:
        assert k in allowed


@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
def test_caps(tempdir, runner):
    libdir = os.path.abspath(os.curdir)
    var = pathlib.Path(tempdir, "var")
    var.mkdir()

    ipc = pathlib.Path(tempdir, "ipc")
    ipc.mkdir()

    monitor = NullMonitor(sys.stderr.fileno())
    with BuildRoot("/", runner, libdir, var) as root:

        def run_and_get_caps():
            cmd = ["/bin/sh", "-c", "cat /proc/self/status > /ipc/status"]
            r = root.run(cmd, monitor, binds=[f"{ipc}:/ipc"])

            assert r.returncode == 0
            with open(os.path.join(ipc, "status"), encoding="utf8") as f:
                data = f.readlines()
            assert data

            print(data)
            perm = list(filter(lambda x: x.startswith("CapEff"), data))

            assert perm and len(perm) == 1
            perm = perm[0]

            perm = perm[7:].strip()  # strip "CapEff"
            print(perm)

            caps = linux.cap_mask_to_set(int(perm, base=16))
            return caps

        # check case of `BuildRoot.caps` is `None`, i.e. don't drop capabilities,
        # thus the effective capabilities should be the bounding set
        assert root.caps is None

        bound_set = linux.cap_bound_set()

        caps = run_and_get_caps()
        assert caps == bound_set

        # drop everything but `CAP_SYS_ADMIN`
        assert "CAP_SYS_ADMIN" in bound_set

        enable = set(["CAP_SYS_ADMIN"])
        disable = bound_set - enable

        root.caps = enable

        caps = run_and_get_caps()

        for e in enable:
            assert e in caps
        for d in disable:
            assert d not in caps
