"""
tests for user state
user absent
user present
user present with custom homedir
"""

import pathlib
import random
import shutil
import sys

import pytest
from saltfactories.utils import random_string

import salt.utils.files
import salt.utils.platform

try:
    import grp
except ImportError:
    grp = None

pytestmark = [
    pytest.mark.slow_test,
    pytest.mark.skip_if_not_root,
    pytest.mark.destructive_test,
    pytest.mark.windows_whitelisted,
]

ANSI_FILESYSTEM_ENCODING = sys.getfilesystemencoding().startswith("ANSI")


@pytest.fixture
def username(sminion):
    _username = random_string("new-account-", uppercase=False)
    try:
        yield _username
    finally:
        try:
            sminion.functions.user.delete(_username, remove=True, force=True)
        except Exception:  # pylint: disable=broad-except
            # The point here is just system cleanup. It can fail if no account was created
            pass


@pytest.fixture
def guid():
    return random.randint(60000, 61000)


@pytest.fixture
def user_home(username, tmp_path):
    if salt.utils.platform.is_windows():
        return tmp_path / username

    return pathlib.Path("/var/lib") / username


@pytest.fixture
def group_1(username, grains):
    groupname = username
    if salt.utils.platform.is_darwin():
        groupname = "staff"
    elif salt.utils.platform.is_photonos():
        groupname = "users"
    elif grains["os_family"] in ("Suse",):
        groupname = "users"
    with pytest.helpers.create_group(name=groupname) as group:
        yield group


@pytest.fixture
def group_2():
    with pytest.helpers.create_group() as group:
        yield group


@pytest.fixture
def existing_account():
    with pytest.helpers.create_account(create_group=True) as _account:
        yield _account


@pytest.mark.slow_test
def test_user_absent(states):
    """
    Test user.absent with a non existing account
    """
    ret = states.user.absent(name=random_string("account-", uppercase=False))
    assert ret.result is True


def test_user_absent_existing_account(states, existing_account):
    """
    Test user.absent with an existing account
    """
    ret = states.user.absent(name=existing_account.username)
    assert ret.result is True


def test_user_present(states, username):
    """
    Test user.present with a non existing account
    """
    ret = states.user.present(name=username)
    assert ret.result is True


def test_user_present_with_existing_group(states, username, existing_account):
    ret = states.user.present(username, gid=existing_account.group.info.gid)
    assert ret.result is True


@pytest.mark.skip_on_windows(
    reason="Home directories are handled differently in Windows"
)
def test_user_present_when_home_dir_does_not_18843(states, existing_account):
    """
    User exists but home directory does not. Home directory get's created
    """
    shutil.rmtree(existing_account.info.home)
    ret = states.user.present(
        name=existing_account.username,
        home=existing_account.info.home,
    )
    assert ret.result is True
    assert pathlib.Path(existing_account.info.home).is_dir()


def test_user_present_nondefault(grains, modules, states, username, user_home):
    ret = states.user.present(name=username, home=str(user_home))
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info

    if salt.utils.platform.is_windows():
        group_name = modules.user.list_groups(username)
    else:
        group_name = grp.getgrgid(user_info["gid"]).gr_name

    if not salt.utils.platform.is_darwin() and not salt.utils.platform.is_windows():
        assert user_home.is_dir()

    if grains["os_family"] in ("Suse",):
        expected_group_name = "users"
    elif grains["os_family"] == "MacOS":
        expected_group_name = "staff"
    elif salt.utils.platform.is_windows():
        expected_group_name = []
    elif grains["os"] == "VMware Photon OS":
        expected_group_name = "users"
    else:
        expected_group_name = username
    assert group_name == expected_group_name


@pytest.mark.skip_on_windows(reason="windows minion does not support 'usergroup'")
def test_user_present_usergroup_false(modules, states, username, group_1, user_home):
    ret = states.user.present(
        name=username,
        gid=group_1.info.gid,
        usergroup=False,
        home=str(user_home),
    )
    assert ret.result is True

    if not salt.utils.platform.is_darwin():
        assert user_home.is_dir()

    user_info = modules.user.info(username)
    assert user_info

    group_name = grp.getgrgid(user_info["gid"]).gr_name
    assert group_name == group_1.name


@pytest.mark.skip_on_windows(reason="windows minion does not support 'usergroup'")
def test_user_present_usergroup_true(modules, states, username, user_home, group_1):
    ret = states.user.present(
        name=username,
        gid=group_1.info.gid,
        usergroup=True,
        home=str(user_home),
    )
    assert ret.result is True

    if not salt.utils.platform.is_darwin():
        assert user_home.is_dir()

    user_info = modules.user.info(username)
    assert user_info

    group_name = grp.getgrgid(user_info["gid"]).gr_name
    assert group_name == group_1.name


