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 (
	TestOIDCClientID     = "test-oidc-client-id"
	TestOIDCClientSecret = "test-oidc-client-secret"
	TestOIDCExternalURL  = "http://localhost:8080"
	TestOIDCProviderName = "TestOIDC"
	TestOIDCUserIDField  = "/sub"
)

func setupOIDCTest(allowedUsers []string, userIDField string) (Provider, gin.IRoutes, gin.IRoutes, *httptest.Server) {
	// Setup OIDC configuration server
	configServer := gin.New()
	configServer.GET("/.well-known/openid_configuration", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"authorization_endpoint": "http://localhost:8080/auth",
			"token_endpoint":         "http://localhost:8080/token",
			"userinfo_endpoint":      "http://localhost:8080/userinfo",
		})
	})
	tsConfig := httptest.NewServer(configServer)

	if userIDField == "" {
		userIDField = TestOIDCUserIDField
	}

	p, err := NewOIDCProvider(
		tsConfig.URL+"/.well-known/openid_configuration",
		[]string{"openid", "profile"},
		userIDField,
		TestOIDCProviderName,
		TestOIDCExternalURL,
		TestOIDCClientID,
		TestOIDCClientSecret,
		allowedUsers,
		[]string{},
		nil,
		nil,
	)
	if err != nil {
		panic(err)
	}

	// Setup OAuth2 token endpoint
	oauth := gin.New()
	oauth.POST("/token", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"access_token": "test-access-token",
			"token_type":   "Bearer",
		})
	})
	tsOAuth := httptest.NewServer(oauth)

	// Setup userinfo endpoint
	userinfo := gin.New()
	tsUserinfo := httptest.NewServer(userinfo)

	// Override endpoints in provider
	op := p.(*oidcProvider)
	op.oauth2.Endpoint = oauth2.Endpoint{
		AuthURL:  tsOAuth.URL + "/auth",
		TokenURL: tsOAuth.URL + "/token",
	}
	op.userInfoURL = tsUserinfo.URL + "/userinfo"

	return p, oauth, userinfo, tsConfig
}

