package model

import (
	"fmt"
	"testing"
	"time"

	"github.com/dchest/uniuri"
	"github.com/onsi/gomega"

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

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

	// Get internal user created in initData
	user, err := GetUserByUserName("test_user01")
	g.Expect(err).To(gomega.BeNil())
	g.Expect(user).NotTo(gomega.BeNil())
	g.Expect(user.Internal).To(gomega.BeTrue())

	// Create a hashed key
	key := uniuri.NewLen(32)
	hashedKey, err := utils.EncodePassword(key, fmt.Sprintf("api_key%d", user.ID))
	g.Expect(err).To(gomega.BeNil())

	// Add credential
	cred, err := AddInternalAPICredential(user.ID, "test-key-1", hashedKey, nil)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(cred).NotTo(gomega.BeNil())
	g.Expect(cred.ID).To(gomega.BeNumerically(">", 0))
	g.Expect(cred.UserID).To(gomega.Equal(user.ID))
	g.Expect(cred.Name).To(gomega.Equal("test-key-1"))
	g.Expect(cred.Status).To(gomega.Equal("active"))
	g.Expect(cred.ExpiresAt).To(gomega.BeNil())
}

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

	user, err := GetUserByUserName("test_user01")
	g.Expect(err).To(gomega.BeNil())

	key := uniuri.NewLen(32)
	hashedKey, err := utils.EncodePassword(key, fmt.Sprintf("api_key%d", user.ID))
	g.Expect(err).To(gomega.BeNil())

	// Add credential with expiration
	expiresAt := time.Now().Add(24 * time.Hour)
	cred, err := AddInternalAPICredential(user.ID, "test-key-with-expiry", hashedKey, &expiresAt)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(cred).NotTo(gomega.BeNil())
	g.Expect(cred.ExpiresAt).NotTo(gomega.BeNil())
	g.Expect(cred.ExpiresAt.Unix()).To(gomega.BeNumerically("~", expiresAt.Unix(), 1))
}

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

	user, err := GetUserByUserName("test_user01")
	g.Expect(err).To(gomega.BeNil())

	key := uniuri.NewLen(32)
	hashedKey, err := utils.EncodePassword(key, fmt.Sprintf("api_key%d", user.ID))
	g.Expect(err).To(gomega.BeNil())

	// Create credential
	cred, err := AddInternalAPICredential(user.ID, "test-key-get", hashedKey, nil)
	g.Expect(err).To(gomega.BeNil())

	// Get the credential
	retrieved, err := GetInternalAPICredential(cred.ID)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(retrieved).NotTo(gomega.BeNil())
	g.Expect(retrieved.ID).To(gomega.Equal(cred.ID))
	g.Expect(retrieved.Name).To(gomega.Equal("test-key-get"))
	g.Expect(retrieved.UserID).To(gomega.Equal(user.ID))
}

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

	// Create a new user for this test to avoid interference
	userName, err := InsertUserIfAbsent("internal_list_test", "list_test_user", "list_test@pingcap.com", true)
	g.Expect(err).To(gomega.BeNil())

	user, err := GetUserByUserName(userName)
	g.Expect(err).To(gomega.BeNil())

	// Create multiple credentials
	for i := 0; i < 5; i++ {
		key := uniuri.NewLen(32)
		hashedKey, err := utils.EncodePassword(key, fmt.Sprintf("api_key%d", user.ID))
		g.Expect(err).To(gomega.BeNil())

		_, err = AddInternalAPICredential(user.ID, fmt.Sprintf("list-test-key-%d", i), hashedKey, nil)
		g.Expect(err).To(gomega.BeNil())
	}

	// List all credentials (no pagination)
	credentials, total, err := ListInternalAPICredentials(user.ID, 0, 0)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(total).To(gomega.Equal(int64(5)))
	g.Expect(len(credentials)).To(gomega.Equal(5))

	// List with pagination
	credentials, total, err = ListInternalAPICredentials(user.ID, 1, 2)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(total).To(gomega.Equal(int64(5)))
	g.Expect(len(credentials)).To(gomega.Equal(2))

	// List second page
	credentials, total, err = ListInternalAPICredentials(user.ID, 2, 2)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(total).To(gomega.Equal(int64(5)))
	g.Expect(len(credentials)).To(gomega.Equal(2))
}

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

	// Create a new user for this test
	userName, err := InsertUserIfAbsent("internal_active_test", "active_test_user", "active_test@pingcap.com", true)
	g.Expect(err).To(gomega.BeNil())

	user, err := GetUserByUserName(userName)
	g.Expect(err).To(gomega.BeNil())

	// Create credentials
	key := uniuri.NewLen(32)
	hashedKey, err := utils.EncodePassword(key, fmt.Sprintf("api_key%d", user.ID))
	g.Expect(err).To(gomega.BeNil())

	_, err = AddInternalAPICredential(user.ID, "active-test-key", hashedKey, nil)
	g.Expect(err).To(gomega.BeNil())

	// List active credentials
	credentials, err := ListActiveInternalAPICredentials(user.ID)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(len(credentials)).To(gomega.BeNumerically(">=", 1))

	// All should be active
	for _, cred := range credentials {
		g.Expect(cred.Status).To(gomega.Equal("active"))
	}
}

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

	user, err := GetUserByUserName("test_user01")
	g.Expect(err).To(gomega.BeNil())

	key := uniuri.NewLen(32)
	hashedKey, err := utils.EncodePassword(key, fmt.Sprintf("api_key%d", user.ID))
	g.Expect(err).To(gomega.BeNil())

	// Create credential
	cred, err := AddInternalAPICredential(user.ID, "test-key-delete", hashedKey, nil)
	g.Expect(err).To(gomega.BeNil())

	// Delete the credential
	err = DeleteInternalAPICredential(cred.ID)
	g.Expect(err).To(gomega.BeNil())

	// Try to get deleted credential (should fail with soft delete)
	_, err = GetInternalAPICredential(cred.ID)
	g.Expect(err).NotTo(gomega.BeNil())
}

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

	user, err := GetUserByUserName("test_user01")
	g.Expect(err).To(gomega.BeNil())

	key := uniuri.NewLen(32)
	hashedKey, err := utils.EncodePassword(key, fmt.Sprintf("api_key%d", user.ID))
	g.Expect(err).To(gomega.BeNil())

	// Create credential
	cred, err := AddInternalAPICredential(user.ID, "test-key-last-used", hashedKey, nil)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(cred.LastUsedAt).To(gomega.BeNil())

	// Update last used
	err = UpdateInternalCredentialLastUsed(cred.ID)
	g.Expect(err).To(gomega.BeNil())

	// Verify update
	updated, err := GetInternalAPICredential(cred.ID)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(updated.LastUsedAt).NotTo(gomega.BeNil())
	g.Expect(updated.LastUsedAt.Unix()).To(gomega.BeNumerically("~", time.Now().Unix(), 2))
}
