package login

import (
	"errors"
	"testing"

	m "github.com/grafana/grafana/pkg/models"
	. "github.com/smartystreets/goconvey/convey"
)

func TestAuthenticateUser(t *testing.T) {
	Convey("Authenticate user", t, func() {
		authScenario("When a user authenticates having too many login attempts", func(sc *authScenarioContext) {
			mockLoginAttemptValidation(ErrTooManyLoginAttempts, sc)
			mockLoginUsingGrafanaDB(nil, sc)
			mockLoginUsingLdap(true, nil, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldEqual, ErrTooManyLoginAttempts)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeFalse)
				So(sc.ldapLoginWasCalled, ShouldBeFalse)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
			})
		})

		authScenario("When grafana user authenticate with valid credentials", func(sc *authScenarioContext) {
			mockLoginAttemptValidation(nil, sc)
			mockLoginUsingGrafanaDB(nil, sc)
			mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldEqual, nil)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
				So(sc.ldapLoginWasCalled, ShouldBeFalse)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
			})
		})

		authScenario("When grafana user authenticate and unexpected error occurs", func(sc *authScenarioContext) {
			customErr := errors.New("custom")
			mockLoginAttemptValidation(nil, sc)
			mockLoginUsingGrafanaDB(customErr, sc)
			mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldEqual, customErr)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
				So(sc.ldapLoginWasCalled, ShouldBeFalse)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
			})
		})

		authScenario("When a non-existing grafana user authenticate and ldap disabled", func(sc *authScenarioContext) {
			mockLoginAttemptValidation(nil, sc)
			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
			mockLoginUsingLdap(false, nil, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldEqual, ErrInvalidCredentials)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
				So(sc.ldapLoginWasCalled, ShouldBeTrue)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
			})
		})

		authScenario("When a non-existing grafana user authenticate and invalid ldap credentials", func(sc *authScenarioContext) {
			mockLoginAttemptValidation(nil, sc)
			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
			mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldEqual, ErrInvalidCredentials)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
				So(sc.ldapLoginWasCalled, ShouldBeTrue)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeTrue)
			})
		})

		authScenario("When a non-existing grafana user authenticate and valid ldap credentials", func(sc *authScenarioContext) {
			mockLoginAttemptValidation(nil, sc)
			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
			mockLoginUsingLdap(true, nil, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldBeNil)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
				So(sc.ldapLoginWasCalled, ShouldBeTrue)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
			})
		})

		authScenario("When a non-existing grafana user authenticate and ldap returns unexpected error", func(sc *authScenarioContext) {
			customErr := errors.New("custom")
			mockLoginAttemptValidation(nil, sc)
			mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc)
			mockLoginUsingLdap(true, customErr, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldEqual, customErr)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
				So(sc.ldapLoginWasCalled, ShouldBeTrue)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeFalse)
			})
		})

		authScenario("When grafana user authenticate with invalid credentials and invalid ldap credentials", func(sc *authScenarioContext) {
			mockLoginAttemptValidation(nil, sc)
			mockLoginUsingGrafanaDB(ErrInvalidCredentials, sc)
			mockLoginUsingLdap(true, ErrInvalidCredentials, sc)
			mockSaveInvalidLoginAttempt(sc)

			err := AuthenticateUser(sc.loginUserQuery)

			Convey("it should result in", func() {
				So(err, ShouldEqual, ErrInvalidCredentials)
				So(sc.loginAttemptValidationWasCalled, ShouldBeTrue)
				So(sc.grafanaLoginWasCalled, ShouldBeTrue)
				So(sc.ldapLoginWasCalled, ShouldBeTrue)
				So(sc.saveInvalidLoginAttemptWasCalled, ShouldBeTrue)
			})
		})
	})
}

type authScenarioContext struct {
	loginUserQuery                   *LoginUserQuery
	grafanaLoginWasCalled            bool
	ldapLoginWasCalled               bool
	loginAttemptValidationWasCalled  bool
	saveInvalidLoginAttemptWasCalled bool
}

type authScenarioFunc func(sc *authScenarioContext)

func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
	loginUsingGrafanaDB = func(query *LoginUserQuery) error {
		sc.grafanaLoginWasCalled = true
		return err
	}
}

func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
	loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
		sc.ldapLoginWasCalled = true
		return enabled, err
	}
}

func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
	validateLoginAttempts = func(username string) error {
		sc.loginAttemptValidationWasCalled = true
		return err
	}
}

func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
	saveInvalidLoginAttempt = func(query *LoginUserQuery) {
		sc.saveInvalidLoginAttemptWasCalled = true
	}
}

func authScenario(desc string, fn authScenarioFunc) {
	Convey(desc, func() {
		origLoginUsingGrafanaDB := loginUsingGrafanaDB
		origLoginUsingLdap := loginUsingLdap
		origValidateLoginAttempts := validateLoginAttempts
		origSaveInvalidLoginAttempt := saveInvalidLoginAttempt

		sc := &authScenarioContext{
			loginUserQuery: &LoginUserQuery{
				Username:  "user",
				Password:  "pwd",
				IpAddress: "192.168.1.1:56433",
			},
		}

		defer func() {
			loginUsingGrafanaDB = origLoginUsingGrafanaDB
			loginUsingLdap = origLoginUsingLdap
			validateLoginAttempts = origValidateLoginAttempts
			saveInvalidLoginAttempt = origSaveInvalidLoginAttempt
		}()

		fn(sc)
	})
}
