package api_key

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/Kong/go-pdk"
	"github.com/Kong/go-pdk/bridge"
	"github.com/Kong/go-pdk/bridge/bridgetest"
	"github.com/Kong/go-pdk/client"
	"github.com/Kong/go-pdk/log"
	"github.com/Kong/go-pdk/request"
	"github.com/dchest/uniuri"
	"github.com/onsi/gomega"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/auth/utils"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/db/model"
)

func Test_IsAllowUri(t *testing.T) {
	g := gomega.NewWithT(t)

	g.Expect(IsAllowUri("/ab", []string{}, nil)).To(gomega.BeFalse())

	g.Expect(IsAllowUri("/clinic/api/v1/diag/precreate?xxx", []string{"/clinic/api/v1/diag"}, nil)).To(gomega.BeTrue())
}

func Test_IsAllowUriRegexp(t *testing.T) {
	g := gomega.NewWithT(t)

	g.Expect(IsAllowUri("/clinic/api/v1/orgs/12132432143215/clusters", []string{"/clinic/api/v1/orgs"}, nil)).To(gomega.BeTrue())

	g.Expect(IsAllowUri("/ab", []string{"/ba"}, nil)).To(gomega.BeFalse())

	// Test regex matching
	g.Expect(IsAllowUri("/clinic/api/v1/orgs/123/clusters", nil, internalUserAllowUrisRegex)).To(gomega.BeTrue())
	g.Expect(IsAllowUri("/clinic/api/v1/orgs/123/clusters/456", nil, internalUserAllowUrisRegex)).To(gomega.BeTrue())
}

func Test_DecodeApiKey(t *testing.T) {
	g := gomega.NewWithT(t)

	// Test decoding a valid API key
	jsonKey := ApiKeyJson{
		Key:    "test-key-123",
		UserId: 42,
		OrgId:  100,
	}
	jsonBytes, err := json.Marshal(jsonKey)
	g.Expect(err).To(gomega.BeNil())

	encoded := base64.StdEncoding.EncodeToString(jsonBytes)
	decoded, err := Decode(encoded)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(decoded.Key).To(gomega.Equal("test-key-123"))
	g.Expect(decoded.UserId).To(gomega.Equal(uint(42)))
	g.Expect(decoded.OrgId).To(gomega.Equal(uint(100)))
}

func Test_DecodeInternalApiKey(t *testing.T) {
	g := gomega.NewWithT(t)

	// Test decoding an internal API key (OrgId=0)
	jsonKey := ApiKeyJson{
		Key:    "internal-key-456",
		UserId: 123,
		OrgId:  0, // Internal user
	}
	jsonBytes, err := json.Marshal(jsonKey)
	g.Expect(err).To(gomega.BeNil())

	encoded := base64.StdEncoding.EncodeToString(jsonBytes)
	decoded, err := Decode(encoded)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(decoded.Key).To(gomega.Equal("internal-key-456"))
	g.Expect(decoded.UserId).To(gomega.Equal(uint(123)))
	g.Expect(decoded.OrgId).To(gomega.Equal(uint(0)))
}

func Test_DecodeInvalidApiKey(t *testing.T) {
	g := gomega.NewWithT(t)

	// Test decoding invalid base64
	_, err := Decode("not-valid-base64!!!")
	g.Expect(err).To(gomega.Equal(ErrInvalidApiKey))

	// Test decoding invalid JSON
	encoded := base64.StdEncoding.EncodeToString([]byte("not-json"))
	_, err = Decode(encoded)
	g.Expect(err).To(gomega.Equal(ErrInvalidApiKey))
}

func Test_IsValid(t *testing.T) {
	g := gomega.NewWithT(t)

	userID := uint(42)
	rawKey := uniuri.NewLen(32)

	// Hash the key
	hashedKey, err := utils.EncodePassword(rawKey, fmt.Sprintf("api_key%d", userID))
	g.Expect(err).To(gomega.BeNil())

	// Test valid key
	keyJson := &ApiKeyJson{
		Key:    rawKey,
		UserId: userID,
		OrgId:  0,
	}
	isValid, err := IsValid(keyJson, hashedKey)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(isValid).To(gomega.BeTrue())

	// Test invalid key
	keyJson.Key = "wrong-key"
	isValid, err = IsValid(keyJson, hashedKey)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(isValid).To(gomega.BeFalse())
}

