package identity

import (
	"context"
	"errors"
	"testing"

	"github.com/go-ldap/ldap/v3"
	libregraph "github.com/opencloud-eu/libre-graph-api-go"
	"github.com/qsfera/server/services/graph/pkg/identity/mocks"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

var classEntry = ldap.NewEntry("qsferaEducationExternalId=Math0123",
	map[string][]string{
		"cn":                           {"Math"},
		"qsferaEducationExternalId": {"Math0123"},
		"qsferaEducationClassType":  {"course"},
		"entryUUID":                    {"abcd-defg"},
	})

var classEntryWithSchool = ldap.NewEntry("qsferaEducationExternalId=Math0123",
	map[string][]string{
		"cn":                           {"Math"},
		"qsferaEducationExternalId": {"Math0123"},
		"qsferaEducationClassType":  {"course"},
		"entryUUID":                    {"abcd-defg"},
		"qsferaMemberOfSchool":      {"abcd-defg"},
	})

var classEntryWithMember = ldap.NewEntry("qsferaEducationExternalId=Math0123",
	map[string][]string{
		"cn":                           {"Math"},
		"qsferaEducationExternalId": {"Math0123"},
		"qsferaEducationClassType":  {"course"},
		"entryUUID":                    {"abcd-defg"},
		"member":                       {"uid=user"},
	})

func TestCreateEducationClass(t *testing.T) {
	lm := &mocks.Client{}
	lm.On("Add", mock.Anything).
		Return(nil)

	lm.On("Search", mock.Anything).
		Return(
			&ldap.SearchResult{
				Entries: []*ldap.Entry{classEntry},
			},
			nil)

	b, err := getMockedBackend(lm, eduConfig, &logger)
	assert.Nil(t, err)
	assert.NotEqual(t, "", b.educationConfig.classObjectClass)
	class := libregraph.NewEducationClass("Math", "course")
	class.SetExternalId("Math0123")
	class.SetId("abcd-defg")
	resClass, err := b.CreateEducationClass(context.Background(), *class)
	lm.AssertNumberOfCalls(t, "Add", 1)
	lm.AssertNumberOfCalls(t, "Search", 1)
	assert.Nil(t, err)
	assert.NotNil(t, resClass)
	assert.Equal(t, resClass.GetDisplayName(), class.GetDisplayName())
	assert.Equal(t, resClass.GetId(), class.GetId())
	assert.Equal(t, resClass.GetExternalId(), class.GetExternalId())
	assert.Equal(t, resClass.GetClassification(), class.GetClassification())
}

func TestGetEducationClasses(t *testing.T) {
	lm := &mocks.Client{}
	lm.On("Search", mock.Anything).Return(nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock")))
	b, _ := getMockedBackend(lm, lconfig, &logger)
	_, err := b.GetEducationClasses(context.Background())
	assert.ErrorContains(t, err, "itemNotFound:")

	lm = &mocks.Client{}
	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{}, nil)
	b, _ = getMockedBackend(lm, lconfig, &logger)
	g, err := b.GetEducationClasses(context.Background())
	if err != nil {
		t.Errorf("Expected success, got '%s'", err.Error())
	} else if g == nil || len(g) != 0 {
		t.Errorf("Expected zero length user slice")
	}

	lm = &mocks.Client{}
	lm.On("Search", mock.Anything).Return(&ldap.SearchResult{
		Entries: []*ldap.Entry{classEntry},
	}, nil)
	b, _ = getMockedBackend(lm, lconfig, &logger)
	g, err = b.GetEducationClasses(context.Background())
	if err != nil {
		t.Errorf("Expected GetEducationClasses to succeed. Got %s", err.Error())
	} else if *g[0].Id != classEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
		t.Errorf("Expected GetEducationClasses to return a valid group")
	}
}

