package auth_test

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v5"
	v0auth "github.com/modelcontextprotocol/registry/internal/api/handlers/v0/auth"
	"github.com/modelcontextprotocol/registry/internal/auth"
	"github.com/modelcontextprotocol/registry/internal/config"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	githubUserEndpoint = "/user"
	githubOrgsEndpoint = "/users/testuser/orgs"
)

func TestGitHubHandler_ExchangeToken(t *testing.T) {
	// Create test handler with mock config
	testSeed := make([]byte, ed25519.SeedSize)
	_, err := rand.Read(testSeed)
	require.NoError(t, err)

	cfg := &config.Config{
		JWTPrivateKey: hex.EncodeToString(testSeed),
	}

	t.Run("successful token exchange with user only", func(t *testing.T) {
		// Create mock GitHub API server
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Verify authorization header
			authHeader := r.Header.Get("Authorization")
			assert.Equal(t, "Bearer valid-github-token", authHeader)

			switch r.URL.Path {
			case githubUserEndpoint:
				user := v0auth.GitHubUserOrOrg{
					Login: "testuser",
					ID:    12345,
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(user) //nolint:errcheck
			case githubOrgsEndpoint:
				orgs := []v0auth.GitHubUserOrOrg{}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(orgs) //nolint:errcheck
			default:
				w.WriteHeader(http.StatusNotFound)
			}
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "valid-github-token")

		require.NoError(t, err)
		assert.NotNil(t, response)
		assert.NotEmpty(t, response.RegistryToken)
		assert.Greater(t, response.ExpiresAt, 0)

		// Validate the JWT token
		jwtManager := auth.NewJWTManager(cfg)
		claims, err := jwtManager.ValidateToken(ctx, response.RegistryToken)
		require.NoError(t, err)
		assert.Equal(t, auth.MethodGitHubAT, claims.AuthMethod)
		assert.Equal(t, "testuser", claims.AuthMethodSubject)
		assert.Len(t, claims.Permissions, 1)
		assert.Equal(t, auth.PermissionActionPublish, claims.Permissions[0].Action)
		assert.Equal(t, "io.github.testuser/*", claims.Permissions[0].ResourcePattern)
	})

	t.Run("successful token exchange with organizations", func(t *testing.T) {
		// Create mock GitHub API server
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.URL.Path {
			case githubUserEndpoint:
				user := v0auth.GitHubUserOrOrg{
					Login: "testuser",
					ID:    12345,
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(user) //nolint:errcheck
			case githubOrgsEndpoint:
				orgs := []v0auth.GitHubUserOrOrg{
					{Login: "test-org-1", ID: 1},
					{Login: "test-org-2", ID: 2},
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(orgs) //nolint:errcheck
			default:
				w.WriteHeader(http.StatusNotFound)
			}
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "valid-github-token")

		require.NoError(t, err)
		assert.NotNil(t, response)

		// Validate the JWT token
		jwtManager := auth.NewJWTManager(cfg)
		claims, err := jwtManager.ValidateToken(ctx, response.RegistryToken)
		require.NoError(t, err)
		assert.Equal(t, "testuser", claims.AuthMethodSubject)
		assert.Len(t, claims.Permissions, 3) // User + 2 orgs

		// Check permissions
		expectedPatterns := []string{
			"io.github.testuser/*",
			"io.github.test-org-1/*",
			"io.github.test-org-2/*",
		}
		for i, perm := range claims.Permissions {
			assert.Equal(t, auth.PermissionActionPublish, perm.Action)
			assert.Equal(t, expectedPatterns[i], perm.ResourcePattern)
		}
	})

	t.Run("invalid token returns error", func(t *testing.T) {
		// Create mock GitHub API server that returns 401
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusUnauthorized)
			w.Write([]byte(`{"message": "Bad credentials"}`)) //nolint:errcheck
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "invalid-token")

		require.Error(t, err)
		assert.Nil(t, response)
		assert.Contains(t, err.Error(), "GitHub API error")
		assert.Contains(t, err.Error(), "401")
	})

	t.Run("GitHub API error on user fetch", func(t *testing.T) {
		// Create mock GitHub API server that returns 500
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.URL.Path == githubUserEndpoint {
				w.WriteHeader(http.StatusInternalServerError)
				w.Write([]byte(`{"message": "Internal server error"}`)) //nolint:errcheck
			}
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "valid-token")

		require.Error(t, err)
		assert.Nil(t, response)
		assert.Contains(t, err.Error(), "failed to get GitHub user")
	})

	t.Run("GitHub API error on orgs fetch", func(t *testing.T) {
		// Create mock GitHub API server
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.URL.Path {
			case githubUserEndpoint:
				user := v0auth.GitHubUserOrOrg{
					Login: "testuser",
					ID:    12345,
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(user) //nolint:errcheck
			case githubOrgsEndpoint:
				w.WriteHeader(http.StatusInternalServerError)
				w.Write([]byte(`{"message": "Internal server error"}`)) //nolint:errcheck
			}
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "valid-token")

		require.Error(t, err)
		assert.Nil(t, response)
		assert.Contains(t, err.Error(), "failed to get GitHub organizations")
	})

	t.Run("invalid GitHub username returns empty permissions", func(t *testing.T) {
		// Create mock GitHub API server with invalid username
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.URL.Path {
			case githubUserEndpoint:
				user := v0auth.GitHubUserOrOrg{
					Login: "user with spaces", // Invalid name
					ID:    12345,
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(user) //nolint:errcheck
			case "/users/user with spaces/orgs":
				orgs := []v0auth.GitHubUserOrOrg{}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(orgs) //nolint:errcheck
			}
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "valid-token")

		require.NoError(t, err)
		assert.NotNil(t, response)

		// Validate the JWT token
		jwtManager := auth.NewJWTManager(cfg)
		claims, err := jwtManager.ValidateToken(ctx, response.RegistryToken)
		require.NoError(t, err)
		assert.Equal(t, "user with spaces", claims.AuthMethodSubject)
		assert.Empty(t, claims.Permissions) // No permissions due to invalid name
	})

	t.Run("invalid org name is filtered out", func(t *testing.T) {
		// Create mock GitHub API server with invalid org name
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.URL.Path {
			case githubUserEndpoint:
				user := v0auth.GitHubUserOrOrg{
					Login: "testuser",
					ID:    12345,
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(user) //nolint:errcheck
			case githubOrgsEndpoint:
				orgs := []v0auth.GitHubUserOrOrg{
					{Login: "valid-org", ID: 1},
					{Login: "org with spaces", ID: 2}, // Invalid name
				}
				w.Header().Set("Content-Type", "application/json")
				json.NewEncoder(w).Encode(orgs) //nolint:errcheck
			}
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "valid-token")

		require.NoError(t, err)
		assert.NotNil(t, response)

		// Validate the JWT token
		jwtManager := auth.NewJWTManager(cfg)
		claims, err := jwtManager.ValidateToken(ctx, response.RegistryToken)
		require.NoError(t, err)
		assert.Equal(t, "testuser", claims.AuthMethodSubject)
		assert.Empty(t, claims.Permissions) // No permissions because one org has invalid name
	})

	t.Run("malformed JSON response", func(t *testing.T) {
		// Create mock GitHub API server that returns invalid JSON
		mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.URL.Path == githubUserEndpoint {
				w.Header().Set("Content-Type", "application/json")
				w.Write([]byte(`{invalid json`)) //nolint:errcheck
			}
		}))
		defer mockServer.Close()

		// Create handler and set mock server URL
		handler := v0auth.NewGitHubHandler(cfg)
		handler.SetBaseURL(mockServer.URL)

		// Test token exchange
		ctx := context.Background()
		response, err := handler.ExchangeToken(ctx, "valid-token")

		require.Error(t, err)
		assert.Nil(t, response)
		assert.Contains(t, err.Error(), "failed to decode")
	})
}