def _check_skip(grains):
    if grains["os"] == "MacOS":
        return True
    return False


@pytest.mark.skipif(
    ANSI_FILESYSTEM_ENCODING,
    reason=(
        "A system encoding which supports Unicode characters must be set. "
        f"Current setting is: {sys.getfilesystemencoding()}. "
        "Try setting $LANG='en_US.UTF-8'"
    ),
)
@pytest.mark.skip_initial_gh_actions_failure(skip=_check_skip)
def test_user_present_unicode(states, username, subtests):
    """
    It ensures that unicode GECOS data will be properly handled, without
    any encoding-related failures.
    """
    with subtests.test("Non existing account"):
        ret = states.user.present(
            name=username,
            fullname="Sålt Test",
            roomnumber="①②③",
            workphone="١٢٣٤",
            homephone="६७८",
        )
        assert ret.result is True

    with subtests.test("Update existing account"):
        ret = states.user.present(
            name=username,
            fullname="Sålt Test",
            roomnumber="①②③",
            workphone="١٢٣٤",
            homephone="६७८",
        )
        assert ret.result is True


@pytest.mark.skip_on_windows(
    reason="windows minion does not support roomnumber or phone",
)
def test_user_present_gecos(modules, states, username):
    """
    It ensures that numeric GECOS data will be properly coerced to strings,
    otherwise the state will fail because the GECOS fields are written as
    strings (and show up in the user.info output as such). Thus the
    comparison will fail, since '12345' != 12345.
    """
    fullname = 123345
    roomnumber = 123
    workphone = homephone = 1234567890
    ret = states.user.present(
        name=username,
        fullname=fullname,
        roomnumber=roomnumber,
        workphone=workphone,
        homephone=homephone,
    )
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info

    assert user_info["fullname"] == str(fullname)
    if not salt.utils.platform.is_darwin():
        # MacOS does not supply the following GECOS fields
        assert user_info["roomnumber"] == str(roomnumber)
        assert user_info["workphone"] == str(workphone)
        assert user_info["homephone"] == str(homephone)


@pytest.mark.skip_on_windows(
    reason="windows minion does not support roomnumber or phone",
)
def test_user_present_gecos_empty_fields(modules, states, username):
    """
    It ensures that if no GECOS data is supplied, the fields will be coerced
    into empty strings as opposed to the string "None".
    """
    fullname = roomnumber = workphone = homephone = ""
    ret = states.user.present(
        name=username,
        fullname=fullname,
        roomnumber=roomnumber,
        workphone=workphone,
        homephone=homephone,
    )
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info

    assert user_info["fullname"] == fullname
    if not salt.utils.platform.is_darwin():
        # MacOS does not supply the following GECOS fields
        assert user_info["roomnumber"] == roomnumber
        assert user_info["workphone"] == workphone
        assert user_info["homephone"] == homephone


@pytest.mark.skip_on_windows(reason="windows minion does not support createhome")
@pytest.mark.parametrize("createhome", [True, False])
def test_user_present_home_directory_created(modules, states, username, createhome):
    """
    It ensures that the home directory is created.
    """
    ret = states.user.present(name=username, createhome=createhome)
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info

    assert pathlib.Path(user_info["home"]).is_dir() is createhome


@pytest.mark.skip_on_windows(reason="windows minion does not support persist_home")
@pytest.mark.skip_on_darwin(reason="Mac minion does not support persist_home")
@pytest.mark.parametrize("persist_home", [True, False])
def test_user_present_home_directory_persisted(
    modules, states, existing_account, user_home, persist_home
):
    """
    It ensures that the home directory is persisted when home changed.
    """
    pathlib.Path(existing_account.info.home, "testfile").touch()
    ret = states.user.present(
        name=existing_account.username, home=str(user_home), persist_home=persist_home
    )
    assert ret.result is True

    user_info = modules.user.info(existing_account.info.name)
    assert user_info

    assert pathlib.Path(user_info["home"], "testfile").is_file() is persist_home


