package oauth2_test

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"

	oauth2_dto "github.com/tidbcloud/auth-server/internal/dto/oauth2"
)

const (
	TokenEndpoint  = "/v1/token"
	RevokeEndpoint = "/v1/revoke"
)

func TestTokenEndpointMethodNotAllowed(t *testing.T) {
	ginEng, _, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	notAllowedMethods := []string{"GET", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}

	for _, method := range notAllowedMethods {
		t.Run(method, func(t *testing.T) {
			w := httptest.NewRecorder()
			req, _ := http.NewRequest(method, TokenEndpoint, nil)
			ginEng.ServeHTTP(w, req)

			assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
		})
	}
}

func TestTokenEndpointBadRequest(t *testing.T) {
	ginEng, _, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	t.Run("EmptyRequestBody", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, nil)
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"bad request body\"}", w.Body.String())
	})

	t.Run("InvalidRequestBody1", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("invalid"))
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"grant_type is required\"}", w.Body.String())
	})

	t.Run("InvalidRequestBody2", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{}"))
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"grant_type is required\"}", w.Body.String())
	})

	t.Run("InvalidGrantType", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"invalid\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"unsupported_grant_type\"}", w.Body.String())
	})
}

func TestTokenEndpointDeviceCodeGrantFailed(t *testing.T) {
	ginEng, _, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	t.Run("MissingDeviceCode", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"urn:ietf:params:oauth:grant-type:device_code\", \"client_id\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"device_code is required\"}", w.Body.String())
	})

	t.Run("MissingClientID", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"urn:ietf:params:oauth:grant-type:device_code\", \"device_code\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"client_id is required\"}", w.Body.String())
	})
}

func TestTokenEndpointDeviceCodeGrantSuccess(t *testing.T) {
	ginEng, mockOauth2Svc, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	mockOauth2Svc.EXPECT().HandleDeviceTokenRequest(gomock.Any(), "test", "test").Return(
		&oauth2_dto.TokenResponse{
			AccessToken:  "ak_123",
			TokenType:    "Bearer",
			ExpiresIn:    3600,
			RefreshToken: "rk_123",
		}, nil,
	).AnyTimes()

	t.Run("JSONRequestSuccess", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"urn:ietf:params:oauth:grant-type:device_code\", \"device_code\":\"test\", \"client_id\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)
		assert.Equal(t, "{\"access_token\":\"ak_123\",\"token_type\":\"Bearer\",\"expires_in\":3600,\"refresh_token\":\"rk_123\"}", w.Body.String())
	})

	t.Run("FormRequestSuccess", func(t *testing.T) {
		w := httptest.NewRecorder()
		formData := url.Values{}
		formData.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code")
		formData.Set("device_code", "test")
		formData.Set("client_id", "test")
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString(formData.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)
		assert.Equal(t, "{\"access_token\":\"ak_123\",\"token_type\":\"Bearer\",\"expires_in\":3600,\"refresh_token\":\"rk_123\"}", w.Body.String())
	})
}

func TestTokenEndpoinAuthorizationCodeGrantFailed(t *testing.T) {
	ginEng, _, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	t.Run("MissingCode", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"authorization_code\", \"client_id\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"code is required\"}", w.Body.String())
	})

	t.Run("MissingClientID", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"authorization_code\", \"code\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"client_id is required\"}", w.Body.String())
	})

	t.Run("MissingClientSecret", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"authorization_code\", \"code\":\"test\", \"client_id\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"client_secret is required\"}", w.Body.String())
	})

	t.Run("MissingRedirectURI", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"authorization_code\", \"code\":\"test\", \"client_id\":\"test\", \"client_secret\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"redirect_uri is required\"}", w.Body.String())
	})
}

