package ldap

import (
	"context"
	"testing"

	. "github.com/smartystreets/goconvey/convey"
	"gopkg.in/ldap.v3"

	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/log"
	m "github.com/grafana/grafana/pkg/models"
)

func TestAuth(t *testing.T) {
	Convey("initialBind", t, func() {
		Convey("Given bind dn and password configured", func() {
			conn := &mockLdapConn{}
			var actualUsername, actualPassword string
			conn.bindProvider = func(username, password string) error {
				actualUsername = username
				actualPassword = password
				return nil
			}
			Auth := &Auth{
				conn: conn,
				server: &ServerConfig{
					BindDN:       "cn=%s,o=users,dc=grafana,dc=org",
					BindPassword: "bindpwd",
				},
			}
			err := Auth.initialBind("user", "pwd")
			So(err, ShouldBeNil)
			So(Auth.requireSecondBind, ShouldBeTrue)
			So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
			So(actualPassword, ShouldEqual, "bindpwd")
		})

		Convey("Given bind dn configured", func() {
			conn := &mockLdapConn{}
			var actualUsername, actualPassword string
			conn.bindProvider = func(username, password string) error {
				actualUsername = username
				actualPassword = password
				return nil
			}
			Auth := &Auth{
				conn: conn,
				server: &ServerConfig{
					BindDN: "cn=%s,o=users,dc=grafana,dc=org",
				},
			}
			err := Auth.initialBind("user", "pwd")
			So(err, ShouldBeNil)
			So(Auth.requireSecondBind, ShouldBeFalse)
			So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
			So(actualPassword, ShouldEqual, "pwd")
		})

		Convey("Given empty bind dn and password", func() {
			conn := &mockLdapConn{}
			unauthenticatedBindWasCalled := false
			var actualUsername string
			conn.unauthenticatedBindProvider = func(username string) error {
				unauthenticatedBindWasCalled = true
				actualUsername = username
				return nil
			}
			Auth := &Auth{
				conn:   conn,
				server: &ServerConfig{},
			}
			err := Auth.initialBind("user", "pwd")
			So(err, ShouldBeNil)
			So(Auth.requireSecondBind, ShouldBeTrue)
			So(unauthenticatedBindWasCalled, ShouldBeTrue)
			So(actualUsername, ShouldBeEmpty)
		})
	})

	Convey("serverBind", t, func() {
		Convey("Given bind dn and password configured", func() {
			conn := &mockLdapConn{}
			var actualUsername, actualPassword string
			conn.bindProvider = func(username, password string) error {
				actualUsername = username
				actualPassword = password
				return nil
			}
			Auth := &Auth{
				conn: conn,
				server: &ServerConfig{
					BindDN:       "o=users,dc=grafana,dc=org",
					BindPassword: "bindpwd",
				},
			}
			err := Auth.serverBind()
			So(err, ShouldBeNil)
			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
			So(actualPassword, ShouldEqual, "bindpwd")
		})

		Convey("Given bind dn configured", func() {
			conn := &mockLdapConn{}
			unauthenticatedBindWasCalled := false
			var actualUsername string
			conn.unauthenticatedBindProvider = func(username string) error {
				unauthenticatedBindWasCalled = true
				actualUsername = username
				return nil
			}
			Auth := &Auth{
				conn: conn,
				server: &ServerConfig{
					BindDN: "o=users,dc=grafana,dc=org",
				},
			}
			err := Auth.serverBind()
			So(err, ShouldBeNil)
			So(unauthenticatedBindWasCalled, ShouldBeTrue)
			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
		})

		Convey("Given empty bind dn and password", func() {
			conn := &mockLdapConn{}
			unauthenticatedBindWasCalled := false
			var actualUsername string
			conn.unauthenticatedBindProvider = func(username string) error {
				unauthenticatedBindWasCalled = true
				actualUsername = username
				return nil
			}
			Auth := &Auth{
				conn:   conn,
				server: &ServerConfig{},
			}
			err := Auth.serverBind()
			So(err, ShouldBeNil)
			So(unauthenticatedBindWasCalled, ShouldBeTrue)
			So(actualUsername, ShouldBeEmpty)
		})
	})

	Convey("When translating ldap user to grafana user", t, func() {

		var user1 = &m.User{}

		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpsertUserCommand) error {
			cmd.Result = user1
			cmd.Result.Login = "torkelo"
			return nil
		})

		Convey("Given no ldap group map match", func() {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{{}},
			})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})

			So(err, ShouldEqual, ErrInvalidCredentials)
		})

		AuthScenario("Given wildcard group match", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "*", OrgRole: "Admin"},
				},
			})

			sc.userQueryReturns(user1)

			result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{})
			So(err, ShouldBeNil)
			So(result, ShouldEqual, user1)
		})

		AuthScenario("Given exact group match", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=users", OrgRole: "Admin"},
				},
			})

			sc.userQueryReturns(user1)

			result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"cn=users"}})
			So(err, ShouldBeNil)
			So(result, ShouldEqual, user1)
		})

		AuthScenario("Given group match with different case", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=users", OrgRole: "Admin"},
				},
			})

			sc.userQueryReturns(user1)

			result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"CN=users"}})
			So(err, ShouldBeNil)
			So(result, ShouldEqual, user1)
		})

		AuthScenario("Given no existing grafana user", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=admin", OrgRole: "Admin"},
					{GroupDN: "cn=editor", OrgRole: "Editor"},
					{GroupDN: "*", OrgRole: "Viewer"},
				},
			})

			sc.userQueryReturns(nil)

			result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				DN:       "torkelo",
				Username: "torkelo",
				Email:    "my@email.com",
				MemberOf: []string{"cn=editor"},
			})

			So(err, ShouldBeNil)

			Convey("Should return new user", func() {
				So(result.Login, ShouldEqual, "torkelo")
			})

			Convey("Should set isGrafanaAdmin to false by default", func() {
				So(result.IsAdmin, ShouldBeFalse)
			})

		})

	})

	Convey("When syncing ldap groups to grafana org roles", t, func() {
		AuthScenario("given no current user orgs", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=users", OrgRole: "Admin"},
				},
			})

			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				MemberOf: []string{"cn=users"},
			})

			Convey("Should create new org user", func() {
				So(err, ShouldBeNil)
				So(sc.addOrgUserCmd, ShouldNotBeNil)
				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
			})
		})

		AuthScenario("given different current org role", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
				},
			})

			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				MemberOf: []string{"cn=users"},
			})

			Convey("Should update org role", func() {
				So(err, ShouldBeNil)
				So(sc.updateOrgUserCmd, ShouldNotBeNil)
				So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
			})
		})

		AuthScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
				},
			})

			sc.userOrgsQueryReturns([]*m.UserOrgDTO{
				{OrgId: 1, Role: m.ROLE_EDITOR},
				{OrgId: 2, Role: m.ROLE_EDITOR},
			})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				MemberOf: []string{"cn=users"},
			})

			Convey("Should remove org role", func() {
				So(err, ShouldBeNil)
				So(sc.removeOrgUserCmd, ShouldNotBeNil)
				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
			})
		})

		AuthScenario("given org role is updated in config", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=admin", OrgId: 1, OrgRole: "Admin"},
					{GroupDN: "cn=users", OrgId: 1, OrgRole: "Viewer"},
				},
			})

			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				MemberOf: []string{"cn=users"},
			})

			Convey("Should update org role", func() {
				So(err, ShouldBeNil)
				So(sc.removeOrgUserCmd, ShouldBeNil)
				So(sc.updateOrgUserCmd, ShouldNotBeNil)
				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
			})
		})

		AuthScenario("given multiple matching ldap groups", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
					{GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
				},
			})

			sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				MemberOf: []string{"cn=admins"},
			})

			Convey("Should take first match, and ignore subsequent matches", func() {
				So(err, ShouldBeNil)
				So(sc.updateOrgUserCmd, ShouldBeNil)
				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
			})
		})

		AuthScenario("given multiple matching ldap groups and no existing groups", func(sc *scenarioContext) {
			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"},
					{GroupDN: "*", OrgId: 1, OrgRole: "Viewer"},
				},
			})

			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				MemberOf: []string{"cn=admins"},
			})

			Convey("Should take first match, and ignore subsequent matches", func() {
				So(err, ShouldBeNil)
				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
			})

			Convey("Should not update permissions unless specified", func() {
				So(err, ShouldBeNil)
				So(sc.updateUserPermissionsCmd, ShouldBeNil)
			})
		})

		AuthScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
			trueVal := true

			Auth := New(&ServerConfig{
				Groups: []*GroupToOrgRole{
					{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
				},
			})

			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
			_, err := Auth.GetGrafanaUserFor(nil, &UserInfo{
				MemberOf: []string{"cn=admins"},
			})

			Convey("Should create user with admin set to true", func() {
				So(err, ShouldBeNil)
				So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
			})
		})
	})

	Convey("When calling SyncUser", t, func() {
		mockLdapConnection := &mockLdapConn{}

		auth := &Auth{
			server: &ServerConfig{
				Host:       "",
				RootCACert: "",
				Groups: []*GroupToOrgRole{
					{GroupDN: "*", OrgRole: "Admin"},
				},
				Attr: AttributeMap{
					Username: "username",
					Surname:  "surname",
					Email:    "email",
					Name:     "name",
					MemberOf: "memberof",
				},
				SearchBaseDNs: []string{"BaseDNHere"},
			},
			conn: mockLdapConnection,
			log:  log.New("test-logger"),
		}

		dialCalled := false
		dial = func(network, addr string) (IConnection, error) {
			dialCalled = true
			return mockLdapConnection, nil
		}

		entry := ldap.Entry{
			DN: "dn", Attributes: []*ldap.EntryAttribute{
				{Name: "username", Values: []string{"roelgerrits"}},
				{Name: "surname", Values: []string{"Gerrits"}},
				{Name: "email", Values: []string{"roel@test.com"}},
				{Name: "name", Values: []string{"Roel"}},
				{Name: "memberof", Values: []string{"admins"}},
			}}
		result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
		mockLdapConnection.setSearchResult(&result)

		AuthScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
			// arrange
			query := &m.LoginUserQuery{
				Username: "roelgerrits",
			}

			hookDial = nil

			sc.userQueryReturns(&m.User{
				Id:    1,
				Email: "roel@test.net",
				Name:  "Roel Gerrits",
				Login: "roelgerrits",
			})
			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})

			// act
			syncErrResult := auth.SyncUser(query)

			// assert
			So(dialCalled, ShouldBeTrue)
			So(syncErrResult, ShouldBeNil)
			// User should be searched in ldap
			So(mockLdapConnection.searchCalled, ShouldBeTrue)
			// Info should be updated (email differs)
			So(sc.updateUserCmd.Email, ShouldEqual, "roel@test.com")
			// User should have admin privileges
			So(sc.addOrgUserCmd.UserId, ShouldEqual, 1)
			So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
		})
	})

	Convey("When searching for a user and not all five attributes are mapped", t, func() {
		mockLdapConnection := &mockLdapConn{}
		entry := ldap.Entry{
			DN: "dn", Attributes: []*ldap.EntryAttribute{
				{Name: "username", Values: []string{"roelgerrits"}},
				{Name: "surname", Values: []string{"Gerrits"}},
				{Name: "email", Values: []string{"roel@test.com"}},
				{Name: "name", Values: []string{"Roel"}},
				{Name: "memberof", Values: []string{"admins"}},
			}}
		result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
		mockLdapConnection.setSearchResult(&result)

		// Set up attribute map without surname and email
		Auth := &Auth{
			server: &ServerConfig{
				Attr: AttributeMap{
					Username: "username",
					Name:     "name",
					MemberOf: "memberof",
				},
				SearchBaseDNs: []string{"BaseDNHere"},
			},
			conn: mockLdapConnection,
			log:  log.New("test-logger"),
		}

		searchResult, err := Auth.searchForUser("roelgerrits")

		So(err, ShouldBeNil)
		So(searchResult, ShouldNotBeNil)

		// User should be searched in ldap
		So(mockLdapConnection.searchCalled, ShouldBeTrue)

		// No empty attributes should be added to the search request
		So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3)
	})
}
