Function Test-GroupMembership {
    <#
    .SYNOPSIS
        Tests whether one or more users are members of one or more AD groups.

    .DESCRIPTION
        Checks each user against each group and reports whether the user is a
        direct member, a recursive member through a nested group, or not a
        member. UserName and GroupName accept arrays, or comma-separated text.

    .PARAMETER GroupName
        One or more group names, SamAccountNames, or distinguished names.

    .PARAMETER UserName
        One or more user SamAccountNames, names, or distinguished names.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [string[]]$GroupName,

        [Parameter(Mandatory=$true, Position=1)]
        [string[]]$UserName
    )

    function Split-TestGroupMembershipValue {
        param([string[]]$Value)

        @(
            foreach ($entry in $Value) {
                if ([string]::IsNullOrWhiteSpace($entry)) { continue }
                foreach ($part in ([string]$entry -split ',')) {
                    $clean = $part.Trim().Trim("'").Trim('"')
                    if (-not [string]::IsNullOrWhiteSpace($clean)) { $clean }
                }
            }
        ) | Select-Object -Unique
    }

    function Test-ADObjectMatchesUser {
        param(
            $Member,
            $User
        )

        if ($null -eq $Member -or $null -eq $User) { return $false }

        $memberValues = @(
            $Member.DistinguishedName,
            $Member.SamAccountName,
            $Member.Name
        ) | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { ([string]$_).Trim().ToUpper() }

        $userValues = @(
            $User.DistinguishedName,
            $User.SamAccountName,
            $User.Name
        ) | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { ([string]$_).Trim().ToUpper() }

        foreach ($value in $userValues) {
            if ($memberValues -contains $value) { return $true }
        }

        return $false
    }

    function Find-NestedGroupMembershipPath {
        param(
            [Parameter(Mandatory=$true)]
            [string]$GroupIdentity,

            [Parameter(Mandatory=$true)]
            $User,

            [string[]]$Path = @(),

            [hashtable]$Visited = @{}
        )

        $groupKey = $GroupIdentity.Trim().ToUpper()
        if ($Visited.ContainsKey($groupKey)) { return $null }
        $Visited[$groupKey] = $true

        $members = @(Get-ToolADGroupMember -Identity $GroupIdentity -ErrorAction Stop)
        foreach ($member in $members) {
            if (Test-ADObjectMatchesUser -Member $member -User $User) {
                return @($Path)
            }

            if ([string]$member.ObjectClass -eq 'group') {
                $nestedIdentity = if ($member.DistinguishedName) { [string]$member.DistinguishedName } elseif ($member.SamAccountName) { [string]$member.SamAccountName } else { [string]$member.Name }
                $nestedName = if ($member.Name) { [string]$member.Name } elseif ($member.SamAccountName) { [string]$member.SamAccountName } else { $nestedIdentity }
                $nestedPath = Find-NestedGroupMembershipPath -GroupIdentity $nestedIdentity -User $User -Path @($Path + $nestedName) -Visited $Visited
                if ($null -ne $nestedPath) { return @($nestedPath) }
            }
        }

        return $null
    }

    $groups = @(Split-TestGroupMembershipValue -Value $GroupName)
    $users = @(Split-TestGroupMembershipValue -Value $UserName)

    foreach ($groupIdentity in $groups) {
        $group = $null
        try {
            $group = Get-ToolADGroup -Identity $groupIdentity -ErrorAction Stop
        }
        catch {
            Write-Warning "Could not resolve group '$groupIdentity'. Error: $($_.Exception.Message)"
        }

        foreach ($userIdentity in $users) {
            $result = [ordered]@{
                UserName       = $userIdentity
                UserDisplayName = ''
                GroupName      = $groupIdentity
                GroupDisplayName = ''
                IsMember       = $false
                MembershipType = 'NotMember'
                ViaGroup       = ''
                Message        = ''
            }

            if (-not $group) {
                $result.Message = "Group '$groupIdentity' could not be resolved."
                [pscustomobject]$result
                continue
            }

            $resolvedGroupIdentity = if ($group.DistinguishedName) { [string]$group.DistinguishedName } elseif ($group.SamAccountName) { [string]$group.SamAccountName } else { $groupIdentity }
            $groupDisplay = if ($group.Name) { [string]$group.Name } elseif ($group.SamAccountName) { [string]$group.SamAccountName } else { $groupIdentity }
            $result.GroupName = if ($group.SamAccountName) { [string]$group.SamAccountName } else { $groupIdentity }
            $result.GroupDisplayName = $groupDisplay

            $user = $null
            try {
                $user = Get-ToolADUser -Identity $userIdentity -ErrorAction Stop
            }
            catch {
                $result.Message = "User '$userIdentity' could not be resolved."
                [pscustomobject]$result
                continue
            }

            $userDisplay = if ($user.DisplayName) { [string]$user.DisplayName } elseif ($user.Name) { [string]$user.Name } else { $userIdentity }
            $result.UserName = if ($user.SamAccountName) { [string]$user.SamAccountName } else { $userIdentity }
            $result.UserDisplayName = $userDisplay

            try {
                $directMembers = @(Get-ToolADGroupMember -Identity $resolvedGroupIdentity -ErrorAction Stop)
                $isDirectMember = @($directMembers | Where-Object { Test-ADObjectMatchesUser -Member $_ -User $user }).Count -gt 0

                if ($isDirectMember) {
                    $result.IsMember = $true
                    $result.MembershipType = 'Direct'
                    $result.Message = "$userDisplay is a direct member of $groupDisplay."
                } else {
                    $nestedPath = Find-NestedGroupMembershipPath -GroupIdentity $resolvedGroupIdentity -User $user -Path @() -Visited @{}
                    if ($null -ne $nestedPath -and $nestedPath.Count -gt 0) {
                        $result.IsMember = $true
                        $result.MembershipType = 'Recursive'
                        $result.ViaGroup = $nestedPath -join ' -> '
                        $result.Message = "$userDisplay is a recursive member of $groupDisplay via $($result.ViaGroup)."
                    } else {
                        $result.Message = "$userDisplay is not a member of $groupDisplay."
                    }
                }
            }
            catch {
                $result.Message = "Could not test '$userIdentity' against '$groupIdentity'. Error: $($_.Exception.Message)"
            }

            [pscustomobject]$result
        }
    }
}
