package authproxy

import (
	"errors"
	"fmt"
	"net/http"
	"testing"

	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/remotecache"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/ldap"
	"github.com/grafana/grafana/pkg/services/multildap"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/macaron.v1"
)

type fakeMultiLDAP struct {
	multildap.MultiLDAP
	ID          int64
	userCalled  bool
	loginCalled bool
}

func (m *fakeMultiLDAP) Login(query *models.LoginUserQuery) (
	*models.ExternalUserInfo, error,
) {
	m.loginCalled = true
	result := &models.ExternalUserInfo{
		UserId: m.ID,
	}
	return result, nil
}

func (m *fakeMultiLDAP) User(login string) (
	*models.ExternalUserInfo,
	ldap.ServerConfig,
	error,
) {
	m.userCalled = true
	result := &models.ExternalUserInfo{
		UserId: m.ID,
	}
	return result, ldap.ServerConfig{}, nil
}

const hdrName = "markelog"

func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, cb func(*http.Request, *setting.Cfg)) *AuthProxy {
	t.Helper()

	cfg := setting.NewCfg()
	cfg.AuthProxyHeaderName = "X-Killa"

	req, err := http.NewRequest("POST", "http://example.com", nil)
	require.NoError(t, err)
	req.Header.Set(cfg.AuthProxyHeaderName, hdrName)

	if cb != nil {
		cb(req, cfg)
	}

	ctx := &models.ReqContext{
		Context: &macaron.Context{
			Req: macaron.Request{
				Request: req,
			},
		},
	}

	auth := New(cfg, &Options{
		RemoteCache: remoteCache,
		Ctx:         ctx,
		OrgID:       4,
	})

	return auth
}

func TestMiddlewareContext(t *testing.T) {
	logger := log.New("test")
	cache := remotecache.NewFakeStore(t)

	t.Run("When the cache only contains the main header with a simple cache key", func(t *testing.T) {
		const id int64 = 33
		// Set cache key
		h, err := HashCacheKey(hdrName)
		require.NoError(t, err)
		key := fmt.Sprintf(CachePrefix, h)
		err = cache.Set(key, id, 0)
		require.NoError(t, err)
		// Set up the middleware
		auth := prepareMiddleware(t, cache, nil)
		gotKey, err := auth.getKey()
		require.NoError(t, err)
		assert.Equal(t, key, gotKey)

		gotID, err := auth.Login(logger, false)
		require.NoError(t, err)

		assert.Equal(t, id, gotID)
	})

	t.Run("When the cache key contains additional headers", func(t *testing.T) {
		const id int64 = 33
		const group = "grafana-core-team"

		h, err := HashCacheKey(hdrName + "-" + group)
		require.NoError(t, err)
		key := fmt.Sprintf(CachePrefix, h)
		err = cache.Set(key, id, 0)
		require.NoError(t, err)

		auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
			req.Header.Set("X-WEBAUTH-GROUPS", group)
			cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"}
		})
		assert.Equal(t, "auth-proxy-sync-ttl:14f69b7023baa0ac98c96b31cec07bc0", key)

		gotID, err := auth.Login(logger, false)
		require.NoError(t, err)
		assert.Equal(t, id, gotID)
	})
}

func TestMiddlewareContext_ldap(t *testing.T) {
	logger := log.New("test")

	t.Run("Logs in via LDAP", func(t *testing.T) {
		const id int64 = 42

		bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error {
			cmd.Result = &models.User{
				Id: id,
			}

			return nil
		})

		origIsLDAPEnabled := isLDAPEnabled
		origGetLDAPConfig := getLDAPConfig
		origNewLDAP := newLDAP
		t.Cleanup(func() {
			newLDAP = origNewLDAP
			isLDAPEnabled = origIsLDAPEnabled
			getLDAPConfig = origGetLDAPConfig
		})

		isLDAPEnabled = func(*setting.Cfg) bool {
			return true
		}

		stub := &fakeMultiLDAP{
			ID: id,
		}

		getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
			config := &ldap.Config{
				Servers: []*ldap.ServerConfig{
					{
						SearchBaseDNs: []string{"BaseDNHere"},
					},
				},
			}
			return config, nil
		}

		newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
			return stub
		}

		cache := remotecache.NewFakeStore(t)

		auth := prepareMiddleware(t, cache, nil)

		gotID, err := auth.Login(logger, false)
		require.NoError(t, err)

		assert.Equal(t, id, gotID)
		assert.True(t, stub.userCalled)
	})

	t.Run("Gets nice error if LDAP is enabled, but not configured", func(t *testing.T) {
		const id int64 = 42
		origIsLDAPEnabled := isLDAPEnabled
		origNewLDAP := newLDAP
		origGetLDAPConfig := getLDAPConfig
		t.Cleanup(func() {
			isLDAPEnabled = origIsLDAPEnabled
			newLDAP = origNewLDAP
			getLDAPConfig = origGetLDAPConfig
		})

		isLDAPEnabled = func(*setting.Cfg) bool {
			return true
		}

		getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) {
			return nil, errors.New("something went wrong")
		}

		cache := remotecache.NewFakeStore(t)

		auth := prepareMiddleware(t, cache, nil)

		stub := &fakeMultiLDAP{
			ID: id,
		}

		newLDAP = func(servers []*ldap.ServerConfig) multildap.IMultiLDAP {
			return stub
		}

		gotID, err := auth.Login(logger, false)
		require.EqualError(t, err, "failed to get the user")

		assert.NotEqual(t, id, gotID)
		assert.False(t, stub.loginCalled)
	})
}