package httphandlers_test

import (
	"errors"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/adminroles"
	"github.com/fiskerinc/cloud-services/pkg/cache"
	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/common/authproviders"
	c "github.com/fiskerinc/cloud-services/pkg/common/context"
	"github.com/fiskerinc/cloud-services/pkg/db/queries"
	"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
	"github.com/fiskerinc/cloud-services/pkg/httphandlers"
	"github.com/fiskerinc/cloud-services/pkg/redis"
	"github.com/fiskerinc/cloud-services/pkg/testhelper"
)

type testCaseAuthAPIToken struct {
	RedisClient      redis.Client
	Query            queries.APITokensInterface
	APICalls         queries.APICallsInterface
	RequiredRoles    map[string][]adminroles.RoleID
	JWTAuth          bool
	ExpectedProvider string
	testhelper.BasicHttpTest
}

func TestAuthAPIToken(t *testing.T) {
	adminroles.RoleCreate = adminroles.RoleID("efcc3025-e2d8-4212-8227-805c7be39d2c")
	adminroles.RoleDelete = adminroles.RoleID("8f78dce7-f5f9-4033-a10c-c9c7408bfcfe")

	someErr := errors.New("some err")
	validExpiresAt := time.Now().Add(time.Hour)
	client := &RedisMockAuthAPIToken{
		GetCacheError: redis.ErrNilObject,
	}
	db := &mocks.MockAPITokens{
		DBMockHelper: mocks.DBMockHelper{
			Error: errors.New("token not found"),
		},
	}
	dbCalls := &mocks.MockAPICalls{
		DBMockHelper: mocks.DBMockHelper{Error: nil},
	}
	apiToken := "XXXXXXXXXXXX"
	req := testhelper.MakeTestRequestWithHeaders(http.MethodGet, "/", map[string]string{cache.ApiKeyHeader: apiToken}, nil)
	roles := map[string][]adminroles.RoleID{
		authproviders.Default: {adminroles.RoleCreate},
	}

	tests := []testCaseAuthAPIToken{
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "Good API Token, no required permission",
				Request:          req,
				ExpectedStatus:   http.StatusOK,
				ExpectedResponse: `OK`,
			},
			RequiredRoles: nil,
			RedisClient:   client,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "Good API Token",
				Request:          req,
				ExpectedStatus:   http.StatusOK,
				ExpectedResponse: `OK`,
			},
			RequiredRoles: roles,
			RedisClient:   client,
			Query: &mocks.MockAPITokens{
				GetResult: &common.APIToken{
					Token: apiToken,
					Roles: strings.Join([]string{string(adminroles.RoleCreate)}, ","),
				},
			},
			APICalls: dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "Good API Token with expiration",
				Request:          req,
				ExpectedStatus:   http.StatusOK,
				ExpectedResponse: `OK`,
			},
			RequiredRoles: roles,
			RedisClient:   client,
			Query: &mocks.MockAPITokens{
				GetResult: &common.APIToken{
					Token:     apiToken,
					Roles:     strings.Join([]string{string(adminroles.RoleCreate)}, ","),
					ExpiresAt: &validExpiresAt,
				},
			},
			APICalls: dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "Good API Token, without permission",
				Request:          req,
				ExpectedStatus:   http.StatusUnauthorized,
				ExpectedResponse: `{"message":"missing permission","error":"Unauthorized"}`,
			},
			RequiredRoles: roles,
			RedisClient:   client,
			Query: &mocks.MockAPITokens{
				GetResult: &common.APIToken{
					Token: apiToken,
					Roles: strings.Join([]string{string(adminroles.RoleDelete)}, ","),
				},
			},
			APICalls: dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "Bad API Token",
				Request:          req,
				ExpectedStatus:   http.StatusUnauthorized,
				ExpectedResponse: `{"message":"token not found","error":"Unauthorized"}`,
			},
			RequiredRoles: roles,
			RedisClient:   client,
			Query:         db,
			APICalls:      dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "Unknown API Token",
				Request:          testhelper.MakeTestRequestWithHeaders(http.MethodGet, "/", map[string]string{cache.ApiKeyHeader: "abc"}, nil),
				ExpectedStatus:   http.StatusUnauthorized,
				ExpectedResponse: `{"message":"token not found","error":"Unauthorized"}`,
			},
			RequiredRoles: roles,
			RedisClient:   client,
			Query:         db,
			APICalls:      dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "No headers",
				Request:          testhelper.MakeTestRequest(http.MethodGet, "/", nil),
				ExpectedStatus:   http.StatusUnauthorized,
				ExpectedResponse: `{"message":"no api token header","error":"Unauthorized"}`,
			},
			RequiredRoles: roles,
			RedisClient:   client,
			Query:         db,
			APICalls:      dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "No headers, JWT auth",
				Request:          testhelper.MakeTestRequest(http.MethodGet, "/", nil),
				ExpectedStatus:   http.StatusUnauthorized,
				ExpectedResponse: `{"message":"no authorization header","error":"Unauthorized"}`,
			},
			JWTAuth:       true,
			RequiredRoles: roles,
			RedisClient:   client,
			Query:         db,
			APICalls:      dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "No headers, JWT auth, no required roles",
				Request:          testhelper.MakeTestRequest(http.MethodGet, "/", nil),
				ExpectedStatus:   http.StatusOK,
				ExpectedResponse: `OK`,
			},
			JWTAuth:     true,
			RedisClient: client,
			Query:       db,
			APICalls:    dbCalls,
		},
		{
			BasicHttpTest: testhelper.BasicHttpTest{
				Name:             "Failed api calls log",
				Request:          req,
				ExpectedStatus:   http.StatusOK,
				ExpectedResponse: `OK`,
			},
			RequiredRoles: roles,
			RedisClient:   client,
			Query: &mocks.MockAPITokens{
				GetResult: &common.APIToken{
					Token:     apiToken,
					Roles:     strings.Join([]string{string(adminroles.RoleCreate)}, ","),
					ExpiresAt: &validExpiresAt,
				},
			},
			APICalls: &mocks.MockAPICalls{
				DBMockHelper: mocks.DBMockHelper{
					Error: someErr,
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.Name, func(t *testing.T) {
			handler := setupAuthAPIToken(t, test)
			testhelper.RunBasicHttpTest(t, test.BasicHttpTest, handler)
		})
	}
}

func setupAuthAPIToken(t *testing.T, test testCaseAuthAPIToken) http.HandlerFunc {
	testHandler := func(w http.ResponseWriter, r *http.Request) {
		if test.ExpectedProvider != "" {
			if provider, ok := r.Context().Value(c.ProviderKey).(string); !ok || provider != test.ExpectedProvider {
				t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedProvider, provider)
			}
		}

		w.Write([]byte(expectedOkBody))
	}

	auth := httphandlers.AuthAPIToken{
		APITokens: test.Query,
		APICalls:  test.APICalls,
		JWTAuth:   test.JWTAuth,
	}

	return auth.GetHandler(test.RequiredRoles, testHandler)
}

type RedisMockAuthAPIToken struct {
	GetCacheError error
	SetCacheError error
	redis.Connection
}

func (m *RedisMockAuthAPIToken) GetCache(key string, dest interface{}, expire int) error {
	return m.GetCacheError
}

func (m *RedisMockAuthAPIToken) SetCache(string, interface{}, int) error {
	return m.SetCacheError
}