func TestOIDCProvider(t *testing.T) {
	p, _, _, tsConfig := setupOIDCTest([]string{}, "")
	defer tsConfig.Close()

	require.Equal(t, p.Name(), TestOIDCProviderName)
	require.Equal(t, p.Type(), "oidc")
	require.Equal(t, p.RedirectURL(), OIDCCallbackEndpoint)
	require.Equal(t, p.AuthURL(), OIDCAuthEndpoint)

	// Check AuthCodeURL
	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("client_id"), TestOIDCClientID)
	require.Equal(t, authCodeURLObj.Query().Get("redirect_uri"), TestOIDCExternalURL+"/.auth/oidc/callback")
	require.Equal(t, authCodeURLObj.Query().Get("response_type"), "code")
	require.Equal(t, authCodeURLObj.Query().Get("state"), "test-state")
	require.Equal(t, "test-state", authCodeURLObj.Query().Get("nonce"))
	require.Contains(t, authCodeURLObj.Query().Get("scope"), "openid")

	// Check Exchange
	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 TestOIDCProviderAuthorization(t *testing.T) {
	tc := []struct {
		name         string
		allowedUsers []string
		userIDField  string
		userResp     string
		expect       bool
	}{
		{
			name:         "allow all users",
			allowedUsers: []string{},
			userIDField:  "/sub",
			userResp:     `{"sub": "user1", "name": "Test User"}`,
			expect:       true,
		},
		{
			name:         "allow single user",
			allowedUsers: []string{"user1", "user2"},
			userIDField:  "/sub",
			userResp:     `{"sub": "user1", "name": "Test User"}`,
			expect:       true,
		},
		{
			name:         "deny single user",
			allowedUsers: []string{"user1"},
			userIDField:  "/sub",
			userResp:     `{"sub": "user2", "name": "Test User"}`,
			expect:       false,
		},
		{
			name:         "custom user ID field",
			allowedUsers: []string{"test@example.com"},
			userIDField:  "/email",
			userResp:     `{"sub": "user1", "email": "test@example.com", "name": "Test User"}`,
			expect:       true,
		},
		{
			name:         "nested user ID field",
			allowedUsers: []string{"user1"},
			userIDField:  "/profile/username",
			userResp:     `{"sub": "123", "profile": {"username": "user1", "display_name": "Test User"}}`,
			expect:       true,
		},
	}

	for _, tt := range tc {
		t.Run(tt.name, func(t *testing.T) {
			p, _, userinfo, tsConfig := setupOIDCTest(tt.allowedUsers, tt.userIDField)
			defer tsConfig.Close()

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

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

func TestOIDCProviderErrors(t *testing.T) {
	t.Run("invalid configuration URL", func(t *testing.T) {
		_, err := NewOIDCProvider(
			"http://invalid-url/.well-known/openid_configuration",
			[]string{"openid"},
			"/sub",
			"TestOIDC",
			TestOIDCExternalURL,
			TestOIDCClientID,
			TestOIDCClientSecret,
			[]string{},
			[]string{},
			nil,
			nil,
		)
		require.Error(t, err)
	})

	t.Run("invalid JSON in configuration", func(t *testing.T) {
		configServer := gin.New()
		configServer.GET("/.well-known/openid_configuration", func(c *gin.Context) {
			c.String(http.StatusOK, "invalid json")
		})
		tsConfig := httptest.NewServer(configServer)
		defer tsConfig.Close()

		_, err := NewOIDCProvider(
			tsConfig.URL+"/.well-known/openid_configuration",
			[]string{"openid"},
			"/sub",
			"TestOIDC",
			TestOIDCExternalURL,
			TestOIDCClientID,
			TestOIDCClientSecret,
			[]string{},
			[]string{},
			nil,
			nil,
		)
		require.Error(t, err)
	})

	t.Run("configuration endpoint error", func(t *testing.T) {
		configServer := gin.New()
		configServer.GET("/.well-known/openid_configuration", func(c *gin.Context) {
			c.JSON(http.StatusBadGateway, gin.H{"error": "server error"})
		})
		tsConfig := httptest.NewServer(configServer)
		defer tsConfig.Close()

		_, err := NewOIDCProvider(
			tsConfig.URL+"/.well-known/openid_configuration",
			[]string{"openid"},
			"/sub",
			"TestOIDC",
			TestOIDCExternalURL,
			TestOIDCClientID,
			TestOIDCClientSecret,
			[]string{},
			[]string{},
			nil,
			nil,
		)
		require.Error(t, err)
		require.Contains(t, err.Error(), "502 Bad Gateway")
	})

	t.Run("missing user ID field", func(t *testing.T) {
		p, _, userinfo, tsConfig := setupOIDCTest([]string{}, "/missing_field")
		defer tsConfig.Close()

		userinfo.GET("/userinfo", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"sub": "user1"})
		})

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

	t.Run("non-string user ID field", func(t *testing.T) {
		p, _, userinfo, tsConfig := setupOIDCTest([]string{}, "/sub")
		defer tsConfig.Close()

		userinfo.GET("/userinfo", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"sub": 12345})
		})

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

	t.Run("userinfo endpoint error", func(t *testing.T) {
		p, _, userinfo, tsConfig := setupOIDCTest([]string{}, "/sub")
		defer tsConfig.Close()

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

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

func TestOIDCProviderGlobPatterns(t *testing.T) {
	// Setup test server with OIDC configuration
	configServer := gin.New()
	configServer.GET("/.well-known/openid_configuration", func(c *gin.Context) {
		c.JSON(200, map[string]interface{}{
			"authorization_endpoint": "http://localhost/auth",
			"token_endpoint":         "http://localhost/token",
			"userinfo_endpoint":      "http://localhost/userinfo",
		})
	})
	tsConfig := httptest.NewServer(configServer)
	defer tsConfig.Close()

	// Create provider with glob patterns
	p, err := NewOIDCProvider(
		tsConfig.URL+"/.well-known/openid_configuration",
		[]string{"openid", "profile"},
		"/email",
		"TestOIDC",
		TestOIDCExternalURL,
		TestOIDCClientID,
		TestOIDCClientSecret,
		[]string{}, // no exact matches
		[]string{"*@example.com", "admin.*@company.*"},
		nil,
		nil,
	)
	require.NoError(t, err)

	// Test glob matching
	testCases := []struct {
		email    string
		expected bool
	}{
		{"user@example.com", true},         // matches *@example.com
		{"test@example.com", true},         // matches *@example.com
		{"admin.user@company.org", true},   // matches admin.*@company.*
		{"admin.test@company.co.uk", true}, // matches admin.*@company.*
		{"user@other.com", false},          // no match
		{"regular@company.org", false},     // no match (not admin.*)
		{"admin@example.com", true},        // matches *@example.com
	}

	for _, tc := range testCases {
		t.Run(tc.email, func(t *testing.T) {
			// Mock userinfo endpoint
			userServer := gin.New()
			userServer.GET("/userinfo", func(c *gin.Context) {
				c.JSON(200, map[string]interface{}{
					"email": tc.email,
				})
			})
			tsUser := httptest.NewServer(userServer)
			defer tsUser.Close()

			// Update provider's userinfo URL for this test
			provider := p.(*oidcProvider)
			provider.userInfoURL = tsUser.URL + "/userinfo"

			// Test authorization
			authorized, userID, _, err := provider.Authorization(context.Background(), &oauth2.Token{AccessToken: "test"})
			require.NoError(t, err)
			require.Equal(t, tc.email, userID)
			require.Equal(t, tc.expected, authorized, "Expected %v for email %s", tc.expected, tc.email)
		})
	}
}

func TestOIDCProviderAttributeMatching(t *testing.T) {
	// Setup test server with OIDC configuration
	configServer := gin.New()
	configServer.GET("/.well-known/openid_configuration", func(c *gin.Context) {
		c.JSON(200, map[string]interface{}{
			"authorization_endpoint": "http://localhost/auth",
			"token_endpoint":         "http://localhost/token",
			"userinfo_endpoint":      "http://localhost/userinfo",
		})
	})
	tsConfig := httptest.NewServer(configServer)
	defer tsConfig.Close()

	testCases := []struct {
		name                  string
		allowedAttributes     map[string][]string
		allowedAttributesGlob map[string][]string
		userInfo              map[string]interface{}
		expected              bool
	}{
		{
			name:              "allow all when no restrictions",
			allowedAttributes: nil,
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com"},
			expected:          true,
		},
		{
			name:              "exact match on string attribute",
			allowedAttributes: map[string][]string{"/department": {"engineering"}},
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com", "department": "engineering"},
			expected:          true,
		},
		{
			name:              "no match on string attribute",
			allowedAttributes: map[string][]string{"/department": {"engineering"}},
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com", "department": "marketing"},
			expected:          false,
		},
		{
			name:              "match on array attribute",
			allowedAttributes: map[string][]string{"/groups": {"admin"}},
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com", "groups": []interface{}{"users", "admin"}},
			expected:          true,
		},
		{
			name:              "no match on array attribute",
			allowedAttributes: map[string][]string{"/groups": {"admin"}},
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com", "groups": []interface{}{"users", "developers"}},
			expected:          false,
		},
		{
			name:              "multiple allowed values - match",
			allowedAttributes: map[string][]string{"/role": {"admin", "moderator"}},
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com", "role": "moderator"},
			expected:          true,
		},
		{
			name:              "nested attribute match",
			allowedAttributes: map[string][]string{"/org/team": {"platform"}},
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com", "org": map[string]interface{}{"team": "platform"}},
			expected:          true,
		},
		{
			name:                  "glob pattern match on string",
			allowedAttributesGlob: map[string][]string{"/department": {"eng*"}},
			userInfo:              map[string]interface{}{"sub": "user1", "email": "user@example.com", "department": "engineering"},
			expected:              true,
		},
		{
			name:                  "glob pattern no match on string",
			allowedAttributesGlob: map[string][]string{"/department": {"eng*"}},
			userInfo:              map[string]interface{}{"sub": "user1", "email": "user@example.com", "department": "marketing"},
			expected:              false,
		},
		{
			name:                  "glob pattern match on array",
			allowedAttributesGlob: map[string][]string{"/groups": {"*-admins"}},
			userInfo:              map[string]interface{}{"sub": "user1", "email": "user@example.com", "groups": []interface{}{"users", "platform-admins"}},
			expected:              true,
		},
		{
			name:                  "glob pattern no match on array",
			allowedAttributesGlob: map[string][]string{"/groups": {"*-admins"}},
			userInfo:              map[string]interface{}{"sub": "user1", "email": "user@example.com", "groups": []interface{}{"users", "developers"}},
			expected:              false,
		},
		{
			name:              "missing attribute - no match",
			allowedAttributes: map[string][]string{"/department": {"engineering"}},
			userInfo:          map[string]interface{}{"sub": "user1", "email": "user@example.com"},
			expected:          false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Create provider with attribute restrictions
			p, err := NewOIDCProvider(
				tsConfig.URL+"/.well-known/openid_configuration",
				[]string{"openid", "profile"},
				"/sub",
				"TestOIDC",
				TestOIDCExternalURL,
				TestOIDCClientID,
				TestOIDCClientSecret,
				[]string{}, // no user restrictions
				[]string{}, // no user glob restrictions
				tc.allowedAttributes,
				tc.allowedAttributesGlob,
			)
			require.NoError(t, err)

			// Mock userinfo endpoint
			userServer := gin.New()
			userServer.GET("/userinfo", func(c *gin.Context) {
				c.JSON(200, tc.userInfo)
			})
			tsUser := httptest.NewServer(userServer)
			defer tsUser.Close()

			// Update provider's userinfo URL for this test
			provider := p.(*oidcProvider)
			provider.userInfoURL = tsUser.URL + "/userinfo"

			// Test authorization
			authorized, _, _, err := provider.Authorization(context.Background(), &oauth2.Token{AccessToken: "test"})
			require.NoError(t, err)
			require.Equal(t, tc.expected, authorized, "Expected %v for test case %s", tc.expected, tc.name)
		})
	}
}
