# unit tests for driver disk utilities (utils/dd)

import os
import shutil
import unittest
import tempfile
import subprocess

from abc import ABC

from contextlib import contextmanager
from collections import namedtuple
from rpmfluff import SourceFile, GeneratedSourceFile, SimpleRpmBuild
from rpmfluff.utils import expectedArch
from rpmfluff.make import make_elf
from shutup import shutup

TOP_SRCDIR = os.environ.get("top_builddir", "../..")
UTILDIR = os.path.join(TOP_SRCDIR, "utils/dd")

# helpers for calling the utilities
Driver = namedtuple("Driver", "source name flags description")


def dd_list(dd_path, kernel_ver, anaconda_ver):
    out = subprocess.check_output([os.path.join(UTILDIR, "dd_list"),
                                   '-d', dd_path,
                                   '-k', kernel_ver,
                                   '-a', anaconda_ver],
                                  stderr=open('/dev/null')).decode('utf-8')
    return [Driver(*d.split('\n', 3)) for d in out.split('\n---\n')[:-1]]


def dd_extract(rpm_path, outdir, kernel_ver, flags='-blmf'):
    out = subprocess.check_output([os.path.join(UTILDIR, "dd_extract"),
                                   flags,
                                   '-r', rpm_path,
                                   '-d', outdir,
                                   '-k', kernel_ver],
                                  stderr=subprocess.STDOUT).decode('utf-8')
    return out


def listfiles(dirname):
    return set(os.path.join(root, f)
               for root, dirs, files in os.walk(dirname)
               for f in files)


# helpers for creating RPMs to test with
@contextmanager
def in_tempdir(prefix='tmp'):
    oldcwd = os.getcwd()
    tmpdir = tempfile.mkdtemp(prefix=prefix)
    os.chdir(tmpdir)
    yield
    os.chdir(oldcwd)
    shutil.rmtree(tmpdir)


def make_rpm(outdir, name='test', version='1.0', release='1', arch=None,
             for_anaconda_ver=None, for_kernel_ver=None,
             payload=None):
    """Create RPM for tests by using rpmfluff library."""
    p = SimpleRpmBuild(name, version, release)
    if for_anaconda_ver:
        p.add_provides('installer-enhancement = %s' % for_anaconda_ver)
    if for_kernel_ver:
        p.add_provides('kernel-modules >= %s' % for_kernel_ver)
    if payload is None:
        payload = []
    for item in payload:
        p.add_installed_file(item.path,
                             item.create_source_file(),
                             **item.kwargs)
    with in_tempdir("anaconda-test-dd."):
        with shutup():
            p.make()
        rpmfile = p.get_built_rpm(arch or expectedArch)
        outfile = os.path.join(outdir, os.path.basename(rpmfile))
        shutil.move(rpmfile, outfile)
    return p


class RPMFile(ABC):
    """Base class for simple container object for information about RPM payloads"""
    def __init__(self, path, srcpath=None, mode=None):
        self.path = path
        self.srcpath = srcpath or os.path.basename(path)
        self.kwargs = {}

        if mode:
            self.kwargs["mode"] = mode

    def create_source_file(self):
        """Create source file for RPM fluff."""
        pass


class TextRPMFile(RPMFile):
    """RPM file with text content"""
    def __init__(self, contents='', **kwargs):
        super().__init__(**kwargs)
        self.contents = contents

    def create_source_file(self):
        return SourceFile(self.srcpath, self.contents)


class BinRPMFile(RPMFile):
    """RPM file with binary content"""
    def create_source_file(self):
        return GeneratedSourceFile(self.srcpath, make_elf())


binfile = BinRPMFile(
    path="/usr/bin/fun",
    mode="0755"
)
libfile = BinRPMFile(
    path="/usr/lib/fun.so",
    mode="0755"
)
fwfile = TextRPMFile(
    path="/lib/firmware/fun.fw",
    contents="HELLO I AM FIRMWARE"
)
kofile = TextRPMFile(
    path="/lib/modules/KERNELVER/extra/net/fun.ko",
    contents="KERNEL MODULE??? YOU BETCHA"
)