func TestGetEducationClass(t *testing.T) {
	tests := []struct {
		name                 string
		id                   string
		filter               string
		expectedItemNotFound bool
	}{
		{
			name:                 "Test search class using id",
			id:                   "abcd-defg",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=abcd-defg)(qsferaEducationExternalId=abcd-defg)))",
			expectedItemNotFound: false,
		},
		{
			name:                 "Test search class using unknown Id",
			id:                   "xxxx-xxxx",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=xxxx-xxxx)(qsferaEducationExternalId=xxxx-xxxx)))",
			expectedItemNotFound: true,
		},
		{
			name:                 "Test search class using external ID",
			id:                   "Math0123",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=Math0123)(qsferaEducationExternalId=Math0123)))",
			expectedItemNotFound: false,
		},
		{
			name:                 "Test search school using unknown externalID",
			id:                   "Unknown3210",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=Unknown3210)(qsferaEducationExternalId=Unknown3210)))",
			expectedItemNotFound: true,
		},
	}

	for _, tt := range tests {
		lm := &mocks.Client{}
		sr := &ldap.SearchRequest{
			BaseDN:     "ou=groups,dc=test",
			Scope:      2,
			SizeLimit:  1,
			Filter:     tt.filter,
			Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember"},
			Controls:   []ldap.Control(nil),
		}
		if tt.expectedItemNotFound {
			lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
		} else {
			lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntry}}, nil)
		}

		b, err := getMockedBackend(lm, eduConfig, &logger)
		assert.Nil(t, err)

		class, err := b.GetEducationClass(context.Background(), tt.id)
		lm.AssertNumberOfCalls(t, "Search", 1)

		if tt.expectedItemNotFound {
			assert.NotNil(t, err)
			assert.Equal(t, "itemNotFound: not found", err.Error())
		} else {
			assert.Nil(t, err)
			assert.Equal(t, "Math", class.GetDisplayName())
			assert.Equal(t, "abcd-defg", class.GetId())
			assert.Equal(t, "Math0123", class.GetExternalId())
		}
	}
}

func TestDeleteEducationClass(t *testing.T) {
	tests := []struct {
		name                 string
		id                   string
		filter               string
		expectedItemNotFound bool
	}{
		{
			name:                 "Test search class using id",
			id:                   "abcd-defg",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=abcd-defg)(qsferaEducationExternalId=abcd-defg)))",
			expectedItemNotFound: false,
		},
		{
			name:                 "Test search class using unknown Id",
			id:                   "xxxx-xxxx",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=xxxx-xxxx)(qsferaEducationExternalId=xxxx-xxxx)))",
			expectedItemNotFound: true,
		},
		{
			name:                 "Test search class using external ID",
			id:                   "Math0123",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=Math0123)(qsferaEducationExternalId=Math0123)))",
			expectedItemNotFound: false,
		},
		{
			name:                 "Test search school using unknown externalID",
			id:                   "Unknown3210",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=Unknown3210)(qsferaEducationExternalId=Unknown3210)))",
			expectedItemNotFound: true,
		},
	}

	for _, tt := range tests {
		lm := &mocks.Client{}
		sr := &ldap.SearchRequest{
			BaseDN:     "ou=groups,dc=test",
			Scope:      2,
			SizeLimit:  1,
			Filter:     tt.filter,
			Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember"},
			Controls:   []ldap.Control(nil),
		}
		if tt.expectedItemNotFound {
			lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
		} else {
			lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntry}}, nil)
		}
		dr := &ldap.DelRequest{
			DN: "qsferaEducationExternalId=Math0123",
		}
		lm.On("Del", dr).Return(nil)

		b, err := getMockedBackend(lm, eduConfig, &logger)
		assert.Nil(t, err)

		err = b.DeleteEducationClass(context.Background(), tt.id)
		lm.AssertNumberOfCalls(t, "Search", 1)

		if tt.expectedItemNotFound {
			lm.AssertNumberOfCalls(t, "Del", 0)
			assert.NotNil(t, err)
			assert.Equal(t, "itemNotFound: not found", err.Error())
		} else {
			assert.Nil(t, err)
		}
	}
}