func Test_GetAllowUris(t *testing.T) {
	g := gomega.NewWithT(t)

	// Internal user should get more URIs
	internalUris, internalRegexs := getAllowUris(true)
	g.Expect(len(internalUris)).To(gomega.BeNumerically(">", 0))
	g.Expect(len(internalRegexs)).To(gomega.BeNumerically(">", 0))

	// External user should get upload URIs only
	externalUris, externalRegexs := getAllowUris(false)
	g.Expect(len(externalUris)).To(gomega.BeNumerically(">", 0))
	g.Expect(len(externalRegexs)).To(gomega.Equal(0))

	// Internal should have more URIs than external
	g.Expect(len(internalUris)).To(gomega.BeNumerically(">", len(externalUris)))
}

func Test_InternalUserAllowUris(t *testing.T) {
	g := gomega.NewWithT(t)

	// Test internal user allowed URIs
	allowedPaths := []string{
		"/clinic/api/v1/diag/precreate",
		"/clinic/api/v1/diag/upload",
		"/clinic/api/v1/diag/flush",
		"/clinic/api/v1/diagnosis/something",
		"/clinic/api/v1/data/metrics",
		"/clinic/api/v1/orgs",
		"/clinic/api/v1/reports",
		"/ngm/api/v1/something",
		"/fault/test",
		"/chaos/test",
	}

	for _, path := range allowedPaths {
		g.Expect(IsAllowUri(path, internalUserAllowUris, internalUserAllowUrisRegex)).To(gomega.BeTrue(), "path %s should be allowed for internal user", path)
	}

	// Test paths that should not be allowed even for internal user (without internal key)
	notAllowedPaths := []string{
		"/some/random/path",
		"/admin/secret",
	}

	for _, path := range notAllowedPaths {
		g.Expect(IsAllowUri(path, internalUserAllowUris, internalUserAllowUrisRegex)).To(gomega.BeFalse(), "path %s should not be allowed", path)
	}
}

func Test_ExternalUserAllowUris(t *testing.T) {
	g := gomega.NewWithT(t)

	// Test external user allowed URIs (upload only)
	allowedPaths := []string{
		"/clinic/api/v1/diag/precreate",
		"/clinic/api/v1/diag/upload",
		"/clinic/api/v1/diag/flush",
	}

	for _, path := range allowedPaths {
		g.Expect(IsAllowUri(path, externalUserAllowUris, externalUserAllowUrisRegex)).To(gomega.BeTrue(), "path %s should be allowed for external user", path)
	}

	// Test paths that should not be allowed for external user
	notAllowedPaths := []string{
		"/clinic/api/v1/diagnosis/something",
		"/clinic/api/v1/data/metrics",
		"/clinic/api/v1/orgs",
		"/ngm/api/v1/something",
	}

	for _, path := range notAllowedPaths {
		g.Expect(IsAllowUri(path, externalUserAllowUris, externalUserAllowUrisRegex)).To(gomega.BeFalse(), "path %s should not be allowed for external user", path)
	}
}

func Test_SkipAuthUris(t *testing.T) {
	g := gomega.NewWithT(t)

	// Test skip auth URIs
	g.Expect(IsAllowUri("/api/byoc/v1/something", SkipAuthUris, nil)).To(gomega.BeTrue())
	g.Expect(IsAllowUri("/api/byoc/v1/clusters/123", SkipAuthUris, nil)).To(gomega.BeTrue())
	g.Expect(IsAllowUri("/clinic/api/v1/something", SkipAuthUris, nil)).To(gomega.BeFalse())
}

func Test_ApiKeyAuthOnlyUris(t *testing.T) {
	g := gomega.NewWithT(t)

	// Test API key auth only URIs
	g.Expect(IsAllowUri("/clinic/api/v1/diagnosis/something", ApiKeyAuthOnlyUris, nil)).To(gomega.BeTrue())
	g.Expect(IsAllowUri("/clinic/api/v1/other/something", ApiKeyAuthOnlyUris, nil)).To(gomega.BeFalse())
}