# Finally, the actual test cases
class ASelfTestCase(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp(prefix="dd_tests.")

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_rpmfluff_simple(self):
        """check if rpmfluff is working"""
        p = make_rpm(outdir=self.tmpdir)
        rpmfile = os.path.basename(p.get_built_rpm(expectedArch))
        assert rpmfile in os.listdir(self.tmpdir)

    def test_rpmfluff_payload(self):
        """check if rpmfluff can add files to built RPMs"""
        p = make_rpm(outdir=self.tmpdir, payload=(binfile, kofile))
        rpmfile = os.path.basename(p.get_built_rpm(expectedArch))
        assert rpmfile in os.listdir(self.tmpdir)

    def test_utils_exist(self):
        """check that the dd utilities exist"""
        assert "dd_list" in os.listdir(UTILDIR)
        assert "dd_extract" in os.listdir(UTILDIR)


class DD_List_TestCase(unittest.TestCase):
    def setUp(self):
        self.tmpdir = tempfile.mkdtemp(prefix="dd_tests.")
        self.k_ver = "4.1.4-333"
        self.a_ver = "22.0"

    def dd_list(self, dd_dir=None, kernel_ver=None, anaconda_ver=None):
        return dd_list(dd_dir or self.tmpdir,
                       kernel_ver or self.k_ver,
                       anaconda_ver or self.a_ver)

    def tearDown(self):
        shutil.rmtree(self.tmpdir)

    def test_dd_list(self):
        """dd_list: check output format"""
        rpm = make_rpm(self.tmpdir, for_kernel_ver=self.k_ver)
        drivers = self.dd_list()
        assert len(drivers) == 1
        d = drivers[0]
        assert d.name == rpm.name
        assert d.description == rpm.basePackage.description
        assert d.description
        assert os.path.exists(d.source)
        assert "modules" in d.flags
        assert "firmwares" in d.flags
        assert "binaries" not in d.flags
        assert "libraries" not in d.flags

    def test_dd_list_multiple(self):
        """dd_list: multiple outputs for multiple packages"""
        names = ['fun', 'even_more_fun', 'too_much_fun']
        for name in names:
            make_rpm(self.tmpdir, name=name, for_kernel_ver=self.k_ver)
        drivers = self.dd_list()
        assert len(drivers) == len(names)
        assert set(d.name for d in drivers) == set(names)

    def test_dd_list_binaries(self):
        """dd_list: 'Provides:installer-enhancement' implies bins/libs"""
        make_rpm(self.tmpdir, for_anaconda_ver=self.a_ver)
        drivers = self.dd_list()
        assert not (drivers == [])
        d = drivers[0]
        assert "binaries" in d.flags
        assert "libraries" in d.flags
        assert "modules" not in d.flags
        assert "firmwares" not in d.flags

    def test_dd_list_old_kmods(self):
        """dd_list: ignore kmods if our kernel is too old"""
        make_rpm(self.tmpdir, for_kernel_ver="5.0.1-555")
        assert self.dd_list() == []

    def test_dd_list_z_stream_kmods(self):
        """dd_list: accept kmods for z-stream kernels (#1207831)"""
        make_rpm(self.tmpdir, for_kernel_ver=self.k_ver)
        drivers = self.dd_list(kernel_ver=self.k_ver+".3")
        assert drivers != []
        d = drivers[0]
        assert "modules" in d.flags

    def test_dd_list_anaconda_old(self):
        """dd_list: ignore installer-enhancements if version doesn't match"""
        make_rpm(self.tmpdir, for_anaconda_ver="23.0")
        assert self.dd_list() == []

    def test_dd_list_no_rpms(self):
        """dd_list: empty directory returns no results"""
        assert self.dd_list() == []

    def test_dd_list_missing_dir(self):
        """dd_list: missing directory returns no results"""
        assert self.dd_list(dd_dir="/non/existent/path") == []


class DD_Extract_TestCase(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.k_ver = "4.1.4-333"
        cls.a_ver = "22.0"
        cls.tmpdir = tempfile.mkdtemp(prefix="dd_tests.")
        cls.rpmpayload = (binfile, kofile, fwfile, libfile)
        make_rpm(cls.tmpdir, payload=cls.rpmpayload)
        (cls.rpmfile,) = listfiles(cls.tmpdir)

    def setUp(self):
        self.outdir = os.path.join(self.tmpdir, "outdir")
        os.mkdir(self.outdir)

    def dd_extract(self, flags='-bmlf'):
        dd_extract(self.rpmfile, self.outdir, self.k_ver, flags=flags)
        return listfiles(self.outdir)

    def tearDown(self):
        shutil.rmtree(self.outdir)

    @classmethod
    def tearDownClass(cls):
        shutil.rmtree(cls.tmpdir)

    def test_dd_extract(self):
        """dd_extract: files are extracted correctly"""
        self.dd_extract()
        for item in self.rpmpayload:
            out_path = self.outdir+item.path
            if isinstance(item, TextRPMFile):
                with open(out_path) as f:
                    assert item.contents == f.read()
            elif isinstance(item, BinRPMFile):
                assert os.path.exists(out_path)
                # check that file have some generated content
                assert os.stat(out_path).st_size > 1

    def test_dd_extract_chmod(self):
        """dd_extract: files get correct mode (#1222056)"""
        self.dd_extract()
        for f in self.rpmpayload:
            # test only files which have file mode explicitly set
            if 'mode' in f.kwargs:
                binmode = os.stat(self.outdir+f.path).st_mode
                expectmode = int(f.kwargs['mode'], 8)
                print("testing", f.path)
                assert binmode & expectmode == expectmode

    def test_dd_extract_modules(self):
        """dd_extract: using --modules extracts only .ko files"""
        outfiles = self.dd_extract(flags='--modules')
        assert outfiles == set([self.outdir+kofile.path])

    def test_dd_extract_binaries(self):
        """dd_extract: using --binaries extracts only /bin, /sbin, etc."""
        outfiles = self.dd_extract(flags='--binaries')
        assert outfiles == set([self.outdir+binfile.path])

    def test_dd_extract_libs(self):
        """dd_extract: using --libraries extracts only /lib etc."""
        outfiles = self.dd_extract(flags='--libraries')
        assert outfiles == set([self.outdir+libfile.path])

    def test_dd_extract_firmware(self):
        """dd_extract: using --firmwares extracts only /lib/firmware"""
        outfiles = self.dd_extract(flags='--firmwares')
        assert outfiles == set([self.outdir+fwfile.path])
