# Copyright 2023 Oliver Smith
# SPDX-License-Identifier: GPL-3.0-or-later
""" Test pmb/parse/kconfig.py """
import pytest
import sys
import os

import pmb_test  # noqa
import pmb.parse.kconfig
from pmb.helpers.exceptions import NonBugError

test_options_checked_count = None


@pytest.fixture
def args(tmpdir, request):
    import pmb.parse
    sys.argv = ["pmbootstrap.py", "kconfig", "check"]
    args = pmb.parse.arguments()
    args.log = args.work + "/log_testsuite.txt"
    pmb.helpers.logging.init(args)
    request.addfinalizer(pmb.helpers.logging.logfd.close)
    return args


def patch_config(monkeypatch):
    """
    Delete the real kconfig_options_* variables in pmb/config/__init__.py and
    replace them with a very basic config for the tests. The idea is that it
    should use all features of the kconfig check code, so we can test all the
    code paths.
    """
    for key in list(pmb.config.__dict__.keys()):
        if key.startswith("kconfig_options"):
            monkeypatch.delattr(pmb.config, key)

    monkeypatch.setattr(pmb.config, "kconfig_options", {
        ">=0.0.0": {  # all versions
            "all": {  # all arches
                "ANDROID_PARANOID_NETWORK": False,
                "BLK_DEV_INITRD": True,
                "DEFAULT_HOSTNAME": "(none)",
            },
        },
        ">=2.6.0": {
            "all": {
                "BINFMT_ELF": True,
            },
        },
        "<4.7.0": {
            "all": {
                "DEVPTS_MULTIPLE_INSTANCES": True,
            },
        },
        "<5.2.0": {
            "armhf armv7 x86": {
                "LBDAF": True
            },
        },
    }, False)

    monkeypatch.setattr(pmb.config, "kconfig_options_waydroid", {
        ">=0.0.0": {
            "all": {
                "SQUASHFS": True,
                "ANDROID_BINDERFS": False,
                "ANDROID_BINDER_DEVICES": ["binder", "hwbinder", "vndbinder"],
            }
        },
    }, False)

    monkeypatch.setattr(pmb.config, "kconfig_options_nftables", {
        ">=3.13.0 <5.17": {
            "all": {
                "NFT_COUNTER": True,
            },
        },
    }, False)


def test_get_all_component_names(monkeypatch):
    patch_config(monkeypatch)
    func = pmb.parse.kconfig.get_all_component_names
    assert func() == ["waydroid", "nftables"]


def test_is_set():
    config = ("CONFIG_WIREGUARD=m\n"
              "# CONFIG_EXT2_FS is not set\n"
              "CONFIG_EXT4_FS=y\n")
    func = pmb.parse.kconfig.is_set
    assert func(config, "WIREGUARD") is True
    assert func(config, "EXT4_FS") is True
    assert func(config, "NON_EXISTING") is False


def test_is_set_str():
    config = 'CONFIG_DEFAULT_HOSTNAME="(none)"\n'
    func = pmb.parse.kconfig.is_set_str
    option = "DEFAULT_HOSTNAME"
    assert func(config, option, "(none)") is True
    assert func(config, option, "hello") is False
    assert func(config, f"{option}_2", "(none)") is False


def test_is_in_array():
    config = 'CONFIG_ANDROID_BINDER_DEVICES="binder,hwbinder,vndbinder"\n'
    func = pmb.parse.kconfig.is_in_array
    option = "ANDROID_BINDER_DEVICES"
    assert func(config, option, "binder") is True
    assert func(config, option, "hwbinder") is True
    assert func(config, option, "vndbinder") is True
    assert func(config, option, "invalidbinder") is False
    assert func(config, f"{option}_2", "binder") is False


def test_check_option():
    func = pmb.parse.kconfig.check_option
    config = ('CONFIG_BOOL=m\n'
              'CONFIG_LIST="a,b,c"\n'
              'CONFIG_STR="test"\n')
    path = "/home/user/myconfig.aarch64"

    assert func("test", False, config, path, "BOOL", True) is True
    assert func("test", True, config, path, "BOOL", True) is True
    assert func("test", True, config, path, "NON_EXISTING", True) is False
    assert func("test", True, config, path, "STR", "test") is True
    assert func("test", True, config, path, "STR", "test2") is False
    assert func("test", True, config, path, "LIST", ["a"]) is True
    assert func("test", True, config, path, "LIST", ["d"]) is False

    with pytest.raises(RuntimeError) as e:
        func("test", True, config, path, "TEST", {"dict": "notsupported"})
    assert "is not supported" in str(e.value)

    with pytest.raises(RuntimeError) as e:
        func("test", True, config, path, "TEST", None)
    assert "is not supported" in str(e.value)