func TestGetEducationClassMembers(t *testing.T) {
	tests := []struct {
		name                 string
		id                   string
		filter               string
		expectedItemNotFound bool
	}{
		{
			name:                 "Test search class using id",
			id:                   "abcd-defg",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=abcd-defg)(qsferaEducationExternalId=abcd-defg)))",
			expectedItemNotFound: false,
		},
		{
			name:                 "Test search class using unknown Id",
			id:                   "xxxx-xxxx",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=xxxx-xxxx)(qsferaEducationExternalId=xxxx-xxxx)))",
			expectedItemNotFound: true,
		},
		{
			name:                 "Test search class using external ID",
			id:                   "Math0123",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=Math0123)(qsferaEducationExternalId=Math0123)))",
			expectedItemNotFound: false,
		},
		{
			name:                 "Test search school using unknown externalID",
			id:                   "Unknown3210",
			filter:               "(&(objectClass=qsferaEducationClass)(|(entryUUID=Unknown3210)(qsferaEducationExternalId=Unknown3210)))",
			expectedItemNotFound: true,
		},
	}

	for _, tt := range tests {
		lm := &mocks.Client{}
		userSr := &ldap.SearchRequest{
			BaseDN:     "uid=user",
			Scope:      0,
			SizeLimit:  1,
			Filter:     "(objectClass=inetOrgPerson)",
			Attributes: ldapUserAttributes,
			Controls:   []ldap.Control(nil),
		}
		lm.On("Search", userSr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}}, nil)
		sr := &ldap.SearchRequest{
			BaseDN:     "ou=groups,dc=test",
			Scope:      2,
			SizeLimit:  1,
			Filter:     tt.filter,
			Attributes: []string{"cn", "entryUUID", "qsferaEducationClassType", "qsferaEducationExternalId", "qsferaMemberOfSchool", "qsferaEducationTeacherMember", "member"},
			Controls:   []ldap.Control(nil),
		}
		if tt.expectedItemNotFound {
			lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{}}, nil)
		} else {
			lm.On("Search", sr).Return(&ldap.SearchResult{Entries: []*ldap.Entry{classEntryWithMember}}, nil)
		}

		b, err := getMockedBackend(lm, eduConfig, &logger)
		assert.Nil(t, err)

		users, err := b.GetEducationClassMembers(context.Background(), tt.id)

		if tt.expectedItemNotFound {
			lm.AssertNumberOfCalls(t, "Search", 1)
			assert.NotNil(t, err)
			assert.Equal(t, "itemNotFound: not found", err.Error())
		} else {
			lm.AssertNumberOfCalls(t, "Search", 2)
			assert.Nil(t, err)
			assert.Equal(t, len(users), 1)
		}
	}
}

