package auth

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"golang.org/x/oauth2"
)

const (
	TestGoogleClientID     = "test-client-id"
	TestGoogleClientSecret = "test-client-secret"
	TestGoogleExternalURL  = "http://localhost:8080"
)

func setupGoogleTest(allowedUsers, allowedWorkspaces []string) (Provider, gin.IRoutes) {
	p, _ := NewGoogleProvider(TestGoogleExternalURL, TestGoogleClientID, TestGoogleClientSecret, allowedUsers, allowedWorkspaces)

	goog := gin.New()
	goog.POST("/token", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"access_token": "test-access-token",
		})
	})
	tsgoog := httptest.NewServer(goog)
	gp := p.(*googleProvider)
	gp.SetOAuth2Endpoint(oauth2.Endpoint{
		AuthURL:  tsgoog.URL + "/auth",
		TokenURL: tsgoog.URL + "/token",
	})

	googapi := gin.New()
	tsgoogapi := httptest.NewServer(googapi)
	gp.SetUserinfoEndpoint(tsgoogapi.URL + "/userinfo")

	return p, googapi
}

func TestGoogleProvider(t *testing.T) {
	p, _ := setupGoogleTest([]string{}, []string{})
	require.Equal(t, p.Name(), "Google")
	require.Equal(t, p.Type(), "google")
	require.Equal(t, p.RedirectURL(), GoogleCallbackEndpoint)
	require.Equal(t, p.AuthURL(), GoogleAuthEndpoint)

	authCodeURL, err := p.AuthCodeURL("test-state")
	require.NoError(t, err)
	require.NotEmpty(t, authCodeURL)
	authCodeURLObj, err := url.Parse(authCodeURL)
	require.NoError(t, err)
	require.Equal(t, authCodeURLObj.Path, "/auth")
	require.Equal(t, authCodeURLObj.Query().Get("client_id"), TestGoogleClientID)
	require.Equal(t, authCodeURLObj.Query().Get("redirect_uri"), TestGoogleExternalURL+"/.auth/google/callback")
	require.Equal(t, authCodeURLObj.Query().Get("response_type"), "code")
	require.Equal(t, authCodeURLObj.Query().Get("state"), "test-state")

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)
	req, _ := http.NewRequest("GET", "/?state=test-state&code=test-code", nil)
	c.Request = req
	_, err = p.Exchange(c, "invalid-state")
	require.Error(t, err)
	token, err := p.Exchange(c, "test-state")
	require.NoError(t, err)
	require.NotNil(t, token)
	require.Equal(t, token.AccessToken, "test-access-token")
}

func TestGoogleProviderWithWorkspace(t *testing.T) {
	p, _ := setupGoogleTest([]string{}, []string{"example.com"})

	authCodeURL, err := p.AuthCodeURL("test-state")
	require.NoError(t, err)
	require.NotEmpty(t, authCodeURL)
	authCodeURLObj, err := url.Parse(authCodeURL)
	require.NoError(t, err)
	require.Equal(t, authCodeURLObj.Query().Get("hd"), "example.com")
}

func TestGoogleProviderAuthorization(t *testing.T) {
	tc := []struct {
		name              string
		allowedUsers      []string
		allowedWorkspaces []string
		userResp          string
		expect            bool
	}{
		{
			name:              "allow all users",
			allowedUsers:      []string{},
			allowedWorkspaces: []string{},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "user1@gmail.com"}`,
			expect:            true,
		},
		{
			name:              "allow single user",
			allowedUsers:      []string{"user1@gmail.com", "user2@gmail.com"},
			allowedWorkspaces: []string{},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "user1@gmail.com"}`,
			expect:            true,
		},
		{
			name:              "deny single user",
			allowedUsers:      []string{"user1@gmail.com"},
			allowedWorkspaces: []string{},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "user2@gmail.com"}`,
			expect:            false,
		},
		{
			name:              "allow by workspace",
			allowedUsers:      []string{},
			allowedWorkspaces: []string{"example.com"},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "user1@example.com", "hd": "example.com"}`,
			expect:            true,
		},
		{
			name:              "deny by workspace",
			allowedUsers:      []string{},
			allowedWorkspaces: []string{"example.com"},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "user1@gmail.com"}`,
			expect:            false,
		},
		{
			name:              "deny by other workspace",
			allowedUsers:      []string{},
			allowedWorkspaces: []string{"example.com"},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "test@other.com", "hd": "other.com"}`,
			expect:            false,
		},
		{
			name:              "allow user without workspace domain",
			allowedUsers:      []string{},
			allowedWorkspaces: []string{},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "test@gmail.com"}`,
			expect:            true,
		},
		{
			name:              "allow specific user with workspace",
			allowedUsers:      []string{"test@example.com"},
			allowedWorkspaces: []string{"other.com"},
			userResp:          `{"sub": "12345", "name": "Test User", "email": "test@example.com", "hd": "example.com"}`,
			expect:            true,
		},
	}

	for _, tt := range tc {
		t.Run(tt.name, func(t *testing.T) {
			p, googapi := setupGoogleTest(tt.allowedUsers, tt.allowedWorkspaces)

			googapi.GET("/userinfo", func(c *gin.Context) {
				c.Data(http.StatusOK, "application/json", []byte(tt.userResp))
			})

			ok, _, _, err := p.Authorization(context.Background(), &oauth2.Token{AccessToken: "test-access-token"})
			require.NoError(t, err)
			require.Equal(t, tt.expect, ok)
		})
	}
}

func TestGoogleProviderAuthorizationAPIError(t *testing.T) {
	p, googapi := setupGoogleTest([]string{}, []string{})

	googapi.GET("/userinfo", func(c *gin.Context) {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
	})

	ok, user, _, err := p.Authorization(context.Background(), &oauth2.Token{AccessToken: "test-access-token"})
	require.Error(t, err)
	require.False(t, ok)
	require.Empty(t, user)
	require.Contains(t, err.Error(), "failed to get user info from Google API")
}

func TestGoogleProviderAuthorizationInvalidJSON(t *testing.T) {
	p, googapi := setupGoogleTest([]string{}, []string{})

	googapi.GET("/userinfo", func(c *gin.Context) {
		c.Data(http.StatusOK, "application/json", []byte(`invalid json`))
	})

	ok, user, _, err := p.Authorization(context.Background(), &oauth2.Token{AccessToken: "test-access-token"})
	require.Error(t, err)
	require.False(t, ok)
	require.Empty(t, user)
}