def test_check_config_options_set():
    func = pmb.parse.kconfig.check_config_options_set
    config = ('CONFIG_BOOL=m\n'
              'CONFIG_LIST="a,b,c"\n'
              'CONFIG_STR="test"\n')
    path = "/home/user/myconfig.aarch64"
    arch = "aarch64"
    pkgver = "6.0"
    component = "testcomponent"

    # Skip check because version is too low
    options = {
        ">=6.0.1": {
            "all": {
                "BOOL": False
            }
        }
    }
    assert func(config, path, arch, options, component, pkgver) is True

    # Skip check because version is too high
    options = {
        "<6.0": {
            "all": {
                "BOOL": False
            }
        }
    }
    assert func(config, path, arch, options, component, pkgver) is True

    # Skip with two version that don't match
    options = {
        "<6.2 >=6.0.1": {
            "all": {
                "BOOL": False
            }
        }
    }
    assert func(config, path, arch, options, component, pkgver) is True

    # Version matches, arch does not match
    options = {
        ">=6.0": {
            "armhf": {
                "BOOL": False
            }
        }
    }
    assert func(config, path, arch, options, component, pkgver) is True

    # Version matches, arch matches (aarch64)
    options = {
        ">=6.0": {
            "aarch64": {
                "BOOL": False
            }
        }
    }
    assert func(config, path, arch, options, component, pkgver) is False

    # Version matches, arch matches (all)
    options = {
        ">=6.0": {
            "all": {
                "BOOL": False
            }
        }
    }
    assert func(config, path, arch, options, component, pkgver) is False

    # Version matches, arch matches (all), rule passes
    options = {
        ">=6.0": {
            "all": {
                "BOOL": True
            }
        }
    }
    assert func(config, path, arch, options, component, pkgver) is True


def test_check_config_options_set_details(monkeypatch):
    global test_options_checked_count

    func = pmb.parse.kconfig.check_config_options_set
    config = ('CONFIG_BOOL=m\n'
              'CONFIG_LIST="a,b,c"\n'
              'CONFIG_STR="test"\n')
    path = "/home/user/myconfig.aarch64"
    arch = "aarch64"
    pkgver = "6.0"
    component = "testcomponent"

    def check_option_fake(*args, **kwargs):
        global test_options_checked_count
        test_options_checked_count += 1
        return False

    monkeypatch.setattr(pmb.parse.kconfig, "check_option", check_option_fake)

    options = {
        ">=0.0.0": {
            "all": {
                "BOOL": False,
                "STR": False,
            }
        }
    }

    # No details: stop after first error
    details = False
    test_options_checked_count = 0
    assert func(config, path, arch, options, component, pkgver, details) is False
    assert test_options_checked_count == 1

    # Details: don't stop, do both checks
    details = True
    test_options_checked_count = 0
    assert func(config, path, arch, options, component, pkgver, details) is False
    assert test_options_checked_count == 2


def test_check_config(monkeypatch, tmpdir):
    # Write test kernel config
    tmpdir = str(tmpdir)
    path = f"{tmpdir}/myconfig.aarch64"
    with open(path, "w") as handle:
        handle.write('CONFIG_BOOL=m\n'
                     'CONFIG_LIST="a,b,c"\n'
                     'CONFIG_STR="test"\n')

    patch_config(monkeypatch)

    func = pmb.parse.kconfig.check_config
    arch = "aarch64"
    pkgver = "6.0"

    # Invalid component
    components_list = ["invalid-component-name"]
    with pytest.raises(AssertionError) as e:
        func(path, arch, pkgver, components_list)
    assert "invalid kconfig component name" in str(e.value)

    # Fail base check
    components_list = []
    assert func(path, arch, pkgver, components_list) is False

    # Fails base check, even with enforce=False
    details = False
    enforce = False
    assert func(path, arch, pkgver, components_list, details, enforce) is False

    # Pass base check
    with open(path, "w") as handle:
        handle.write('CONFIG_BLK_DEV_INITRD=y\n'
                     'CONFIG_DEFAULT_HOSTNAME="(none)"\n'
                     'CONFIG_BINFMT_ELF=y\n')
    components_list = []
    assert func(path, arch, pkgver, components_list) is True

    # Fail additional check
    components_list = ["waydroid"]
    assert func(path, arch, pkgver, components_list) is False

    # Fail additional check, but result is still True with enforce=False
    components_list = ["waydroid"]
    details = True
    enforce = False
    assert func(path, arch, pkgver, components_list, details, enforce) is True