func Test_MWTryAuthDecodedKeyVerificationFailureReturnsAuthErr(t *testing.T) {
	t.Cleanup(func() {
		verifyAPIKeyFunc = VerifyAPIKey
		verifyInternalKeyFunc = VerifyInternalKey
		verifyUploadKeyFunc = VerifyUploadKey
		getUserByIDFunc = model.GetUserByID
	})

	getUserByIDFunc = func(uint) (*db.User, error) {
		t.Fatal("GetUserByID must not be called after API key verification fails")
		return nil, nil
	}
	verifyUploadKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
		t.Fatal("upload key verification must not be used for /data-proxy")
		return nil, nil
	}

	t.Run("internal key verification failure", func(t *testing.T) {
		g := gomega.NewWithT(t)

		verifyAPIKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
			return nil, ErrInvalidApiKey
		}
		verifyInternalKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
			return nil, ErrInvalidApiKey
		}

		skipSessionAuth, user, authErr := MWTryAuth(newAuthTestPDK(t, encodedAPIKey(t, ApiKeyJson{
			Key:    "bad-internal-key",
			UserId: 100,
			OrgId:  0,
		}), "/data-proxy/metrics/1/api/v1/query"), nil, true)

		g.Expect(skipSessionAuth).To(gomega.BeTrue())
		g.Expect(user).To(gomega.BeNil())
		g.Expect(authErr).ToNot(gomega.BeNil())
		g.Expect(authErr.Code()).To(gomega.Equal(AUTH_ERROR_CODE))
		g.Expect(authErr.Message()).To(gomega.Equal(InvalidAPIKey))
	})

	t.Run("non upload path verification failure", func(t *testing.T) {
		g := gomega.NewWithT(t)

		verifyAPIKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
			return nil, ErrInvalidApiKey
		}
		verifyInternalKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
			t.Fatal("internal key verification must not be used for non-internal keys")
			return nil, nil
		}

		skipSessionAuth, user, authErr := MWTryAuth(newAuthTestPDK(t, encodedAPIKey(t, ApiKeyJson{
			Key:    "bad-api-key",
			UserId: 101,
			OrgId:  1,
		}), "/data-proxy/metrics/1/api/v1/query"), nil, true)

		g.Expect(skipSessionAuth).To(gomega.BeTrue())
		g.Expect(user).To(gomega.BeNil())
		g.Expect(authErr).ToNot(gomega.BeNil())
		g.Expect(authErr.Code()).To(gomega.Equal(AUTH_ERROR_CODE))
		g.Expect(authErr.Message()).To(gomega.Equal(InvalidAPIKey))
	})
}

func Test_MWTryAuthPreservesInternalCredentialIdentity(t *testing.T) {
	t.Cleanup(func() {
		verifyAPIKeyFunc = VerifyAPIKey
		verifyInternalKeyFunc = VerifyInternalKey
		verifyUploadKeyFunc = VerifyUploadKey
		getUserByIDFunc = model.GetUserByID
	})

	verifyAPIKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
		return nil, ErrInvalidApiKey
	}
	verifyInternalKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
		return &VerifiedCredential{
			UserID: 200,
			Type:   session.CredentialTypeInternal,
			ID:     17,
			Name:   "nutshell-prod",
		}, nil
	}
	getUserByIDFunc = func(id uint) (*db.User, error) {
		g := gomega.NewWithT(t)
		g.Expect(id).To(gomega.Equal(uint(200)))
		user := &db.User{UserName: "automation", Email: "automation@example.com", Internal: true}
		user.ID = id
		return user, nil
	}

	skipSessionAuth, user, authErr := MWTryAuth(newSuccessfulAuthTestPDK(t, encodedAPIKey(t, ApiKeyJson{
		Key:    "internal-key",
		UserId: 200,
		OrgId:  0,
	}), "/data-proxy/metrics/1/api/v1/query"), nil, true)

	g := gomega.NewWithT(t)
	g.Expect(skipSessionAuth).To(gomega.BeTrue())
	g.Expect(authErr).To(gomega.BeNil())
	g.Expect(user).ToNot(gomega.BeNil())
	g.Expect(user.AuthMethod).To(gomega.Equal(session.AuthMethodAPIKey))
	g.Expect(user.CredentialType).To(gomega.Equal(session.CredentialTypeInternal))
	g.Expect(user.CredentialID).To(gomega.Equal(uint(17)))
	g.Expect(user.CredentialName).To(gomega.Equal("nutshell-prod"))
}