func TestJWTTokenValidation(t *testing.T) {
	testSeed := make([]byte, ed25519.SeedSize)
	_, err := rand.Read(testSeed)
	require.NoError(t, err)

	cfg := &config.Config{
		JWTPrivateKey: hex.EncodeToString(testSeed),
	}

	jwtManager := auth.NewJWTManager(cfg)
	ctx := context.Background()

	t.Run("generate and validate token", func(t *testing.T) {
		// Create test claims
		claims := auth.JWTClaims{
			AuthMethod:        auth.MethodGitHubAT,
			AuthMethodSubject: "testuser",
			Permissions: []auth.Permission{
				{
					Action:          auth.PermissionActionPublish,
					ResourcePattern: "io.github.testuser/*",
				},
			},
		}

		// Generate token
		tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
		require.NoError(t, err)
		assert.NotEmpty(t, tokenResponse.RegistryToken)

		// Validate token
		validatedClaims, err := jwtManager.ValidateToken(ctx, tokenResponse.RegistryToken)
		require.NoError(t, err)
		assert.Equal(t, auth.MethodGitHubAT, validatedClaims.AuthMethod)
		assert.Equal(t, "testuser", validatedClaims.AuthMethodSubject)
		assert.Len(t, validatedClaims.Permissions, 1)
	})

	t.Run("token expiration", func(t *testing.T) {
		// Create claims with past expiration
		pastTime := time.Now().Add(-1 * time.Hour)
		claims := auth.JWTClaims{
			AuthMethod:        auth.MethodGitHubAT,
			AuthMethodSubject: "testuser",
			RegisteredClaims: jwt.RegisteredClaims{
				ExpiresAt: jwt.NewNumericDate(pastTime),
				IssuedAt:  jwt.NewNumericDate(pastTime.Add(-1 * time.Hour)),
			},
		}

		// Generate token
		tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
		require.NoError(t, err)

		// Validate token - should fail due to expiration
		_, err = jwtManager.ValidateToken(ctx, tokenResponse.RegistryToken)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "token is expired")
	})

	t.Run("invalid signature", func(t *testing.T) {
		// Create test claims
		claims := auth.JWTClaims{
			AuthMethod:        auth.MethodGitHubAT,
			AuthMethodSubject: "testuser",
		}

		// Generate token
		tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
		require.NoError(t, err)

		// Tamper with the token
		tamperedToken := tokenResponse.RegistryToken + "tampered"

		// Validate token - should fail due to invalid signature
		_, err = jwtManager.ValidateToken(ctx, tamperedToken)
		require.Error(t, err)
	})
}