func TestTokenEndpointAuthorizationCodeGrantSuccess(t *testing.T) {
	ginEng, mockOauth2Svc, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	mockOauth2Svc.EXPECT().CreateAccessTokenByCodeGrant(gomock.Any(), "test_id", "test_secret", "code", "https://example.com").Return(
		&oauth2_dto.TokenResponse{
			AccessToken:  "ak_123",
			TokenType:    "Bearer",
			ExpiresIn:    3600,
			RefreshToken: "rk_123",
		}, nil,
	).AnyTimes()

	t.Run("JSONRequestSuccess", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString("{\"grant_type\":\"authorization_code\", \"code\":\"code\", \"client_id\":\"test_id\", \"client_secret\":\"test_secret\", \"redirect_uri\":\"https://example.com\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)
		assert.Equal(t, "{\"access_token\":\"ak_123\",\"token_type\":\"Bearer\",\"expires_in\":3600,\"refresh_token\":\"rk_123\"}", w.Body.String())
	})

	t.Run("FormRequestSuccess", func(t *testing.T) {
		w := httptest.NewRecorder()
		formData := url.Values{}
		formData.Set("grant_type", "authorization_code")
		formData.Set("code", "code")
		formData.Set("client_id", "test_id")
		formData.Set("client_secret", "test_secret")
		formData.Set("redirect_uri", "https://example.com")
		req, _ := http.NewRequest("POST", TokenEndpoint, bytes.NewBufferString(formData.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)
		assert.Equal(t, "{\"access_token\":\"ak_123\",\"token_type\":\"Bearer\",\"expires_in\":3600,\"refresh_token\":\"rk_123\"}", w.Body.String())
	})
}

func TestRevokeEndpointMethodNotAllowed(t *testing.T) {
	ginEng, _, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	notAllowedMethods := []string{"GET", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}

	for _, method := range notAllowedMethods {
		t.Run(method, func(t *testing.T) {
			w := httptest.NewRecorder()
			req, _ := http.NewRequest(method, TokenEndpoint, nil)
			ginEng.ServeHTTP(w, req)

			assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
		})
	}
}

func TestRevokeEndpointBadRequest(t *testing.T) {
	ginEng, _, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	t.Run("EmptyRequestBody", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", RevokeEndpoint, nil)
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"bad request body\"}", w.Body.String())
	})

	t.Run("InvalidRequestBody1", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", RevokeEndpoint, bytes.NewBufferString("invalid"))
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"client_id is required\"}", w.Body.String())
	})

	t.Run("InvalidRequestBody2", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", RevokeEndpoint, bytes.NewBufferString("{}"))
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"client_id is required\"}", w.Body.String())
	})

	t.Run("MissingClientSecret", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", RevokeEndpoint, bytes.NewBufferString("{\"client_id\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"client_secret is required\"}", w.Body.String())
	})

	t.Run("MissingToken", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", RevokeEndpoint, bytes.NewBufferString("{\"client_id\":\"test\", \"client_secret\":\"test\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"token is required\"}", w.Body.String())
	})

	t.Run("InvalidTokenTypeHint", func(t *testing.T) {
		w := httptest.NewRecorder()
		req, _ := http.NewRequest("POST", RevokeEndpoint, bytes.NewBufferString("{\"client_id\":\"test\", \"client_secret\":\"test\", \"token\":\"test\", \"token_type_hint\":\"invalid\"}"))
		req.Header.Set("Content-Type", "application/json")
		ginEng.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
		assert.Equal(t, "{\"error\":\"invalid_request\",\"error_description\":\"unsupported token type\"}", w.Body.String())
	})
}

func TestRevokeEndpointSuccess(t *testing.T) {
	ginEng, mockOauth2Svc, ctrl := SetupTestRouter()
	defer ctrl.Finish()

	mockOauth2Svc.EXPECT().HandleRevokeTokenRequest(gomock.Any(), "test", "test", "test", "access_token").Return()

	w := httptest.NewRecorder()
	req, _ := http.NewRequest("POST", RevokeEndpoint, bytes.NewBufferString("{\"client_id\":\"test\", \"client_secret\":\"test\", \"token\":\"test\", \"token_type_hint\":\"access_token\"}"))
	req.Header.Set("Content-Type", "application/json")
	ginEng.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)
	assert.Equal(t, "", w.Body.String())
}
