package rfc2307

import (
	"errors"
	"fmt"
	"io/ioutil"
	"reflect"
	"testing"

	"gopkg.in/ldap.v2"

	"github.com/openshift/origin/pkg/auth/ldaputil"
	"github.com/openshift/origin/pkg/auth/ldaputil/testclient"
	"github.com/openshift/origin/pkg/cmd/admin/groups/sync/syncerror"
)

func newTestLDAPInterface(client ldap.Client) *LDAPInterface {
	// below are common test implementations of LDAPInterface fields
	groupQuery := ldaputil.LDAPQueryOnAttribute{
		LDAPQuery: ldaputil.LDAPQuery{
			BaseDN:       "ou=groups,dc=example,dc=com",
			Scope:        ldaputil.ScopeWholeSubtree,
			DerefAliases: ldaputil.DerefAliasesAlways,
			TimeLimit:    0,
			Filter:       "objectClass=groupOfNames",
		},
		QueryAttribute: "dn",
	}
	groupNameAttributes := []string{"cn"}
	groupMembershipAttributes := []string{"member"}
	userQuery := ldaputil.LDAPQueryOnAttribute{
		LDAPQuery: ldaputil.LDAPQuery{
			BaseDN:       "ou=users,dc=example,dc=com",
			Scope:        ldaputil.ScopeWholeSubtree,
			DerefAliases: ldaputil.DerefAliasesAlways,
			TimeLimit:    0,
			Filter:       "objectClass=inetOrgPerson",
		},
		QueryAttribute: "dn",
	}
	userNameAttributes := []string{"cn"}

	errorHandler := syncerror.NewCompoundHandler(
		syncerror.NewMemberLookupOutOfBoundsSuppressor(ioutil.Discard),
		syncerror.NewMemberLookupMemberNotFoundSuppressor(ioutil.Discard),
	)

	return NewLDAPInterface(testclient.NewConfig(client),
		groupQuery,
		groupNameAttributes,
		groupMembershipAttributes,
		userQuery,
		userNameAttributes,
		errorHandler)
}

// newTestUser returns a new LDAP entry with the CN
func newTestUser(CN string) *ldap.Entry {
	return ldap.NewEntry(fmt.Sprintf("cn=%s,ou=users,dc=example,dc=com", CN), map[string][]string{"cn": {CN}})
}

// newTestGroup returns a new LDAP entry with the given CN and member
func newTestGroup(CN, member string) *ldap.Entry {
	DN := fmt.Sprintf("cn=%s,ou=groups,dc=example,dc=com", CN)
	if len(CN) > 0 {
		return ldap.NewEntry(DN, map[string][]string{"cn": {CN}, "member": {member}})
	} else {
		// no CN
		return ldap.NewEntry(DN, map[string][]string{"member": {member}})
	}
}

