package apikeyimpl

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/apimachinery/identity"
	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/services/apikey"
	"github.com/grafana/grafana/pkg/services/user"
	"github.com/grafana/grafana/pkg/tests/testsuite"
)

func TestMain(m *testing.M) {
	testsuite.Run(m)
}

type getStore func(db.DB) store

type getApiKeysTestCase struct {
	desc               string
	user               identity.Requester
	expectedAllNumKeys int
}

func mockTimeNow() {
	var timeSeed int64
	timeNow = func() time.Time {
		loc := time.FixedZone("MockZoneUTC-5", -5*60*60)
		fakeNow := time.Unix(timeSeed, 0).In(loc)
		timeSeed++
		return fakeNow
	}
}

func resetTimeNow() {
	timeNow = time.Now
}

func seedApiKeys(t *testing.T, store store, num int) {
	t.Helper()

	for i := 0; i < num; i++ {
		_, err := store.AddAPIKey(context.Background(), &apikey.AddCommand{
			Name:  fmt.Sprintf("key:%d", i),
			Key:   fmt.Sprintf("key:%d", i),
			OrgID: 1,
		})
		require.NoError(t, err)
	}
}

func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
	t.Helper()

	mockTimeNow()
	defer resetTimeNow()

	t.Run("Testing API Key data access", func(t *testing.T) {
		db := db.InitTestDB(t)
		ss := fn(db)

		t.Run("Given saved api key", func(t *testing.T) {
			cmd := apikey.AddCommand{OrgID: 1, Name: "hello", Key: "asd"}
			_, err := ss.AddAPIKey(context.Background(), &cmd)
			assert.Nil(t, err)

			t.Run("Should be able to get key by name", func(t *testing.T) {
				query := apikey.GetByNameQuery{KeyName: "hello", OrgID: 1}
				key, err := ss.GetApiKeyByName(context.Background(), &query)

				assert.Nil(t, err)
				assert.NotNil(t, key)
			})

			t.Run("Should be able to get key by hash", func(t *testing.T) {
				key, err := ss.GetAPIKeyByHash(context.Background(), cmd.Key)

				assert.Nil(t, err)
				assert.NotNil(t, key)
			})
		})

		t.Run("Add non expiring key", func(t *testing.T) {
			cmd := apikey.AddCommand{OrgID: 1, Name: "non-expiring", Key: "asd1", SecondsToLive: 0}
			_, err := ss.AddAPIKey(context.Background(), &cmd)
			assert.Nil(t, err)

			query := apikey.GetByNameQuery{KeyName: "non-expiring", OrgID: 1}
			key, err := ss.GetApiKeyByName(context.Background(), &query)
			assert.Nil(t, err)
			assert.Nil(t, key.Expires)
		})

		t.Run("Add an expiring key", func(t *testing.T) {
			// expires in one hour
			cmd := apikey.AddCommand{OrgID: 1, Name: "expiring-in-an-hour", Key: "asd2", SecondsToLive: 3600}
			_, err := ss.AddAPIKey(context.Background(), &cmd)
			assert.Nil(t, err)

			query := apikey.GetByNameQuery{KeyName: "expiring-in-an-hour", OrgID: 1}
			key, err := ss.GetApiKeyByName(context.Background(), &query)
			assert.Nil(t, err)

			assert.True(t, *key.Expires >= timeNow().Unix())

			// timeNow() has been called twice since creation; once by AddAPIKey and once by GetApiKeyByName
			// therefore two seconds should be subtracted by next value returned by timeNow()
			// that equals the number by which timeSeed has been advanced
			then := timeNow().Add(-2 * time.Second)
			expected := then.Add(1 * time.Hour).UTC().Unix()
			assert.Equal(t, *key.Expires, expected)
		})

		t.Run("Last Used At datetime update", func(t *testing.T) {
			// expires in one hour
			cmd := apikey.AddCommand{OrgID: 1, Name: "last-update-at", Key: "asd3", SecondsToLive: 3600}
			key, err := ss.AddAPIKey(context.Background(), &cmd)
			require.NoError(t, err)

			assert.Nil(t, key.LastUsedAt)

			err = ss.UpdateAPIKeyLastUsedDate(context.Background(), key.ID)
			require.NoError(t, err)

			query := apikey.GetByNameQuery{KeyName: "last-update-at", OrgID: 1}
			key, err = ss.GetApiKeyByName(context.Background(), &query)
			assert.Nil(t, err)
			assert.NotNil(t, key.LastUsedAt)
		})

		t.Run("Add a key with negative lifespan", func(t *testing.T) {
			// expires in one day
			cmd := apikey.AddCommand{OrgID: 1, Name: "key-with-negative-lifespan", Key: "asd3", SecondsToLive: -3600}
			_, err := ss.AddAPIKey(context.Background(), &cmd)
			assert.EqualError(t, err, apikey.ErrInvalidExpiration.Error())

			query := apikey.GetByNameQuery{KeyName: "key-with-negative-lifespan", OrgID: 1}
			_, err = ss.GetApiKeyByName(context.Background(), &query)
			assert.EqualError(t, err, "invalid API key")
		})

		t.Run("Add keys", func(t *testing.T) {
			// never expires
			cmd := apikey.AddCommand{OrgID: 1, Name: "key1", Key: "key1", SecondsToLive: 0}
			_, err := ss.AddAPIKey(context.Background(), &cmd)
			assert.Nil(t, err)

			// expires in 1s
			cmd = apikey.AddCommand{OrgID: 1, Name: "key2", Key: "key2", SecondsToLive: 1}
			_, err = ss.AddAPIKey(context.Background(), &cmd)
			assert.Nil(t, err)

			// expires in one hour
			cmd = apikey.AddCommand{OrgID: 1, Name: "key3", Key: "key3", SecondsToLive: 3600}
			_, err = ss.AddAPIKey(context.Background(), &cmd)
			assert.Nil(t, err)

			// advance mocked getTime by 1s
			timeNow()
		})
	})

	t.Run("Testing API Key errors", func(t *testing.T) {
		db := db.InitTestDB(t)
		ss := fn(db)

		t.Run("Testing API Duplicate Key Errors", func(t *testing.T) {
			t.Run("Given saved api key", func(t *testing.T) {
				cmd := apikey.AddCommand{OrgID: 0, Name: "duplicate", Key: "asd"}
				_, err := ss.AddAPIKey(context.Background(), &cmd)
				assert.Nil(t, err)

				t.Run("Add API Key with existing Org ID and Name", func(t *testing.T) {
					cmd := apikey.AddCommand{OrgID: 0, Name: "duplicate", Key: "asd"}
					_, err = ss.AddAPIKey(context.Background(), &cmd)
					assert.EqualError(t, err, apikey.ErrDuplicate.Error())
				})
			})
		})
	})

	t.Run("Testing Get API keys", func(t *testing.T) {
		tests := []getApiKeysTestCase{
			{
				desc:               "expect all keys for wildcard scope",
				user:               &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{}},
				expectedAllNumKeys: 10,
			},
			{
				desc:               "expect only api keys that user have scopes for",
				user:               &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{}},
				expectedAllNumKeys: 10,
			},
			{
				desc:               "expect no keys when user have no scopes",
				user:               &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{}},
				expectedAllNumKeys: 10,
			},
		}

		for _, tt := range tests {
			t.Run(tt.desc, func(t *testing.T) {
				db := db.InitTestDB(t, db.InitTestDBOpt{})
				store := fn(db)
				seedApiKeys(t, store, 10)

				res, err := store.GetAllAPIKeys(context.Background(), 1)
				require.NoError(t, err)
				assert.Equal(t, tt.expectedAllNumKeys, len(res))
			})
		}
	})
}