def test_check(args, monkeypatch, tmpdir):
    func = pmb.parse.kconfig.check
    details = True
    components_list = []
    patch_config(monkeypatch)

    # Create fake pmaports kernel structure
    tmpdir = str(tmpdir)
    monkeypatch.setattr(args, "aports", tmpdir)
    path_aport = f"{tmpdir}/device/community/linux-nokia-n900"
    path_apkbuild = f"{path_aport}/APKBUILD"
    os.makedirs(path_aport)

    # APKBUILD
    with open(path_apkbuild, "w") as handle:
        handle.write('pkgname=linux-nokia-n900\n'
                     'pkgver=5.15\n'
                     'options="pmb:kconfigcheck-nftables"\n')

    # Non-existing #1
    must_exist = True
    pkgname = "linux-does-not-exist"
    with pytest.raises(RuntimeError) as e:
        func(args, pkgname, components_list, details, must_exist)
    assert "Could not find aport" in str(e.value)

    # Non-existing #2
    must_exist = False
    pkgname = "linux-does-not-exist"
    assert func(args, pkgname, components_list, details, must_exist) is None

    # Invalid kernel config name
    path_kconfig = f"{path_aport}/config-nokia-n900_armv7"
    with open(path_kconfig, "w") as handle:
        handle.write('CONFIG_BOOL=m\n')
    must_exist = True
    pkgname = "linux-nokia-n900"
    with pytest.raises(NonBugError) as e:
        func(args, pkgname, components_list, details, must_exist)
    assert "is not a valid kernel config" in str(e.value)
    os.unlink(path_kconfig)

    # Pass checks of base and nftables
    path_kconfig = f"{path_aport}/config-nokia-n900.armv7"
    with open(path_kconfig, "w") as handle:
        handle.write('CONFIG_BLK_DEV_INITRD=y\n'
                     'CONFIG_DEFAULT_HOSTNAME="(none)"\n'
                     'CONFIG_BINFMT_ELF=y\n'
                     'CONFIG_NFT_COUNTER=y\n')
    must_exist = True
    pkgname = "nokia-n900"
    assert func(args, pkgname, components_list, details, must_exist) is True

    # Don't pass nftables check
    with open(path_kconfig, "w") as handle:
        handle.write('CONFIG_BLK_DEV_INITRD=y\n'
                     'CONFIG_DEFAULT_HOSTNAME="(none)"\n'
                     'CONFIG_BINFMT_ELF=y\n')
    assert func(args, pkgname, components_list, details, must_exist) is False

    # Don't pass waydroid check (extra component check passed via cmdline)
    with open(path_kconfig, "w") as handle:
        handle.write('CONFIG_BLK_DEV_INITRD=y\n'
                     'CONFIG_DEFAULT_HOSTNAME="(none)"\n'
                     'CONFIG_BINFMT_ELF=y\n'
                     'CONFIG_NFT_COUNTER=y\n')
    components_list = ["waydroid"]
    assert func(args, pkgname, components_list, details, must_exist) is False


def test_extract_arch(tmpdir):
    func = pmb.parse.kconfig.extract_arch
    path = f"{tmpdir}/config"

    with open(path, "w") as handle:
        handle.write('CONFIG_ARM=y\n')
    assert func(path) == "armv7"

    with open(path, "w") as handle:
        handle.write('CONFIG_ARM64=y\n')
    assert func(path) == "aarch64"

    with open(path, "w") as handle:
        handle.write('CONFIG_RISCV=y\n')
    assert func(path) == "riscv64"

    with open(path, "w") as handle:
        handle.write('CONFIG_X86_32=y\n')
    assert func(path) == "x86"

    with open(path, "w") as handle:
        handle.write('CONFIG_X86_64=y\n')
    assert func(path) == "x86_64"

    with open(path, "w") as handle:
        handle.write('hello')
    assert func(path) == "unknown"


def test_extract_version(tmpdir):
    func = pmb.parse.kconfig.extract_version
    path = f"{tmpdir}/config"

    with open(path, "w") as handle:
        handle.write("#\n"
                     "# Automatically generated file; DO NOT EDIT.\n"
                     "# Linux/arm64 3.10.93 Kernel Configuration\n")
    assert func(path) == "3.10.93"

    with open(path, "w") as handle:
        handle.write("#\n"
                     "# Automatically generated file; DO NOT EDIT.\n"
                     "# Linux/arm64 6.2.0 Kernel Configuration\n")
    assert func(path) == "6.2.0"

    with open(path, "w") as handle:
        handle.write("#\n"
                     "# Automatically generated file; DO NOT EDIT.\n"
                     "# Linux/riscv 6.1.0-rc3 Kernel Configuration\n")
    assert func(path) == "6.1.0_rc3"

    with open(path, "w") as handle:
        handle.write("#\n"
                     "# Automatically generated file; DO NOT EDIT.\n"
                     "# no version here\n")
    assert func(path) == "unknown"


def test_check_file(tmpdir, monkeypatch):
    patch_config(monkeypatch)
    func = pmb.parse.kconfig.check_file
    path = f"{tmpdir}/config"

    # Fail the basic check
    with open(path, "w") as handle:
        handle.write("#\n"
                     "# Automatically generated file; DO NOT EDIT.\n"
                     "# Linux/arm64 3.10.93 Kernel Configuration\n"
                     "CONFIG_ARM64=y\n")

    func(path) is False

    # Pass the basic check
    with open(path, "w") as handle:
        handle.write("#\n"
                     "# Automatically generated file; DO NOT EDIT.\n"
                     "# Linux/arm64 3.10.93 Kernel Configuration\n"
                     "CONFIG_ARM64=y\n"
                     "BLK_DEV_INITRD=y\n"
                     "DEFAULT_HOSTNAME=\"(none)\"\n"
                     "BINFMT_ELF=y\n"
                     "DEVPTS_MULTIPLE_INSTANCES=y\n"
                     "LBDAF=y\n")

    func(path) is True
