package internal_key

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/onsi/gomega"

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

var service *InternalKeyService

func TestMain(m *testing.M) {
	fmt.Println("start tests")
	impl.ReleaseAll()

	_ = db_docker.TryInitDB()
	initData()
	service = NewInternalKeyService()

	code := m.Run()
	impl.ReleaseAll()

	os.Exit(code)
}

func initData() {
	// Create internal user
	_, err := model.InsertUserIfAbsent("internal_key_test", "internal_key_user", "internal_key@pingcap.com", true)
	if err != nil {
		panic(err)
	}

	// Create non-internal user
	_, err = model.InsertUserIfAbsent("external_key_test", "external_key_user", "external_key@pingcap.com", false)
	if err != nil {
		panic(err)
	}
}

func createTestContext(method string, user *session.CtxUser, body []byte, params []gin.Param, query string) (*httptest.ResponseRecorder, *gin.Context) {
	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	c.Set(session.CtxUserKey, user)

	var bodyReader io.Reader
	if body != nil {
		bodyReader = bytes.NewReader(body)
	}

	c.Request = httptest.NewRequest(method, "http://127.0.0.1:8080"+query, bodyReader)
	c.Params = params
	if body != nil {
		c.Request.Header.Set("Content-Type", "application/json")
	}

	return w, c
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: true,
	}

	// Create key with empty JSON body
	body := []byte("{}")
	w, c := createTestContext(http.MethodPost, ctxUser, body, nil, "")
	service.Create(c)

	g.Expect(w.Code).To(gomega.Equal(http.StatusOK))

	var resp KeyResponse
	err = json.Unmarshal(w.Body.Bytes(), &resp)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(resp.ID).To(gomega.BeNumerically(">", 0))
	g.Expect(resp.Key).NotTo(gomega.BeEmpty())
	g.Expect(resp.Status).To(gomega.Equal("active"))
	fmt.Println("Created key response:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: true,
	}

	reqBody := CreateKeyRequest{
		Name: "my-test-key",
	}
	body, err := json.Marshal(reqBody)
	g.Expect(err).To(gomega.BeNil())

	w, c := createTestContext(http.MethodPost, ctxUser, body, nil, "")
	service.Create(c)

	g.Expect(w.Code).To(gomega.Equal(http.StatusOK))

	var resp KeyResponse
	err = json.Unmarshal(w.Body.Bytes(), &resp)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(resp.Name).To(gomega.Equal("my-test-key"))
	fmt.Println("Created key with name:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: true,
	}

	expiresIn := int64(86400) // 24 hours
	reqBody := CreateKeyRequest{
		Name:      "expiring-key",
		ExpiresIn: &expiresIn,
	}
	body, err := json.Marshal(reqBody)
	g.Expect(err).To(gomega.BeNil())

	w, c := createTestContext(http.MethodPost, ctxUser, body, nil, "")
	service.Create(c)

	g.Expect(w.Code).To(gomega.Equal(http.StatusOK))

	var resp KeyResponse
	err = json.Unmarshal(w.Body.Bytes(), &resp)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(resp.ExpiresAt).NotTo(gomega.BeNil())
	fmt.Println("Created key with expiration:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: false, // Non-internal user
	}

	w, c := createTestContext(http.MethodPost, ctxUser, nil, nil, "")
	service.Create(c)

	// Should be forbidden
	g.Expect(w.Code).To(gomega.Equal(http.StatusOK)) // gin context error handling
	// Check the error in context
	g.Expect(len(c.Errors)).To(gomega.BeNumerically(">", 0))
	fmt.Println("Non-internal user create response:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: true,
	}

	w, c := createTestContext(http.MethodGet, ctxUser, nil, nil, "?limit=10&page=1")
	service.List(c)

	g.Expect(w.Code).To(gomega.Equal(http.StatusOK))

	var resp ListKeysResponse
	err = json.Unmarshal(w.Body.Bytes(), &resp)
	g.Expect(err).To(gomega.BeNil())
	g.Expect(resp.Total).To(gomega.BeNumerically(">=", 0))
	fmt.Println("List keys response:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: false,
	}

	w, c := createTestContext(http.MethodGet, ctxUser, nil, nil, "")
	service.List(c)

	g.Expect(len(c.Errors)).To(gomega.BeNumerically(">", 0))
	fmt.Println("Non-internal user list response:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: true,
	}

	// First create a key to delete - pass empty JSON body
	body := []byte("{}")
	w, c := createTestContext(http.MethodPost, ctxUser, body, nil, "")
	service.Create(c)
	g.Expect(w.Code).To(gomega.Equal(http.StatusOK))

	var createResp KeyResponse
	err = json.Unmarshal(w.Body.Bytes(), &createResp)
	g.Expect(err).To(gomega.BeNil())

	// Now delete it
	params := []gin.Param{
		{Key: "id", Value: fmt.Sprintf("%d", createResp.ID)},
	}
	w, c = createTestContext(http.MethodDelete, ctxUser, nil, params, "")
	service.Delete(c)

	g.Expect(w.Code).To(gomega.Equal(http.StatusOK))
	fmt.Println("Delete key response:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: true,
	}

	params := []gin.Param{
		{Key: "id", Value: "99999"}, // Non-existent ID
	}
	w, c := createTestContext(http.MethodDelete, ctxUser, nil, params, "")
	service.Delete(c)

	g.Expect(len(c.Errors)).To(gomega.BeNumerically(">", 0))
	fmt.Println("Delete non-existent key response:", w.Body.String())
}

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

	// Create a key as internal_key_user
	user1, err := model.GetUserByUserName("internal_key_user")
	g.Expect(err).To(gomega.BeNil())

	ctxUser1 := &session.CtxUser{
		Username: user1.UserName,
		UserID:   user1.ID,
		Email:    user1.Email,
		Internal: true,
	}

	// Pass empty JSON body
	body := []byte("{}")
	w, c := createTestContext(http.MethodPost, ctxUser1, body, nil, "")
	service.Create(c)
	g.Expect(w.Code).To(gomega.Equal(http.StatusOK))

	var createResp KeyResponse
	err = json.Unmarshal(w.Body.Bytes(), &createResp)
	g.Expect(err).To(gomega.BeNil())

	// Create another internal user
	_, err = model.InsertUserIfAbsent("internal_key_test2", "internal_key_user2", "internal_key2@pingcap.com", true)
	g.Expect(err).To(gomega.BeNil())

	user2, err := model.GetUserByUserName("internal_key_user2")
	g.Expect(err).To(gomega.BeNil())

	ctxUser2 := &session.CtxUser{
		Username: user2.UserName,
		UserID:   user2.ID,
		Email:    user2.Email,
		Internal: true,
	}

	// Try to delete user1's key as user2
	params := []gin.Param{
		{Key: "id", Value: fmt.Sprintf("%d", createResp.ID)},
	}
	w, c = createTestContext(http.MethodDelete, ctxUser2, nil, params, "")
	service.Delete(c)

	// Should be forbidden
	g.Expect(len(c.Errors)).To(gomega.BeNumerically(">", 0))
	fmt.Println("Delete other user's key response:", w.Body.String())
}

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

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

	ctxUser := &session.CtxUser{
		Username: user.UserName,
		UserID:   user.ID,
		Email:    user.Email,
		Internal: false,
	}

	params := []gin.Param{
		{Key: "id", Value: "1"},
	}
	w, c := createTestContext(http.MethodDelete, ctxUser, nil, params, "")
	service.Delete(c)

	g.Expect(len(c.Errors)).To(gomega.BeNumerically(">", 0))
	fmt.Println("Non-internal user delete response:", w.Body.String())
}

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

	w, c := createTestContext(http.MethodPost, nil, nil, nil, "")
	c.Set(session.CtxUserKey, nil) // No user in context
	service.Create(c)

	g.Expect(len(c.Errors)).To(gomega.BeNumerically(">", 0))
	fmt.Println("No user create response:", w.Body.String())
}
