# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: GPL-2.0-or-later
"""Test the `debsigs` command-line tool using `debsig-verify` and OpenPGP tools."""

from __future__ import annotations

import contextlib
import dataclasses
import os
import pathlib
import subprocess  # noqa: S404
import sys
import tempfile
import typing

import click
import tomli_w
import typedload

from . import defs
from .pgpcli import gnupg as c_gnupg
from .pgpcli import sop as c_sop


if typing.TYPE_CHECKING:
    from collections.abc import Iterator
    from typing import Final


OPGP_UID: Final = "Kieron Latimer <kieron@example.net>"
"""The user ID to generate a test key with."""

DEB_NAME_1: Final = "ethstats"
"""The name of the first Debian package file to download and sign."""

DEB_NAME_2: Final = "keychain"
"""The name of the second Debian package file to download and sign."""


@contextlib.contextmanager
def _prepare_temp_dir(cfg: defs.Config) -> Iterator[defs.Config]:
    """Create a temporary directory, clean it up afterwards."""
    with tempfile.TemporaryDirectory(prefix="debsigs-functional.") as tempd_obj:
        tempd: Final = pathlib.Path(tempd_obj)
        home: Final = tempd / "home"
        work: Final = tempd / "work"

        home.mkdir(mode=0o755)
        work.mkdir(mode=0o755)

        env: Final = dict(cfg.env)
        env["LC_ALL"] = "C.UTF-8"
        env["LANGUAGE"] = ""
        env["HOME"] = str(home)
        for name in (name for name in env if name.startswith(("GNUPG", "GPG"))):
            del env[name]

        yield dataclasses.replace(
            cfg,
            env=env,
            path=defs.ConfigPaths(base=tempd, home=home, work=work),
        )


def _test_sign_file(
    cfg: defs.Config,
    keys: defs.KeyFiles,
    cli_gnupg: c_gnupg.GnuPGCli,
    cli_sop: c_sop.SOPCli,
    pub_key: defs.PublicKey,
) -> None:
    """Create a temporary file, sign it, verify the signature."""
    contents: Final = "This is a test.\n"
    testf: Final = cfg.path.work / "sign-test.txt"

    if testf.exists() or testf.is_symlink():
        raise RuntimeError(repr(testf))

    try:
        testf.write_text(contents, encoding="UTF-8")
        signature: Final = cli_gnupg.sign_detached(keys, testf)
        if signature.parent != testf.parent:
            raise RuntimeError(repr((testf, signature)))

        cli_sop.verify_detached(keys, pub_key, testf, signature)
        signature.unlink()

        det_signature: Final = cli_sop.sign_detached(keys, testf)
        if det_signature != signature:
            raise RuntimeError(repr((signature, det_signature)))
        cli_gnupg.verify_detached(keys, pub_key, testf, signature)
    finally:
        if signature.exists():
            signature.unlink()
        if testf.exists():
            testf.unlink()


def _get_deb_changes(cfg: defs.Config) -> defs.ChangesFile:
    """Fetch a Debian package from the configured Apt repositories."""
    ppath: Final = cfg.path.home / "deb-orig"
    if not ppath.is_dir():
        ppath.mkdir(mode=0o755)

    current_files: Final = {path for path in ppath.iterdir() if path.is_file()}
    subprocess.check_call(  # noqa: S603
        ["apt-get", "download", "--", DEB_NAME_1, DEB_NAME_2],  # noqa: S607
        cwd=ppath,
        env=cfg.env,
    )
    all_files: Final = {path for path in ppath.iterdir() if path.is_file()}
    new_files: Final = list(all_files - current_files)
    gone_files: Final = list(current_files - all_files)
    match (new_files, gone_files):
        case ([_first, _second], []):
            pass

        case _:
            raise RuntimeError(repr((current_files, all_files)))

    others: Final = [ppath / "debsigs-test-more.txt"]
    others[0].write_text("This file should not be changed.\n", encoding="UTF-8")

    changes: Final = defs.ChangesFile.craft(cfg, ppath / "debsigs-test.changes", new_files, others)
    changes.verify(cfg)
    return changes


def _store_test_config(
    cfg: defs.Config,
    keys: defs.KeyFiles,
    pub_key: defs.PublicKey,
    changes_orig: defs.ChangesFile,
) -> dict[str, str]:
    """Dump the configuration into TOML files, store the paths into the environment."""
    cfgpath: Final = cfg.path.work / "test-config.toml"
    with cfgpath.open(mode="wb") as cfgf:
        tomli_w.dump(
            typedload.dump(
                {
                    "format": {"version": {"major": 1, "minor": 0}},
                    "cfg": cfg,
                    "keys": keys,
                    "pub_key": pub_key,
                    "changes_orig": changes_orig,
                },
            ),
            cfgf,
        )

    env_test: Final = dict(cfg.env)
    env_test["DEBSIGS_TEST_CONFIG_FILE"] = str(cfgpath)
    return env_test


def run_test(cfg: defs.Config) -> None:
    """Prepare the working directory, run the tests."""
    cli_gnupg: Final = c_gnupg.GnuPGCli(cfg)
    cli_sop: Final = c_sop.SOPCli(cfg)

    keys: Final = cli_sop.generate_keys(OPGP_UID)
    pub_key: Final = cli_gnupg.import_secret_key(keys, OPGP_UID)
    _test_sign_file(cfg, keys, cli_gnupg, cli_sop, pub_key)

    changes_orig: Final = _get_deb_changes(cfg)

    env_test: Final = _store_test_config(cfg, keys, pub_key, changes_orig)
    subprocess.check_call(  # noqa: S603
        [
            sys.executable,
            "-m",
            "pytest",
            "--pyargs",
            "-vv",
            "testsigs.unit",
        ],
        cwd=cfg.path.work,
        env=env_test,
    )


@click.command(name="functional")
@click.option(
    "--debsigs-program",
    type=click.Path(exists=True, executable=True, path_type=pathlib.Path, resolve_path=True),
    default="/usr/bin/debsigs",
    help="the debsigs program to test",
)
@click.option(
    "--debsig-verify-program",
    type=click.Path(exists=True, executable=True, path_type=pathlib.Path, resolve_path=True),
    default="/usr/bin/debsig-verify",
    help="the debsig-verify program to check the signed package files with",
)
@click.option(
    "--gnupg-program",
    type=click.Path(exists=True, executable=True, path_type=pathlib.Path, resolve_path=True),
    default="/usr/bin/gpg",
    help="the GnuPG program to test and sign with",
)
@click.option(
    "--sop-program",
    type=click.Path(exists=True, executable=True, path_type=pathlib.Path, resolve_path=True),
    default="/usr/bin/sqop",
    help="the stateless OpenPGP program to generate keys with",
)
def main(
    *,
    debsigs_program: pathlib.Path,
    debsig_verify_program: pathlib.Path,
    gnupg_program: pathlib.Path,
    sop_program: pathlib.Path,
) -> None:
    """Parse command-line options, prepare the working directory, run the tests."""
    base_cfg: Final = defs.Config(
        env=dict(os.environ),
        path=defs.ConfigPaths(base=pathlib.Path(), home=pathlib.Path(), work=pathlib.Path()),
        prog=defs.ConfigPrograms(
            debsigs=debsigs_program,
            debsig_verify=debsig_verify_program,
            gnupg=gnupg_program,
            sop=sop_program,
        ),
    )
    with _prepare_temp_dir(base_cfg) as cfg:
        run_test(cfg)


if __name__ == "__main__":
    main()