func Test_MWTryAuthPreservesLegacyAndUploadCredentialIdentity(t *testing.T) {
	t.Cleanup(func() {
		verifyAPIKeyFunc = VerifyAPIKey
		verifyInternalKeyFunc = VerifyInternalKey
		verifyUploadKeyFunc = VerifyUploadKey
		getUserByIDFunc = model.GetUserByID
	})

	tests := []struct {
		name           string
		path           string
		key            ApiKeyJson
		credentialType string
		credentialID   uint
		credentialName string
		verify         func(*ApiKeyJson) (*VerifiedCredential, error)
	}{
		{
			name:           "legacy api credential",
			path:           "/clinic/api/v1/dashboard/clusters2",
			key:            ApiKeyJson{Key: "api-key", UserId: 201, OrgId: 1},
			credentialType: session.CredentialTypeAPI,
			credentialID:   18,
			credentialName: "legacy-api",
			verify: func(*ApiKeyJson) (*VerifiedCredential, error) {
				return &VerifiedCredential{UserID: 201, Type: session.CredentialTypeAPI, ID: 18, Name: "legacy-api"}, nil
			},
		},
		{
			name:           "upload credential",
			path:           "/clinic/api/v1/diag/upload",
			key:            ApiKeyJson{Key: "upload-key", UserId: 202, OrgId: 2},
			credentialType: session.CredentialTypeUpload,
			credentialID:   19,
			credentialName: "upload-api",
			verify: func(*ApiKeyJson) (*VerifiedCredential, error) {
				return &VerifiedCredential{UserID: 202, Type: session.CredentialTypeUpload, ID: 19, Name: "upload-api"}, nil
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			verifyInternalKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
				t.Fatal("internal key verification must not be used")
				return nil, nil
			}
			if tt.credentialType == session.CredentialTypeUpload {
				verifyAPIKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) { return nil, ErrInvalidApiKey }
				verifyUploadKeyFunc = tt.verify
			} else {
				verifyAPIKeyFunc = tt.verify
				verifyUploadKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
					t.Fatal("upload key verification must not be used")
					return nil, nil
				}
			}
			getUserByIDFunc = func(id uint) (*db.User, error) {
				user := &db.User{UserName: tt.name, Internal: tt.credentialType == session.CredentialTypeAPI}
				user.ID = id
				return user, nil
			}

			_, user, authErr := MWTryAuth(newSuccessfulAuthTestPDK(t, encodedAPIKey(t, tt.key), tt.path), nil, true)

			g := gomega.NewWithT(t)
			g.Expect(authErr).To(gomega.BeNil())
			g.Expect(user).ToNot(gomega.BeNil())
			g.Expect(user.AuthMethod).To(gomega.Equal(session.AuthMethodAPIKey))
			g.Expect(user.CredentialType).To(gomega.Equal(tt.credentialType))
			g.Expect(user.CredentialID).To(gomega.Equal(tt.credentialID))
			g.Expect(user.CredentialName).To(gomega.Equal(tt.credentialName))
		})
	}
}

func Test_MWTryAuthReturnsVerifiedCallerWhenPathIsRejected(t *testing.T) {
	t.Cleanup(func() {
		verifyAPIKeyFunc = VerifyAPIKey
		verifyInternalKeyFunc = VerifyInternalKey
		verifyUploadKeyFunc = VerifyUploadKey
		getUserByIDFunc = model.GetUserByID
	})

	verifyAPIKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
		return &VerifiedCredential{UserID: 203, Type: session.CredentialTypeAPI, ID: 20, Name: "rejected-api"}, nil
	}
	getUserByIDFunc = func(id uint) (*db.User, error) {
		user := &db.User{UserName: "external-user"}
		user.ID = id
		return user, nil
	}

	skipSessionAuth, user, authErr := MWTryAuth(newRejectedPathAuthTestPDK(t, encodedAPIKey(t, ApiKeyJson{
		Key: "api-key", UserId: 203, OrgId: 3,
	}), "/clinic/api/v1/dashboard/clusters2"), nil, true)

	g := gomega.NewWithT(t)
	g.Expect(skipSessionAuth).To(gomega.BeTrue())
	g.Expect(authErr).ToNot(gomega.BeNil())
	g.Expect(authErr.Message()).To(gomega.Equal(InvalidAPIPath))
	g.Expect(user).ToNot(gomega.BeNil())
	g.Expect(user.CredentialID).To(gomega.Equal(uint(20)))
	g.Expect(user.CredentialName).To(gomega.Equal("rejected-api"))
}