func TestLDAP_UpdateEducationClass(t *testing.T) {
	externalIDs := []string{"Math3210"}
	changeString := "xxxx-xxxx"
	type args struct {
		id    string
		class libregraph.EducationClass
	}
	type modifyData struct {
		arg *ldap.ModifyRequest
		ret error
	}
	type modifyDNData struct {
		arg *ldap.ModifyDNRequest
		ret error
	}
	type searchData struct {
		res *ldap.SearchResult
		err error
	}
	tests := []struct {
		name         string
		args         args
		modifyDNData modifyDNData
		modifyData   modifyData
		searchData   searchData
		assertion    func(assert.TestingT, error, ...any) bool
	}{
		{
			name: "Change name",
			args: args{
				id: "abcd-defg",
				class: libregraph.EducationClass{
					DisplayName: "Math-2",
				},
			},
			assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Nil(tt, err) },
			modifyData: modifyData{
				arg: &ldap.ModifyRequest{
					DN: "qsferaEducationExternalId=Math0123",
					Changes: []ldap.Change{
						{
							Operation: ldap.ReplaceAttribute,
							Modification: ldap.PartialAttribute{
								Type: "cn",
								Vals: []string{"Math-2"},
							},
						},
					},
				},
			},
			modifyDNData: modifyDNData{
				arg: &ldap.ModifyDNRequest{},
				ret: nil,
			},
			searchData: searchData{
				res: &ldap.SearchResult{
					Entries: []*ldap.Entry{classEntry},
				},
				err: nil,
			},
		},
		{
			name: "Change external ID",
			args: args{
				id: "abcd-defg",
				class: libregraph.EducationClass{
					ExternalId: &externalIDs[0],
				},
			},
			assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Nil(tt, err) },
			modifyData: modifyData{
				arg: &ldap.ModifyRequest{},
			},
			modifyDNData: modifyDNData{
				arg: &ldap.ModifyDNRequest{
					DN:           "qsferaEducationExternalId=Math0123",
					NewRDN:       "qsferaEducationExternalId=Math3210",
					DeleteOldRDN: true,
					NewSuperior:  "",
				},
				ret: nil,
			},
			searchData: searchData{
				res: &ldap.SearchResult{
					Entries: []*ldap.Entry{classEntry},
				},
				err: nil,
			},
		},
		{
			name: "Change both name and external ID",
			args: args{
				id: "abcd-defg",
				class: libregraph.EducationClass{
					DisplayName: "Math-2",
					ExternalId:  &externalIDs[0],
				},
			},
			assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Nil(tt, err) },
			modifyData: modifyData{
				arg: &ldap.ModifyRequest{
					DN: "qsferaEducationExternalId=Math3210,ou=groups,dc=test",
					Changes: []ldap.Change{
						{
							Operation: ldap.ReplaceAttribute,
							Modification: ldap.PartialAttribute{
								Type: "cn",
								Vals: []string{"Math-2"},
							},
						},
					},
				},
			},
			modifyDNData: modifyDNData{
				arg: &ldap.ModifyDNRequest{
					DN:           "qsferaEducationExternalId=Math0123",
					NewRDN:       "qsferaEducationExternalId=Math3210",
					DeleteOldRDN: true,
					NewSuperior:  "",
				},
				ret: nil,
			},
			searchData: searchData{
				res: &ldap.SearchResult{
					Entries: []*ldap.Entry{classEntry},
				},
				err: nil,
			},
		},
		{
			name: "Check error: attempt at changing ID",
			args: args{
				id: "abcd-defg",
				class: libregraph.EducationClass{
					Id: &changeString,
				},
			},
			assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
			modifyData: modifyData{
				arg: &ldap.ModifyRequest{},
			},
			modifyDNData: modifyDNData{
				arg: &ldap.ModifyDNRequest{},
				ret: nil,
			},
			searchData: searchData{
				res: &ldap.SearchResult{
					Entries: []*ldap.Entry{classEntry},
				},
				err: nil,
			},
		},
		{
			name: "Check error: attempt at changing description",
			args: args{
				id: "abcd-defg",
				class: libregraph.EducationClass{
					Description: &changeString,
				},
			},
			assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
			modifyData: modifyData{
				arg: &ldap.ModifyRequest{},
			},
			modifyDNData: modifyDNData{
				arg: &ldap.ModifyDNRequest{},
				ret: nil,
			},
			searchData: searchData{
				res: &ldap.SearchResult{
					Entries: []*ldap.Entry{classEntry},
				},
				err: nil,
			},
		},
		{
			name: "Check error: attempt at changing classification",
			args: args{
				id: "abcd-defg",
				class: libregraph.EducationClass{
					Classification: changeString,
				},
			},
			assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
			modifyData: modifyData{
				arg: &ldap.ModifyRequest{},
			},
			modifyDNData: modifyDNData{
				arg: &ldap.ModifyDNRequest{},
				ret: nil,
			},
			searchData: searchData{
				res: &ldap.SearchResult{
					Entries: []*ldap.Entry{classEntry},
				},
				err: nil,
			},
		},
		{
			name: "Check error: attempt at changing members",
			args: args{
				id: "abcd-defg",
				class: libregraph.EducationClass{
					Members: []libregraph.User{*libregraph.NewUser("display name", "username")},
				},
			},
			assertion: func(tt assert.TestingT, err error, i ...any) bool { return assert.Error(tt, err) },
			modifyData: modifyData{
				arg: &ldap.ModifyRequest{},
			},
			modifyDNData: modifyDNData{
				arg: &ldap.ModifyDNRequest{},
				ret: nil,
			},
			searchData: searchData{
				res: &ldap.SearchResult{
					Entries: []*ldap.Entry{classEntry},
				},
				err: nil,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			lm := &mocks.Client{}
			b, err := getMockedBackend(lm, eduConfig, &logger)
			if err != nil {
				panic(err)
			}

			lm.On("Modify", tt.modifyData.arg).Return(tt.modifyData.ret)
			lm.On("ModifyDN", tt.modifyDNData.arg).Return(tt.modifyDNData.ret)
			lm.On("Search", mock.Anything).Return(tt.searchData.res, tt.searchData.err)

			ctx := context.Background()

			_, err = b.UpdateEducationClass(ctx, tt.args.id, tt.args.class)
			tt.assertion(t, err)
		})
	}
}