func TestExtractMembers(t *testing.T) {
	var testCases = []struct {
		name            string
		client          ldap.Client
		expectedError   error
		expectedMembers []*ldap.Entry
	}{
		{
			name: "group lookup errors",
			client: testclient.NewMatchingSearchErrorClient(
				testclient.New(),
				"cn=testGroup,ou=groups,dc=example,dc=com",
				errors.New("generic search error"),
			),
			expectedError:   errors.New("generic search error"),
			expectedMembers: nil,
		},
		{
			name: "member lookup errors",
			// this is a nested test client, the first nest tries to error on the user DN
			// the second nest attempts to give back from the DN mapping
			// the third nest is the default "safe" impl from ldaputil
			client: testclient.NewMatchingSearchErrorClient(
				testclient.NewDNMappingClient(
					testclient.New(),
					map[string][]*ldap.Entry{
						"cn=testGroup,ou=groups,dc=example,dc=com": {newTestGroup("testGroup", "cn=testUser,ou=users,dc=example,dc=com")},
					},
				),
				"cn=testUser,ou=users,dc=example,dc=com",
				errors.New("generic search error"),
			),
			expectedError:   syncerror.NewMemberLookupError("cn=testGroup,ou=groups,dc=example,dc=com", "cn=testUser,ou=users,dc=example,dc=com", errors.New("generic search error")),
			expectedMembers: nil,
		},
		{
			name: "out of scope member lookup suppressed",
			client: testclient.NewMatchingSearchErrorClient(
				testclient.NewDNMappingClient(
					testclient.New(),
					map[string][]*ldap.Entry{
						"cn=testGroup,ou=groups,dc=example,dc=com": {newTestGroup("testGroup", "cn=testUser,ou=users,dc=other-example,dc=com")},
					},
				),
				"cn=testUser,ou=users,dc=other-example,dc=com",
				ldaputil.NewQueryOutOfBoundsError("cn=testUser,ou=users,dc=other-example,dc=com", "cn=testGroup,ou=groups,dc=example,dc=com"),
			),
			expectedError:   nil,
			expectedMembers: []*ldap.Entry{},
		},
		{
			name: "no such object member lookup error suppressed",
			client: testclient.NewMatchingSearchErrorClient(
				testclient.NewDNMappingClient(
					testclient.New(),
					map[string][]*ldap.Entry{
						"cn=testGroup,ou=groups,dc=example,dc=com": {newTestGroup("testGroup", "cn=testUser,ou=users,dc=other-example,dc=com")},
					},
				),
				"cn=testUser,ou=users,dc=other-example,dc=com",
				ldaputil.NewNoSuchObjectError("cn=testUser,ou=users,dc=other-example,dc=com"),
			),
			expectedError:   nil,
			expectedMembers: []*ldap.Entry{},
		},
		{
			name: "member not found member lookup error suppressed",
			client: testclient.NewMatchingSearchErrorClient(
				testclient.NewDNMappingClient(
					testclient.New(),
					map[string][]*ldap.Entry{
						"cn=testGroup,ou=groups,dc=example,dc=com": {newTestGroup("testGroup", "cn=testUser,ou=users,dc=other-example,dc=com")},
					},
				),
				"cn=testUser,ou=users,dc=other-example,dc=com",
				ldaputil.NewEntryNotFoundError("cn=testUser,ou=users,dc=other-example,dc=com", "objectClass=groupOfNames"),
			),
			expectedError:   nil,
			expectedMembers: []*ldap.Entry{},
		},
		{
			name: "no errors",
			client: testclient.NewDNMappingClient(
				testclient.New(),
				map[string][]*ldap.Entry{
					"cn=testGroup,ou=groups,dc=example,dc=com": {newTestGroup("testGroup", "cn=testUser,ou=users,dc=example,dc=com")},
					"cn=testUser,ou=users,dc=example,dc=com":   {newTestUser("testUser")},
				},
			),
			expectedError:   nil,
			expectedMembers: []*ldap.Entry{newTestUser("testUser")},
		},
	}
	for _, testCase := range testCases {
		ldapInterface := newTestLDAPInterface(testCase.client)
		members, err := ldapInterface.ExtractMembers("cn=testGroup,ou=groups,dc=example,dc=com")
		if !reflect.DeepEqual(err, testCase.expectedError) {
			t.Errorf("%s: incorrect error returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedError, err)
		}
		if !reflect.DeepEqual(members, testCase.expectedMembers) {
			t.Errorf("%s: incorrect members returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedMembers, members)
		}
	}
}

func TestGroupEntryFor(t *testing.T) {
	var testCases = []struct {
		name                string
		cacheSeed           map[string]*ldap.Entry
		queryBaseDNOverride string
		client              ldap.Client
		expectedError       error
		expectedEntry       *ldap.Entry
	}{
		{
			name:          "cached get",
			cacheSeed:     map[string]*ldap.Entry{"cn=testGroup,ou=groups,dc=example,dc=com": newTestGroup("testGroup", "cn=testUser,ou=users,dc=example,dc=com")},
			expectedError: nil,
			expectedEntry: newTestGroup("testGroup", "cn=testUser,ou=users,dc=example,dc=com"),
		},
		{
			name:                "search request failure",
			queryBaseDNOverride: "dc=foo",
			expectedError:       ldaputil.NewQueryOutOfBoundsError("cn=testGroup,ou=groups,dc=example,dc=com", "dc=foo"),
			expectedEntry:       nil,
		},
		{
			name: "query failure",
			client: testclient.NewMatchingSearchErrorClient(
				testclient.New(),
				"cn=testGroup,ou=groups,dc=example,dc=com",
				errors.New("generic search error"),
			),
			expectedError: errors.New("generic search error"),
			expectedEntry: nil,
		},
		{
			name: "no errors",
			client: testclient.NewDNMappingClient(
				testclient.New(),
				map[string][]*ldap.Entry{
					"cn=testGroup,ou=groups,dc=example,dc=com": {newTestGroup("testGroup", "cn=testUser,ou=users,dc=example,dc=com")},
				},
			),
			expectedError: nil,
			expectedEntry: newTestGroup("testGroup", "cn=testUser,ou=users,dc=example,dc=com"),
		},
	}
	for _, testCase := range testCases {
		ldapInterface := newTestLDAPInterface(testCase.client)
		if len(testCase.cacheSeed) > 0 {
			ldapInterface.cachedGroups = testCase.cacheSeed
		}
		if len(testCase.queryBaseDNOverride) > 0 {
			ldapInterface.groupQuery.BaseDN = testCase.queryBaseDNOverride
		}
		entry, err := ldapInterface.GroupEntryFor("cn=testGroup,ou=groups,dc=example,dc=com")
		if !reflect.DeepEqual(err, testCase.expectedError) {
			t.Errorf("%s: incorrect error returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedError, err)
		}
		if !reflect.DeepEqual(entry, testCase.expectedEntry) {
			t.Errorf("%s: incorrect entry returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedEntry, entry)
		}
	}
}

func TestListGroups(t *testing.T) {
	var testCases = []struct {
		name              string
		client            ldap.Client
		groupUIDAttribute string
		expectedError     error
		expectedGroups    []string
	}{
		{
			name: "query errors",
			client: testclient.NewMatchingSearchErrorClient(
				testclient.New(),
				"ou=groups,dc=example,dc=com",
				errors.New("generic search error"),
			),
			expectedError:  errors.New("generic search error"),
			expectedGroups: nil,
		},
		{
			name: "no UID on entry",
			client: testclient.NewDNMappingClient(
				testclient.New(),
				map[string][]*ldap.Entry{
					"ou=groups,dc=example,dc=com": {newTestGroup("", "cn=testUser,ou=users,dc=example,dc=com")},
				},
			),
			groupUIDAttribute: "cn",
			expectedError:     fmt.Errorf("unable to find LDAP group UID for %s", newTestGroup("", "cn=testUser,ou=users,dc=example,dc=com")),
			expectedGroups:    nil,
		},
		{
			name: "no error",
			client: testclient.NewDNMappingClient(
				testclient.New(),
				map[string][]*ldap.Entry{
					"ou=groups,dc=example,dc=com": {newTestGroup("testGroup", "cn=testUser,ou=users,dc=example,dc=com")},
				},
			),
			expectedError:  nil,
			expectedGroups: []string{"cn=testGroup,ou=groups,dc=example,dc=com"},
		},
	}
	for _, testCase := range testCases {
		ldapInterface := newTestLDAPInterface(testCase.client)
		if len(testCase.groupUIDAttribute) > 0 {
			ldapInterface.groupQuery.QueryAttribute = testCase.groupUIDAttribute
		}
		groupNames, err := ldapInterface.ListGroups()
		if !reflect.DeepEqual(err, testCase.expectedError) {
			t.Errorf("%s: incorrect error returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedError, err)
		}
		if !reflect.DeepEqual(groupNames, testCase.expectedGroups) {
			t.Errorf("%s: incorrect entry returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedGroups, groupNames)
		}
	}
}

func TestUserEntryFor(t *testing.T) {
	var testCases = []struct {
		name                string
		cacheSeed           map[string]*ldap.Entry
		queryBaseDNOverride string
		client              ldap.Client
		expectedError       error
		expectedEntry       *ldap.Entry
	}{
		{
			name: "cached get",
			cacheSeed: map[string]*ldap.Entry{
				"cn=testUser,ou=users,dc=example,dc=com": newTestUser("testUser"),
			},
			expectedError: nil,
			expectedEntry: newTestUser("testUser"),
		},
		{
			name:                "search request failure",
			queryBaseDNOverride: "dc=foo",
			expectedError:       ldaputil.NewQueryOutOfBoundsError("cn=testUser,ou=users,dc=example,dc=com", "dc=foo"),
			expectedEntry:       nil,
		},
		{
			name: "query failure",
			client: testclient.NewMatchingSearchErrorClient(
				testclient.New(),
				"cn=testUser,ou=users,dc=example,dc=com",
				errors.New("generic search error"),
			),
			expectedError: errors.New("generic search error"),
			expectedEntry: nil,
		},
		{
			name: "no errors",
			client: testclient.NewDNMappingClient(
				testclient.New(),
				map[string][]*ldap.Entry{
					"cn=testUser,ou=users,dc=example,dc=com": {newTestUser("testUser")},
				},
			),
			expectedError: nil,
			expectedEntry: newTestUser("testUser"),
		},
	}
	for _, testCase := range testCases {
		ldapInterface := newTestLDAPInterface(testCase.client)
		if len(testCase.cacheSeed) > 0 {
			ldapInterface.cachedUsers = testCase.cacheSeed
		}
		if len(testCase.queryBaseDNOverride) > 0 {
			ldapInterface.userQuery.BaseDN = testCase.queryBaseDNOverride
		}
		entry, err := ldapInterface.userEntryFor("cn=testUser,ou=users,dc=example,dc=com")
		if !reflect.DeepEqual(err, testCase.expectedError) {
			t.Errorf("%s: incorrect error returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedError, err)
		}
		if !reflect.DeepEqual(entry, testCase.expectedEntry) {
			t.Errorf("%s: incorrect entry returned:\n\texpected:\n\t%v\n\tgot:\n\t%v\n", testCase.name, testCase.expectedEntry, entry)
		}
	}
}