func Test_MWTryAuthReturnsVerifiedCallerWhenUserLookupFails(t *testing.T) {
	t.Cleanup(func() {
		verifyAPIKeyFunc = VerifyAPIKey
		verifyInternalKeyFunc = VerifyInternalKey
		verifyUploadKeyFunc = VerifyUploadKey
		getUserByIDFunc = model.GetUserByID
	})

	verifyAPIKeyFunc = func(*ApiKeyJson) (*VerifiedCredential, error) {
		return &VerifiedCredential{UserID: 204, Type: session.CredentialTypeAPI, ID: 21, Name: "known-api"}, nil
	}
	getUserByIDFunc = func(uint) (*db.User, error) {
		return nil, fmt.Errorf("user lookup unavailable")
	}

	skipSessionAuth, user, authErr := MWTryAuth(newAuthTestPDK(t, encodedAPIKey(t, ApiKeyJson{
		Key: "api-key", UserId: 204, OrgId: 3,
	}), "/clinic/api/v1/dashboard/clusters2"), nil, true)

	g := gomega.NewWithT(t)
	g.Expect(skipSessionAuth).To(gomega.BeTrue())
	g.Expect(authErr).ToNot(gomega.BeNil())
	g.Expect(authErr.Message()).To(gomega.Equal(InvalidUser))
	g.Expect(user).ToNot(gomega.BeNil())
	g.Expect(user.UserID).To(gomega.Equal(uint(204)))
	g.Expect(user.AuthMethod).To(gomega.Equal(session.AuthMethodAPIKey))
	g.Expect(user.CredentialType).To(gomega.Equal(session.CredentialTypeAPI))
	g.Expect(user.CredentialID).To(gomega.Equal(uint(21)))
	g.Expect(user.CredentialName).To(gomega.Equal("known-api"))
}

func encodedAPIKey(t *testing.T, key ApiKeyJson) string {
	t.Helper()

	data, err := json.Marshal(key)
	if err != nil {
		t.Fatal(err)
	}
	return base64.StdEncoding.EncodeToString(data)
}

func newAuthTestPDK(t *testing.T, bearer, path string) *pdk.PDK {
	t.Helper()

	pdkBridge := bridge.New(bridgetest.Mock(t, []bridgetest.MockStep{
		{Method: "kong.request.get_header", Args: bridge.WrapString("Authorization"), Ret: bridge.WrapString("Bearer " + bearer)},
		{Method: "kong.request.get_path", Ret: bridge.WrapString(path)},
		{Method: "kong.log.err"},
	}))
	return &pdk.PDK{
		Request: request.Request{PdkBridge: pdkBridge},
		Log:     log.Log{PdkBridge: pdkBridge},
	}
}

func newSuccessfulAuthTestPDK(t *testing.T, bearer, path string) *pdk.PDK {
	t.Helper()

	pdkBridge := bridge.New(bridgetest.Mock(t, []bridgetest.MockStep{
		{Method: "kong.request.get_header", Args: bridge.WrapString("Authorization"), Ret: bridge.WrapString("Bearer " + bearer)},
		{Method: "kong.request.get_path", Ret: bridge.WrapString(path)},
		{Method: "kong.client.get_forwarded_ip", Ret: bridge.WrapString("127.0.0.1")},
	}))
	return &pdk.PDK{
		Client:  client.Client{PdkBridge: pdkBridge},
		Request: request.Request{PdkBridge: pdkBridge},
		Log:     log.Log{PdkBridge: pdkBridge},
	}
}

func newRejectedPathAuthTestPDK(t *testing.T, bearer, path string) *pdk.PDK {
	t.Helper()

	pdkBridge := bridge.New(bridgetest.Mock(t, []bridgetest.MockStep{
		{Method: "kong.request.get_header", Args: bridge.WrapString("Authorization"), Ret: bridge.WrapString("Bearer " + bearer)},
		{Method: "kong.request.get_path", Ret: bridge.WrapString(path)},
		{Method: "kong.log.warn"},
	}))
	return &pdk.PDK{
		Request: request.Request{PdkBridge: pdkBridge},
		Log:     log.Log{PdkBridge: pdkBridge},
	}
}
