"""
grml2usb basic pytests
~~~~~~~~~~~~~~~~~~~~~~

This script contains basic "unit" tests, implemented for and executed with pytest.

Requirements:
pytest (pip install pytest)

Runwith:
<project root>$ pytest [-m {basic}]

:copyright: (c) 2020 by Manuel Rom <roma@synpro.solutions>
:license: GPL v2 or any later version
:bugreports: http://grml.org/bugs/
"""

import importlib
import os
import subprocess
import uuid

import pytest

grml2usb = importlib.import_module("grml2usb", ".")


def test_which_finds_existing_program():
    """which should find programs existing in PATH"""
    result = grml2usb.which("ls")
    assert result is not None
    assert result.endswith("/ls")
    assert os.path.isfile(result)


def test_which_returns_none_for_nonexistent_program():
    """which should return None for non-existing programs"""
    assert grml2usb.which("nonexistent_program_xyz123") is None


def test_which_skips_non_executable_files(tmp_path, monkeypatch):
    """which skips files that are not executable"""
    non_exe = tmp_path / "program"
    non_exe.touch()
    non_exe.chmod(0o644)
    monkeypatch.setenv("PATH", str(tmp_path))
    assert grml2usb.which("program") is None


def test_write_uuid(tmp_path):
    target_file = tmp_path / "test_uuid.txt"
    returned_uid = grml2usb.write_uuid(target_file)
    assert str(uuid.UUID(returned_uid)) == returned_uid
    assert target_file.read_text() == returned_uid


def test_get_target_bootid_existing(tmp_path):
    conf_dir = tmp_path / "conf"
    conf_dir.mkdir()
    bootid_file = conf_dir / "bootid.txt"
    existing_uuid = "12345678-1234-5678-1234-567812345678"
    bootid_file.write_text(existing_uuid)

    result = grml2usb.get_target_bootid(tmp_path)
    assert result == existing_uuid


def test_get_target_bootid_new(tmp_path, monkeypatch):
    monkeypatch.setattr(grml2usb, "execute", lambda f, *args: f(*args))
    conf_dir = tmp_path / "conf"
    result = grml2usb.get_target_bootid(tmp_path)
    assert str(uuid.UUID(result)) == result
    assert (conf_dir / "bootid.txt").read_text() == result


def test_build_loopbackcfg(tmp_path):
    # Create some config files to be sourced
    grub_dir = tmp_path / "boot" / "grub"
    grub_dir.mkdir(parents=True)
    (grub_dir / "grml64_default.cfg").touch()
    (grub_dir / "grml32_default.cfg").touch()
    (grub_dir / "grml64_options.cfg").touch()

    grml2usb.build_loopbackcfg(str(tmp_path))

    loopback_cfg = grub_dir / "loopback.cfg"
    lines = loopback_cfg.read_text().splitlines()

    assert lines == [
        "# grml2usb generated grub2 configuration file",
        "source /boot/grub/header.cfg",
        "source /boot/grub/grml32_default.cfg",
        "source /boot/grub/grml64_default.cfg",
        "source /boot/grub/grml64_options.cfg",
        "source /boot/grub/addons.cfg",
        "source /boot/grub/footer.cfg",
    ]


@pytest.mark.check_for_usbdevice
def test_extract_device_name():
    """Assert, that 'extract_device_name' returns a device name for a given path"""
    assert grml2usb.extract_device_name("/dev/sda") == "sda"
    assert grml2usb.extract_device_name("/dev/sdb") == "sdb"
    assert grml2usb.extract_device_name("/dev/sdb4") == "sdb"


@pytest.mark.check_for_usbdevice
def test_extract_device_name_invalid():
    """Assert, that 'extract_device_name' raises an Error, when given an incorrect string"""
    with pytest.raises(AttributeError):
        assert grml2usb.extract_device_name("/dev")
    with pytest.raises(AttributeError):
        assert grml2usb.extract_device_name("foobar")


def _run_x(args, check: bool = True, **kwargs):
    # str-ify Paths, not necessary, but for readability in logs.
    args = [arg if isinstance(arg, str) else str(arg) for arg in args]
    args_str = '" "'.join(args)
    print(f'D: Running "{args_str}"', flush=True)
    return subprocess.run(args, check=check, **kwargs)


def _find_free_loopdev() -> str:
    return _run_x(["losetup", "-f"], capture_output=True).stdout.decode().strip()


def _identify_file(path) -> str:
    return (
        _run_x(["file", path], capture_output=True)
        .stdout.decode()
        .strip()
        .split(": ", 1)[1]
    )


@pytest.mark.require_root
def test_smoke(tmp_path):
    loop_dev = _find_free_loopdev()
    partition = f"{loop_dev!s}p1"

    iso_url = "https://daily.grml.org/grml-small-amd64-unstable/latest/grml-small-amd64-unstable_latest.iso"
    iso_name = "grml.iso"
    if not os.path.exists(iso_name):
        _run_x(["curl", "-fSl#", "--output", iso_name, iso_url])

    grml2usb_options = grml2usb.parser.parse_args(
        ["--format", "--force", iso_name, partition]
    )
    print("Options:", grml2usb_options)

    part_size = 1 * 1024 * 1024  # 1 GB
    part_size_sectors = int(part_size * (1024 / 512))
    dd_size = str(int((part_size / 1024) + 100))

    # format (see sfdisk manual page):
    # <start>,<size_in_sectors>,<id>,<bootable>
    # 1st partition, EFI (FAT-12/16/32, ID ef) + bootable flag
    sfdisk_template = f"2048,{part_size_sectors},ef,*\n"
    print("Using sfdisk template:\n", sfdisk_template, "\n---")
    loop_backing_file = tmp_path / "loop"

    _run_x(
        ["dd", "if=/dev/zero", f"of={loop_backing_file!s}", "bs=1M", f"count={dd_size}"]
    )
    sfdisk_input_file = tmp_path / "sfdisk.txt"
    with sfdisk_input_file.open("wt") as fh:
        fh.write(sfdisk_template)
        fh.flush()

    with sfdisk_input_file.open() as fh:
        _run_x(["/sbin/sfdisk", loop_backing_file], stdin=fh)

    _run_x(["losetup", loop_dev, loop_backing_file])
    _run_x(["partprobe", loop_dev])

    try:
        grml2usb.main(grml2usb_options)
    finally:
        _run_x(["losetup", "-d", loop_dev])

    assert _identify_file(loop_backing_file).startswith(
        "DOS/MBR boot sector; partition 1 : ID=0xef, active"
    )
