# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: BSD-2-Clause
"""Test `stdsyslog` using `rsyslog'."""

from __future__ import annotations

import os
import pathlib
import platform
import random
import shlex
import subprocess  # noqa: S404
import tempfile
import time
import typing

import click

from . import isol_docker

if typing.TYPE_CHECKING:
    from typing import Final


TEST_USERNAME: Final = "stdtestlog"
"""The username of the test account to create within the container."""

TEST_GROUPNAME: Final = TEST_USERNAME
"""The name of the primary group of the test account to create within the container."""

PATH_CONT_BASE: Final = pathlib.Path("/opt/stdsyslog")
PATH_CONT_TEMP: Final = pathlib.Path("/opt/stdtemplog")

FILE_SUDOERS: Final = "stdtestlog"

LINE_INFO_1: Final = "This is a test. This is only a test."
LINE_INFO_2: Final = "This is something you really need to see"

LINE_ERR_1: Final = "No one expects error messages..."
LINE_ERR_2: Final = "...yet sometimes we do get those"
LINE_ERR_3: Final = "What, even more?"


@click.command(name="run")
@click.option(
    "--image",
    "-i",
    type=str,
    default="debian:unstable",
    help="the image to launch a container from",
)
@click.option("--prepared", is_flag=True, help="the necessary packages are already installed")
@click.option(
    "--program",
    "-p",
    type=click.Path(
        exists=True,
        dir_okay=False,
        file_okay=True,
        executable=True,
        resolve_path=True,
        path_type=pathlib.Path,
    ),
    default="/usr/bin/stdsyslog",
    help="the path to the stdsyslog program to test",
)
def cmd_container_run(*, image: str, prepared: bool, program: pathlib.Path) -> None:
    """Launch a container, prepare the environment, run the `stdsyslog` test within."""
    topdir: Final = pathlib.Path(__file__).absolute().parent.parent.parent.parent

    with tempfile.TemporaryDirectory(prefix="stdtestlog.container.") as tempd_obj:
        tempd: Final = pathlib.Path(tempd_obj)

        sudoers: Final = tempd / FILE_SUDOERS
        sudoers.write_text(f"{TEST_USERNAME} ALL = (ALL) NOPASSWD: ALL\n", encoding="UTF-8")

        with isol_docker.Container.start(
            image=image,
            vols_ro={
                topdir: PATH_CONT_BASE,
                program: "/usr/bin/stdsyslog",
                sudoers: PATH_CONT_TEMP / FILE_SUDOERS,
            },
        ) as cont:
            print("Running `stdsyslog --help` within the container")
            cont.check_call(["/usr/bin/stdsyslog", "--help"])

            if not prepared:
                print("Updating the Apt cache within the container")
                cont.check_call(["apt-get", "update"])

                print("Installing the necessary packages")
                cont.check_call(
                    [
                        "env",
                        "DEBIAN_FRONTEND=noninteractive",
                        "apt-get",
                        "install",
                        "-y",
                        "--",
                        "python3",
                        "python3-click",
                        "rsyslog",
                        "sudo",
                    ],
                )

            uid: Final = os.getuid()
            gid: Final = os.getgid()
            print(f"Creating the {TEST_USERNAME} test user account ({uid}:{gid})")
            cont.check_call(["groupadd", "-g", str(gid), "--", TEST_GROUPNAME])
            cont.check_call(
                [
                    "useradd",
                    "-u",
                    str(uid),
                    "-g",
                    str(gid),
                    "-d",
                    f"/home/{TEST_USERNAME}",
                    "-M",
                    "-s",
                    "/bin/dash",
                    "--",
                    TEST_USERNAME,
                ],
            )
            cont.check_call(["id", "--", TEST_USERNAME])

            print("Setting up the `sudo` configuration")
            cont.check_call(
                [
                    "install",
                    "-o",
                    "root",
                    "-g",
                    "root",
                    "-m",
                    "644",
                    "--",
                    PATH_CONT_TEMP / FILE_SUDOERS,
                    f"/etc/sudoers.d/{FILE_SUDOERS}",
                ],
            )

            print("Running the test within the container")
            cont.check_call(
                [
                    "env",
                    "PYTHONPATH=/opt/stdsyslog/python/src",
                    "python3",
                    "-B",
                    "-u",
                    "-m",
                    "stdtestlog",
                    "run",
                    "-p",
                    "/usr/bin/stdsyslog",
                ],
                run_as=f"{uid}:{gid}",
            )

            print("Seems the test passed within the container")


@click.group(name="container")
def cmd_container() -> None:
    """Perform container-related actions: launch, install packages, configure `sudo`, etc."""


cmd_container.add_command(cmd_container_run)


