package clients

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/authn"
	"github.com/grafana/grafana/pkg/services/ldap"
	"github.com/grafana/grafana/pkg/services/ldap/multildap"
	"github.com/grafana/grafana/pkg/services/ldap/service"
	"github.com/grafana/grafana/pkg/services/login"
	"github.com/grafana/grafana/pkg/services/login/authinfotest"
	"github.com/grafana/grafana/pkg/services/org"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/services/user/usertest"
	"github.com/grafana/grafana/pkg/setting"
)

type ldapTestCase struct {
	desc             string
	username         string
	password         string
	expectedErr      error
	expectedLDAPErr  error
	expectedLDAPInfo *login.ExternalUserInfo
	expectedIdentity *authn.Identity

	// Disabling User
	expectedUser        user.User
	expectedUserErr     error
	expectedAuthInfo    login.UserAuth
	expectedAuthInfoErr error
	disableCalled       bool
	expectDisable       bool
}

func TestLDAP_AuthenticateProxy(t *testing.T) {
	tests := []ldapTestCase{
		{
			desc:     "should return valid identity when found by ldap service",
			username: "test",
			expectedLDAPInfo: &login.ExternalUserInfo{
				AuthModule: login.LDAPAuthModule,
				AuthId:     "123",
				Email:      "test@test.com",
				Login:      "test",
				Name:       "test test",
				Groups:     []string{"1", "2"},
				OrgRoles:   map[int64]org.RoleType{1: org.RoleViewer},
			},
			expectedIdentity: &authn.Identity{
				OrgID:           1,
				OrgRoles:        map[int64]org.RoleType{1: org.RoleViewer},
				Login:           "test",
				Name:            "test test",
				Email:           "test@test.com",
				AuthenticatedBy: login.LDAPAuthModule,
				AuthID:          "123",
				Groups:          []string{"1", "2"},
				ClientParams: authn.ClientParams{
					SyncUser:        true,
					SyncTeams:       true,
					EnableUser:      true,
					FetchSyncedUser: true,
					SyncOrgRoles:    true,
					SyncPermissions: true,
					LookUpParams: login.UserLookupParams{
						Email: strPtr("test@test.com"),
						Login: strPtr("test"),
					},
				},
			},
		},
		{
			desc:            "should return error when user is not found",
			username:        "test",
			expectedLDAPErr: multildap.ErrDidNotFindUser,
			expectedUserErr: user.ErrUserNotFound,
			expectedErr:     errIdentityNotFound,
		},
		{
			desc:             "should disable user when user is not found",
			username:         "test",
			expectedLDAPErr:  multildap.ErrDidNotFindUser,
			expectedUser:     user.User{ID: 11, Login: "test"},
			expectedAuthInfo: login.UserAuth{UserId: 11, AuthId: "cn=test,ou=users,dc=example,dc=org", AuthModule: login.LDAPAuthModule},
			expectDisable:    true,
			expectedErr:      errIdentityNotFound,
		},
	}

	for i := range tests {
		tt := tests[i]
		t.Run(tt.desc, func(t *testing.T) {
			c := setupLDAPTestCase(&tt)

			identity, err := c.AuthenticateProxy(context.Background(), &authn.Request{OrgID: 1}, tt.username, nil)
			assert.ErrorIs(t, err, tt.expectedErr)
			assert.EqualValues(t, tt.expectedIdentity, identity)
			assert.Equal(t, tt.expectDisable, tt.disableCalled)
		})
	}
}

func TestLDAP_AuthenticatePassword(t *testing.T) {
	tests := []ldapTestCase{
		{
			desc:     "should successfully authenticate with correct username and password",
			username: "test",
			password: "test123",
			expectedLDAPInfo: &login.ExternalUserInfo{
				AuthModule: login.LDAPAuthModule,
				AuthId:     "123",
				Email:      "test@test.com",
				Login:      "test",
				Name:       "test test",
				Groups:     []string{"1", "2"},
				OrgRoles:   map[int64]org.RoleType{1: org.RoleViewer},
			},
			expectedIdentity: &authn.Identity{
				OrgID:           1,
				OrgRoles:        map[int64]org.RoleType{1: org.RoleViewer},
				Login:           "test",
				Name:            "test test",
				Email:           "test@test.com",
				AuthenticatedBy: login.LDAPAuthModule,
				AuthID:          "123",
				Groups:          []string{"1", "2"},
				ClientParams: authn.ClientParams{
					SyncUser:        true,
					SyncTeams:       true,
					EnableUser:      true,
					FetchSyncedUser: true,
					SyncOrgRoles:    true,
					SyncPermissions: true,
					LookUpParams: login.UserLookupParams{
						Email: strPtr("test@test.com"),
						Login: strPtr("test"),
					},
				},
			},
		},
		{
			desc:            "should fail if provided password was incorrect",
			username:        "test",
			password:        "wrong",
			expectedErr:     errInvalidPassword,
			expectedLDAPErr: ldap.ErrInvalidCredentials,
		},
		{
			desc:            "should fail if not found",
			username:        "test",
			password:        "wrong",
			expectedErr:     errIdentityNotFound,
			expectedLDAPErr: ldap.ErrCouldNotFindUser,
			expectedUserErr: user.ErrUserNotFound,
		},
		{
			desc:             "should disable user if not found",
			username:         "test",
			password:         "wrong",
			expectedErr:      errIdentityNotFound,
			expectedLDAPErr:  ldap.ErrCouldNotFindUser,
			expectedUser:     user.User{ID: 11, Login: "test"},
			expectedAuthInfo: login.UserAuth{UserId: 11, AuthId: "cn=test,ou=users,dc=example,dc=org", AuthModule: login.LDAPAuthModule},
			expectDisable:    true,
		},
	}

	for i := range tests {
		tt := tests[i]
		t.Run(tt.desc, func(t *testing.T) {
			c := setupLDAPTestCase(&tt)

			identity, err := c.AuthenticatePassword(context.Background(), &authn.Request{OrgID: 1}, tt.username, tt.password)
			assert.ErrorIs(t, err, tt.expectedErr)
			assert.EqualValues(t, tt.expectedIdentity, identity)
			assert.Equal(t, tt.expectDisable, tt.disableCalled)
		})
	}
}

func setupLDAPTestCase(tt *ldapTestCase) *LDAP {
	userService := &usertest.FakeUserService{
		ExpectedError: tt.expectedUserErr,
		ExpectedUser:  &tt.expectedUser,
		UpdateFn: func(ctx context.Context, cmd *user.UpdateUserCommand) error {
			tt.disableCalled = true
			return nil
		},
	}
	authInfoService := &authinfotest.FakeService{
		ExpectedUserAuth: &tt.expectedAuthInfo,
		ExpectedError:    tt.expectedAuthInfoErr,
	}

	c := &LDAP{
		cfg:             setting.NewCfg(),
		logger:          log.New("authn.ldap.test"),
		service:         &service.LDAPFakeService{ExpectedUser: tt.expectedLDAPInfo, ExpectedError: tt.expectedLDAPErr},
		userService:     userService,
		authInfoService: authInfoService,
	}

	return c
}

func strPtr(s string) *string {
	return &s
}
