# ABOUTME: Tests for user detection — parsing /etc/passwd, avatar lookup, GTK theme reading.
# ABOUTME: Uses temporary files and mocking to avoid system dependencies.

from pathlib import Path
from dataclasses import dataclass

import pytest

from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme


class TestGetUsers:
    """Tests for parsing /etc/passwd to find login users."""

    def test_returns_users_in_uid_range(self, tmp_path: Path) -> None:
        passwd = tmp_path / "passwd"
        passwd.write_text(
            "root:x:0:0:root:/root:/bin/bash\n"
            "nobody:x:65534:65534:Nobody:/:/usr/bin/nologin\n"
            "dominik:x:1000:1000:Dominik:/home/dominik:/bin/zsh\n"
            "testuser:x:1001:1001:Test User:/home/testuser:/bin/bash\n"
        )

        users = get_users(passwd_path=passwd)

        assert len(users) == 2
        assert users[0].username == "dominik"
        assert users[0].uid == 1000
        assert users[0].gecos == "Dominik"
        assert users[0].home == Path("/home/dominik")
        assert users[1].username == "testuser"

    def test_excludes_nologin_shells(self, tmp_path: Path) -> None:
        passwd = tmp_path / "passwd"
        passwd.write_text(
            "systemuser:x:1000:1000:System:/home/system:/usr/sbin/nologin\n"
            "falseuser:x:1001:1001:False:/home/false:/bin/false\n"
            "realuser:x:1002:1002:Real:/home/real:/bin/bash\n"
        )

        users = get_users(passwd_path=passwd)

        assert len(users) == 1
        assert users[0].username == "realuser"

    def test_returns_empty_for_no_matching_users(self, tmp_path: Path) -> None:
        passwd = tmp_path / "passwd"
        passwd.write_text("root:x:0:0:root:/root:/bin/bash\n")

        users = get_users(passwd_path=passwd)

        assert users == []

    def test_handles_missing_gecos_field(self, tmp_path: Path) -> None:
        passwd = tmp_path / "passwd"
        passwd.write_text("user:x:1000:1000::/home/user:/bin/bash\n")

        users = get_users(passwd_path=passwd)

        assert len(users) == 1
        assert users[0].gecos == ""
        assert users[0].display_name == "user"

    def test_skips_invalid_uid(self, tmp_path: Path) -> None:
        """Corrupt /etc/passwd with non-numeric UID should not crash."""
        passwd = tmp_path / "passwd"
        passwd.write_text(
            "corrupt:x:NOTANUMBER:1000:Corrupt:/home/corrupt:/bin/bash\n"
            "valid:x:1000:1000:Valid:/home/valid:/bin/bash\n"
        )

        users = get_users(passwd_path=passwd)

        assert len(users) == 1
        assert users[0].username == "valid"

    def test_skips_username_with_slash(self, tmp_path: Path) -> None:
        """Usernames containing path separators should be rejected."""
        passwd = tmp_path / "passwd"
        passwd.write_text(
            "../evil:x:1000:1000:Evil:/home/evil:/bin/bash\n"
            "normal:x:1001:1001:Normal:/home/normal:/bin/bash\n"
        )

        users = get_users(passwd_path=passwd)

        assert len(users) == 1
        assert users[0].username == "normal"


class TestGetAvatarPath:
    """Tests for avatar file lookup."""

    def test_finds_accountsservice_icon(self, tmp_path: Path) -> None:
        icons_dir = tmp_path / "icons"
        icons_dir.mkdir()
        avatar = icons_dir / "dominik"
        avatar.write_bytes(b"PNG")

        result = get_avatar_path("dominik", accountsservice_dir=icons_dir)

        assert result == avatar

    def test_falls_back_to_dot_face(self, tmp_path: Path) -> None:
        home = tmp_path / "home" / "dominik"
        home.mkdir(parents=True)
        face = home / ".face"
        face.write_bytes(b"PNG")
        empty_icons = tmp_path / "no_icons"

        result = get_avatar_path(
            "dominik", accountsservice_dir=empty_icons, home_dir=home
        )

        assert result == face

    def test_ignores_symlinked_face(self, tmp_path: Path) -> None:
        """~/.face as symlink should be ignored to prevent traversal."""
        home = tmp_path / "home" / "attacker"
        home.mkdir(parents=True)
        target = tmp_path / "secret.txt"
        target.write_text("sensitive data")
        face = home / ".face"
        face.symlink_to(target)
        empty_icons = tmp_path / "no_icons"

        result = get_avatar_path(
            "attacker", accountsservice_dir=empty_icons, home_dir=home
        )

        assert result is None

    def test_returns_none_when_no_avatar(self, tmp_path: Path) -> None:
        empty_icons = tmp_path / "no_icons"
        home = tmp_path / "home" / "nobody"

        result = get_avatar_path(
            "nobody", accountsservice_dir=empty_icons, home_dir=home
        )

        assert result is None


class TestGetUserGtkTheme:
    """Tests for reading GTK theme from user's settings.ini."""

    def test_reads_theme_from_settings(self, tmp_path: Path) -> None:
        gtk_dir = tmp_path / ".config" / "gtk-4.0"
        gtk_dir.mkdir(parents=True)
        settings = gtk_dir / "settings.ini"
        settings.write_text(
            "[Settings]\n"
            "gtk-theme-name=Adwaita-dark\n"
            "gtk-icon-theme-name=Papirus\n"
        )

        result = get_user_gtk_theme(config_dir=gtk_dir)

        assert result == "Adwaita-dark"

    def test_returns_none_when_no_settings(self, tmp_path: Path) -> None:
        gtk_dir = tmp_path / "nonexistent"

        result = get_user_gtk_theme(config_dir=gtk_dir)

        assert result is None

    def test_returns_none_when_no_theme_key(self, tmp_path: Path) -> None:
        gtk_dir = tmp_path / ".config" / "gtk-4.0"
        gtk_dir.mkdir(parents=True)
        settings = gtk_dir / "settings.ini"
        settings.write_text("[Settings]\ngtk-icon-theme-name=Papirus\n")

        result = get_user_gtk_theme(config_dir=gtk_dir)

        assert result is None

    def test_returns_none_for_corrupt_settings_ini(self, tmp_path: Path) -> None:
        """settings.ini without section header should not crash."""
        gtk_dir = tmp_path / ".config" / "gtk-4.0"
        gtk_dir.mkdir(parents=True)
        settings = gtk_dir / "settings.ini"
        settings.write_text("gtk-theme-name=Adwaita-dark\n")

        result = get_user_gtk_theme(config_dir=gtk_dir)

        assert result is None

    def test_handles_interpolation_characters(self, tmp_path: Path) -> None:
        """Theme names with % characters should not trigger interpolation errors."""
        gtk_dir = tmp_path / ".config" / "gtk-4.0"
        gtk_dir.mkdir(parents=True)
        settings = gtk_dir / "settings.ini"
        settings.write_text("[Settings]\ngtk-theme-name=My%Theme\n")

        result = get_user_gtk_theme(config_dir=gtk_dir)

        assert result == "My%Theme"

    def test_ignores_symlinked_accountsservice_icon(self, tmp_path: Path) -> None:
        """AccountsService icon as symlink should be ignored to prevent traversal."""
        icons_dir = tmp_path / "icons"
        icons_dir.mkdir()
        target = tmp_path / "secret.txt"
        target.write_text("sensitive data")
        icon = icons_dir / "attacker"
        icon.symlink_to(target)

        result = get_avatar_path(
            "attacker", accountsservice_dir=icons_dir
        )

        assert result is None