func TestPermissionResourceMatching(t *testing.T) {
	testSeed := make([]byte, ed25519.SeedSize)
	_, err := rand.Read(testSeed)
	require.NoError(t, err)

	cfg := &config.Config{
		JWTPrivateKey: hex.EncodeToString(testSeed),
	}

	jwtManager := auth.NewJWTManager(cfg)

	testCases := []struct {
		name          string
		resource      string
		pattern       string
		action        auth.PermissionAction
		expectedMatch bool
	}{
		{
			name:          "exact match",
			resource:      "io.github.testuser/myrepo",
			pattern:       "io.github.testuser/myrepo",
			action:        auth.PermissionActionPublish,
			expectedMatch: true,
		},
		{
			name:          "wildcard match",
			resource:      "io.github.testuser/myrepo",
			pattern:       "io.github.testuser/*",
			action:        auth.PermissionActionPublish,
			expectedMatch: true,
		},
		{
			name:          "global wildcard",
			resource:      "io.github.anyuser/anyrepo",
			pattern:       "*",
			action:        auth.PermissionActionPublish,
			expectedMatch: true,
		},
		{
			name:          "no match different user",
			resource:      "io.github.otheruser/repo",
			pattern:       "io.github.testuser/*",
			action:        auth.PermissionActionPublish,
			expectedMatch: false,
		},
		{
			name:          "no match different action",
			resource:      "io.github.testuser/repo",
			pattern:       "io.github.testuser/*",
			action:        auth.PermissionActionEdit,
			expectedMatch: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			permissions := []auth.Permission{
				{
					Action:          auth.PermissionActionPublish,
					ResourcePattern: tc.pattern,
				},
			}

			hasPermission := jwtManager.HasPermission(tc.resource, tc.action, permissions)
			assert.Equal(t, tc.expectedMatch, hasPermission)
		})
	}
}