def assert_added_lines(logpath: pathlib.Path, current: list[str], added: list[str]) -> None:
    """Make sure the expected lines were appended to the file."""
    expected: Final = current + added
    actual: Final = logpath.read_text(encoding="UTF-8").splitlines()
    try:
        for line_expected, line_actual in zip(expected, actual, strict=True):
            if not line_actual.endswith(line_expected):
                raise RuntimeError(repr((expected, actual)))
    except ValueError as err:
        raise RuntimeError(repr((expected, actual))) from err

    current.extend(added)


def run_stdsyslog_and_check(
    program: pathlib.Path,
    facility: str,
    logpath: pathlib.Path,
    test_lines: list[str],
) -> None:
    """Run the specified program, check the logfile."""

    def run_and_check(
        cmd: list[str | pathlib.Path],
        *,
        added_lines: list[str],
        opts: list[str | pathlib.Path] | None = None,
    ) -> None:
        """Run the program, make sure it added the new lines."""
        if opts is None:
            opts = []
        subprocess.check_call([program, "-f", facility, *opts, "--", *cmd])  # noqa: S603
        print("Waiting for a second")
        time.sleep(1)

        print(f"Making sure {logpath} only contains a single line")
        assert_added_lines(logpath, test_lines, added_lines)

    node: Final = platform.node()
    print(f"Running `hostname`, expecting '{node}'")
    run_and_check(
        ["hostname"],
        added_lines=[node],
    )

    print("Making sure an error message is also logged")
    run_and_check(
        [
            "sh",
            "-c",
            f"echo {shlex.quote(LINE_ERR_1)} 1>&2; echo {shlex.quote(LINE_ERR_2)} 1>&2",
        ],
        added_lines=[LINE_ERR_1, LINE_ERR_2],
    )

    print("Making sure no output is logged in a non-default configuration")
    run_and_check(
        [
            "sh",
            "-c",
            f"echo {shlex.quote(LINE_INFO_2)}",
        ],
        added_lines=[],
        opts=["-d", "2:warning"],
    )

    print("Making sure an error message is also logged in a non-default configuration")
    run_and_check(
        [
            "sh",
            "-c",
            f"echo {shlex.quote(LINE_ERR_3)} 1>&2",
        ],
        added_lines=[LINE_ERR_3],
        opts=["-d", "2:warning"],
    )

    print(f"Seems {program} behaves just fine")


@click.command(name="run")
@click.option(
    "--program",
    "-p",
    type=click.Path(
        exists=True,
        dir_okay=False,
        file_okay=True,
        executable=True,
        resolve_path=True,
        path_type=pathlib.Path,
    ),
    default="/usr/bin/stdsyslog",
    help="the path to the stdsyslog program to test",
)
def cmd_run(*, program: pathlib.Path) -> None:
    """Configure `rsyslog`, run the test."""
    with tempfile.TemporaryDirectory(prefix="stdtestlog.") as tempd_obj:
        tempd: Final = pathlib.Path(tempd_obj)

        facility: Final = f"local{random.randint(0, 9)}"  # noqa: S311  # no strong crypto here
        logpath: Final = tempd / "stdtestlog.log"
        rsyspath: Final = tempd / "rsyslog-stdtestlog.conf"
        rsyspath.write_text(
            f"""
module(load="imuxsock")

{facility}.* {logpath}
""",
            encoding="UTF-8",
        )

        print(f"Starting `rsyslogd -n -iNONE -f {rsyspath}`")
        rsysproc: Final = subprocess.Popen(  # noqa: S603
            ["sudo", "rsyslogd", "-n", "-iNONE", "-f", rsyspath],  # noqa: S607
        )
        try:
            print(f"Got rsyslog pid {rsysproc.pid}")

            print("Waiting for a second")
            time.sleep(1)
            print("Making sure rsyslog is still alive")
            if rsysproc.poll() is not None:
                raise RuntimeError(repr(rsysproc.wait()))

            print(f"Making sure {logpath} does not exist yet")
            if logpath.is_symlink() or logpath.exists():
                raise RuntimeError(repr(logpath))

            print("Logging a single line")
            subprocess.check_call(  # noqa: S603
                ["logger", "-p", f"{facility}.info", "--", LINE_INFO_1],  # noqa: S607
            )
            print("Waiting for a second")
            time.sleep(1)

            print(f"Making sure {logpath} only contains a single line")
            test_lines: Final[list[str]] = []
            assert_added_lines(logpath, test_lines, [LINE_INFO_1])

            run_stdsyslog_and_check(program, facility, logpath, test_lines)
        finally:
            if rsysproc.poll() is None:
                print("Stopping the rsyslogd process")
                rsysproc.terminate()

            print("Waiting for the rsyslogd process to go away")
            rsysres: Final = rsysproc.wait()
            if rsysres != 0:
                raise RuntimeError(repr(rsysres))


@click.group(name="stdtestlog")
def main() -> None:
    """Configure `rsyslog`, start it, run `stdsyslog`, look for its output."""


main.add_command(cmd_container)
main.add_command(cmd_run)


if __name__ == "__main__":
    main()