@pytest.mark.skip_on_darwin(reason="groups/gid not fully supported")
@pytest.mark.skip_on_windows(reason="groups/gid not fully supported")
def test_user_present_change_gid_but_keep_group(
    modules, states, username, group_1, group_2
):
    """
    This tests the case in which the default group is changed at the same
    time as it is also moved into the "groups" list.
    """

    # Add the user
    ret = states.user.present(name=username, gid=group_1.info.gid)
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info

    assert user_info["gid"] == group_1.info.gid
    assert user_info["groups"] == [group_1.name]

    # Now change the gid and move alt_group to the groups list in the
    # same salt run.
    ret = states.user.present(
        name=username,
        gid=group_2.info.gid,
        groups=[group_1.name],
        allow_gid_change=True,
    )
    assert ret.result is True

    # Be sure that we did what we intended
    user_info = modules.user.info(username)
    assert user_info

    assert user_info["gid"] == group_2.info.gid
    assert user_info["groups"] == [group_2.name, group_1.name]


@pytest.mark.skip_unless_on_windows
def test_user_present_existing(states, username):
    win_profile = f"C:\\User\\{username}"
    win_logonscript = "C:\\logon.vbs"
    win_description = "Test User Account"
    ret = states.user.present(
        name=username,
        win_homedrive="U:",
        win_profile=win_profile,
        win_logonscript=win_logonscript,
        win_description=win_description,
    )
    assert ret.result is True

    win_profile = f"C:\\Users\\{username}"
    win_description = "Temporary Account"
    ret = states.user.present(
        name=username,
        win_homedrive="R:",
        win_profile=win_profile,
        win_logonscript=win_logonscript,
        win_description=win_description,
    )
    assert ret.result is True
    assert ret.changes
    assert "homedrive" in ret.changes
    assert ret.changes["homedrive"] == "R:"
    assert "profile" in ret.changes
    assert ret.changes["profile"] == win_profile
    assert "description" in ret.changes
    assert ret.changes["description"] == win_description


@pytest.mark.skip_unless_on_linux(reason="underlying functionality only runs on Linux")
def test_user_present_change_groups(modules, states, username, group_1, group_2):
    ret = states.user.present(
        name=username,
        groups=[group_1.name, group_2.name],
    )
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info
    assert user_info["groups"] == [group_2.name, group_1.name]

    # run again and remove group_2
    ret = states.user.present(
        name=username,
        groups=[group_1.name],
    )
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info
    assert user_info["groups"] == [group_1.name]


@pytest.mark.skip_unless_on_linux(reason="underlying functionality only runs on Linux")
def test_user_present_change_optional_groups(
    modules, states, username, group_1, group_2
):
    ret = states.user.present(
        name=username,
        optional_groups=[group_1.name, group_2.name],
    )
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info
    assert user_info["groups"] == [group_2.name, group_1.name]

    # run again and remove group_2
    ret = states.user.present(
        name=username,
        optional_groups=[group_1.name],
    )
    assert ret.result is True

    user_info = modules.user.info(username)
    assert user_info
    assert user_info["groups"] == [group_1.name]


@pytest.fixture
def user_present_groups(states):
    groups = ["testgroup1", "testgroup2"]
    try:
        yield groups
    finally:
        for group in groups:
            ret = states.group.absent(name=group)
            assert ret.result is True


@pytest.mark.skip_unless_on_linux(reason="underlying functionality only runs on Linux")
def test_user_present_no_groups(modules, states, username, user_present_groups, guid):
    """
    test user.present when groups arg is not
    included by the group is created in another
    state. Re-run the states to ensure there are
    not changes and it is idempotent.
    """
    ret = states.group.present(name=username, gid=guid)
    assert ret.result is True

    ret = states.user.present(
        name=username,
        uid=guid,
        gid=guid,
    )
    assert ret.result is True
    assert ret.changes["groups"] == [username]
    assert ret.changes["name"] == username

    ret = states.group.present(
        name=user_present_groups[0],
        members=[username],
    )
    assert ret.changes["members"] == [username]

    ret = states.group.present(
        name=user_present_groups[1],
        members=[username],
    )
    assert ret.changes["members"] == [username]

    user_info = modules.user.info(username)
    assert user_info
    assert user_info["groups"] == [username, *user_present_groups]

    # run again, expecting no changes
    ret = states.group.present(name=username)
    assert ret.result is True
    assert ret.changes == {}

    ret = states.user.present(
        name=username,
    )
    assert ret.result is True
    assert ret.changes == {}

    ret = states.group.present(
        name=user_present_groups[0],
        members=[username],
    )
    assert ret.result is True
    assert ret.changes == {}

    ret = states.group.present(
        name=user_present_groups[1],
        members=[username],
    )
    assert ret.result is True
    assert ret.changes == {}

    user_info = modules.user.info(username)
    assert user_info
    assert user_info["groups"] == [username, *user_present_groups]
