package login

import (
	"testing"

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

func TestGrafanaLogin(t *testing.T) {
	Convey("Login using Grafana DB", t, func() {
		grafanaLoginScenario("When login with non-existing user", func(sc *grafanaLoginScenarioContext) {
			sc.withNonExistingUser()
			err := loginUsingGrafanaDB(sc.loginUserQuery)

			Convey("it should result in user not found error", func() {
				So(err, ShouldEqual, m.ErrUserNotFound)
			})

			Convey("it should not call password validation", func() {
				So(sc.validatePasswordCalled, ShouldBeFalse)
			})

			Convey("it should not pupulate user object", func() {
				So(sc.loginUserQuery.User, ShouldBeNil)
			})
		})

		grafanaLoginScenario("When login with invalid credentials", func(sc *grafanaLoginScenarioContext) {
			sc.withInvalidPassword()
			err := loginUsingGrafanaDB(sc.loginUserQuery)

			Convey("it should result in invalid credentials error", func() {
				So(err, ShouldEqual, ErrInvalidCredentials)
			})

			Convey("it should call password validation", func() {
				So(sc.validatePasswordCalled, ShouldBeTrue)
			})

			Convey("it should not pupulate user object", func() {
				So(sc.loginUserQuery.User, ShouldBeNil)
			})
		})

		grafanaLoginScenario("When login with valid credentials", func(sc *grafanaLoginScenarioContext) {
			sc.withValidCredentials()
			err := loginUsingGrafanaDB(sc.loginUserQuery)

			Convey("it should not result in error", func() {
				So(err, ShouldBeNil)
			})

			Convey("it should call password validation", func() {
				So(sc.validatePasswordCalled, ShouldBeTrue)
			})

			Convey("it should pupulate user object", func() {
				So(sc.loginUserQuery.User, ShouldNotBeNil)
				So(sc.loginUserQuery.User.Login, ShouldEqual, sc.loginUserQuery.Username)
				So(sc.loginUserQuery.User.Password, ShouldEqual, sc.loginUserQuery.Password)
			})
		})
	})
}

type grafanaLoginScenarioContext struct {
	loginUserQuery         *m.LoginUserQuery
	validatePasswordCalled bool
}

type grafanaLoginScenarioFunc func(c *grafanaLoginScenarioContext)

func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) {
	Convey(desc, func() {
		origValidatePassword := validatePassword

		sc := &grafanaLoginScenarioContext{
			loginUserQuery: &m.LoginUserQuery{
				Username:  "user",
				Password:  "pwd",
				IpAddress: "192.168.1.1:56433",
			},
			validatePasswordCalled: false,
		}

		defer func() {
			validatePassword = origValidatePassword
		}()

		fn(sc)
	})
}

func mockPasswordValidation(valid bool, sc *grafanaLoginScenarioContext) {
	validatePassword = func(providedPassword string, userPassword string, userSalt string) error {
		sc.validatePasswordCalled = true

		if !valid {
			return ErrInvalidCredentials
		}

		return nil
	}
}

func (sc *grafanaLoginScenarioContext) getUserByLoginQueryReturns(user *m.User) {
	bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
		if user == nil {
			return m.ErrUserNotFound
		}

		query.Result = user
		return nil
	})
}

func (sc *grafanaLoginScenarioContext) withValidCredentials() {
	sc.getUserByLoginQueryReturns(&m.User{
		Id:       1,
		Login:    sc.loginUserQuery.Username,
		Password: sc.loginUserQuery.Password,
		Salt:     "salt",
	})
	mockPasswordValidation(true, sc)
}

func (sc *grafanaLoginScenarioContext) withNonExistingUser() {
	sc.getUserByLoginQueryReturns(nil)
}

func (sc *grafanaLoginScenarioContext) withInvalidPassword() {
	sc.getUserByLoginQueryReturns(&m.User{
		Password: sc.loginUserQuery.Password,
		Salt:     "salt",
	})
	mockPasswordValidation(false, sc)
}