func TestValidGitHubNames(t *testing.T) {
	// Create a minimal handler to test name validation
	testSeed := make([]byte, ed25519.SeedSize)
	_, err := rand.Read(testSeed)
	require.NoError(t, err)

	cfg := &config.Config{
		JWTPrivateKey: hex.EncodeToString(testSeed),
	}

	validNameTests := []struct {
		name      string
		username  string
		orgs      []v0auth.GitHubUserOrOrg
		wantPerms int
	}{
		{
			name:      "valid username only",
			username:  "valid-user",
			orgs:      []v0auth.GitHubUserOrOrg{},
			wantPerms: 1,
		},
		{
			name:      "valid username with numbers",
			username:  "user123",
			orgs:      []v0auth.GitHubUserOrOrg{},
			wantPerms: 1,
		},
		{
			name:     "valid username with org",
			username: "valid-user",
			orgs: []v0auth.GitHubUserOrOrg{
				{Login: "valid-org", ID: 1},
			},
			wantPerms: 2,
		},
		{
			name:      "invalid username with spaces",
			username:  "invalid user",
			orgs:      []v0auth.GitHubUserOrOrg{},
			wantPerms: 0, // Should return nil/empty permissions
		},
		{
			name:      "invalid username with special chars",
			username:  "user@invalid",
			orgs:      []v0auth.GitHubUserOrOrg{},
			wantPerms: 0,
		},
		{
			name:     "valid username with invalid org",
			username: "valid-user",
			orgs: []v0auth.GitHubUserOrOrg{
				{Login: "invalid org", ID: 1},
			},
			wantPerms: 0, // Should return nil if any name is invalid
		},
	}

	for _, tc := range validNameTests {
		t.Run(tc.name, func(t *testing.T) {
			// Create mock server
			mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				switch r.URL.Path {
				case "/user":
					user := v0auth.GitHubUserOrOrg{
						Login: tc.username,
						ID:    12345,
					}
					w.Header().Set("Content-Type", "application/json")
					json.NewEncoder(w).Encode(user) //nolint:errcheck
				case "/users/" + tc.username + "/orgs":
					w.Header().Set("Content-Type", "application/json")
					json.NewEncoder(w).Encode(tc.orgs) //nolint:errcheck
				}
			}))
			defer mockServer.Close()

			// Create handler and set mock server URL
			handler := v0auth.NewGitHubHandler(cfg)
			handler.SetBaseURL(mockServer.URL)

			// Test token exchange
			ctx := context.Background()
			response, err := handler.ExchangeToken(ctx, "valid-token")
			require.NoError(t, err)

			// Validate the JWT token and check permissions
			jwtManager := auth.NewJWTManager(cfg)
			claims, err := jwtManager.ValidateToken(ctx, response.RegistryToken)
			require.NoError(t, err)
			assert.Len(t, claims.Permissions, tc.wantPerms)
		})
	}
}

func TestGitHubHandler_Creation(t *testing.T) {
	testSeed := make([]byte, ed25519.SeedSize)
	_, err := rand.Read(testSeed)
	require.NoError(t, err)

	cfg := &config.Config{
		JWTPrivateKey: hex.EncodeToString(testSeed),
	}

	handler := v0auth.NewGitHubHandler(cfg)
	assert.NotNil(t, handler, "handler should not be nil")
}

func TestConcurrentTokenExchange(t *testing.T) {
	// Test that the handler is thread-safe
	testSeed := make([]byte, ed25519.SeedSize)
	_, err := rand.Read(testSeed)
	require.NoError(t, err)

	cfg := &config.Config{
		JWTPrivateKey: hex.EncodeToString(testSeed),
	}

	// Create mock GitHub API server
	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/user":
			user := v0auth.GitHubUserOrOrg{
				Login: "testuser",
				ID:    12345,
			}
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(user) //nolint:errcheck
		case githubOrgsEndpoint:
			orgs := []v0auth.GitHubUserOrOrg{}
			w.Header().Set("Content-Type", "application/json")
			json.NewEncoder(w).Encode(orgs) //nolint:errcheck
		}
	}))
	defer mockServer.Close()

	handler := v0auth.NewGitHubHandler(cfg)
	handler.SetBaseURL(mockServer.URL)

	// Run multiple concurrent exchanges
	concurrency := 10
	errors := make(chan error, concurrency)

	for i := 0; i < concurrency; i++ {
		go func() {
			ctx := context.Background()
			_, err := handler.ExchangeToken(ctx, fmt.Sprintf("token-%d", i))
			errors <- err
		}()
	}

	// Collect results
	for i := 0; i < concurrency; i++ {
		err := <-errors
		assert.NoError(t, err)
	}
}
