package oauth

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestAuthManager_StateTransitions(t *testing.T) {
	// Test the state machine transitions
	t.Run("initial state is unknown", func(t *testing.T) {
		tmpDir := t.TempDir()
		mgr, err := NewAuthManager(AuthManagerConfig{
			TokenStorageDir: tmpDir,
			FileMode:        false,
		})
		if err != nil {
			t.Fatalf("Failed to create auth manager: %v", err)
		}
		defer func() { _ = mgr.Close() }()

		if mgr.GetState() != AuthStateUnknown {
			t.Errorf("expected initial state to be Unknown, got %s", mgr.GetState())
		}
	})

	t.Run("state string representations", func(t *testing.T) {
		testCases := []struct {
			state    AuthState
			expected string
		}{
			{AuthStateUnknown, "unknown"},
			{AuthStateAuthenticated, "authenticated"},
			{AuthStatePendingAuth, "pending_auth"},
			{AuthStateError, "error"},
			{AuthState(99), "unknown"}, // Unknown value defaults to "unknown"
		}

		for _, tc := range testCases {
			if tc.state.String() != tc.expected {
				t.Errorf("expected AuthState(%d).String() = %q, got %q", tc.state, tc.expected, tc.state.String())
			}
		}
	})
}

func TestAuthManager_CheckConnection_NoToken(t *testing.T) {
	// CheckConnection no longer probes the server over HTTP.
	// Without a valid token, it returns AuthStatePendingAuth so callers
	// can attempt to connect and handle mcp-go's typed errors.
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        false,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	ctx := context.Background()
	state, err := mgr.CheckConnection(ctx, "https://example.com")

	if err != nil {
		t.Errorf("expected no error, got: %v", err)
	}
	if state != AuthStatePendingAuth {
		t.Errorf("expected AuthStatePendingAuth when no token, got %s", state)
	}
}

func TestAuthManager_CheckConnection_WithValidToken(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Pre-store a valid token
	serverURL := "https://muster.example.com"
	issuerURL := "https://dex.example.com"
	token := &StoredToken{
		AccessToken: "valid-token",
		TokenType:   "Bearer",
		Expiry:      time.Now().Add(1 * time.Hour),
		ServerURL:   serverURL,
		IssuerURL:   issuerURL,
		CreatedAt:   time.Now(),
	}

	// Store directly via the client's token store
	oauth2Token := token.ToOAuth2Token()
	err = mgr.client.tokenStore.StoreToken(serverURL, issuerURL, oauth2Token)
	if err != nil {
		t.Fatalf("Failed to store token: %v", err)
	}

	ctx := context.Background()
	state, err := mgr.CheckConnection(ctx, serverURL)

	// Should be authenticated when we have a valid token
	if state != AuthStateAuthenticated {
		t.Errorf("expected state Authenticated with valid token, got %s (err: %v)", state, err)
	}
	if err != nil {
		t.Errorf("expected no error with valid token, got: %v", err)
	}
}

func TestNormalizeServerURL(t *testing.T) {
	testCases := []struct {
		input    string
		expected string
	}{
		{"https://example.com/mcp", "https://example.com"},
		{"https://example.com/sse", "https://example.com"},
		{"https://example.com/", "https://example.com"},
		{"https://example.com", "https://example.com"},
		{"https://example.com/mcp/", "https://example.com"}, // Strips trailing slash first, then /mcp
		{"https://example.com/api/mcp", "https://example.com/api"},
		{"https://example.com/api/sse", "https://example.com/api"},
		{"http://localhost:8080/mcp", "http://localhost:8080"},
		{"http://localhost:8080/sse", "http://localhost:8080"},
	}

	for _, tc := range testCases {
		t.Run(tc.input, func(t *testing.T) {
			result := normalizeServerURL(tc.input)
			if result != tc.expected {
				t.Errorf("normalizeServerURL(%q) = %q, want %q", tc.input, result, tc.expected)
			}
		})
	}
}

func TestAuthManager_GettersAndSetters(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        false,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Test initial state
	if mgr.GetState() != AuthStateUnknown {
		t.Errorf("expected initial state Unknown, got %s", mgr.GetState())
	}

	if mgr.GetAuthChallenge() != nil {
		t.Error("expected nil auth challenge initially")
	}

	if mgr.GetAuthURL() != "" {
		t.Error("expected empty auth URL initially")
	}

	if mgr.GetLastError() != nil {
		t.Error("expected nil last error initially")
	}

	if mgr.GetServerURL() != "" {
		t.Error("expected empty server URL initially")
	}
}

func TestAuthManager_GetAccessToken_NotAuthenticated(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        false,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Try to get token when not authenticated
	_, err = mgr.GetAccessToken()
	if err == nil {
		t.Error("expected error when getting token while not authenticated")
	}

	// Try to get bearer token when not authenticated
	_, err = mgr.GetBearerToken()
	if err == nil {
		t.Error("expected error when getting bearer token while not authenticated")
	}
}

func TestAuthManager_StartAuthFlow_InvalidState(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        false,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	ctx := context.Background()

	// Try to start auth flow in Unknown state (should fail)
	_, err = mgr.StartAuthFlow(ctx)
	if err == nil {
		t.Error("expected error when starting auth flow in Unknown state")
	}
}

func TestAuthManager_ClearToken(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Store a token first via CheckConnection (to set serverURL)
	ctx := context.Background()

	// Mock a server to trigger connection check
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	_, _ = mgr.CheckConnection(ctx, server.URL)

	// Clear should work even when there's no token
	err = mgr.ClearToken()
	if err != nil {
		t.Errorf("ClearToken failed: %v", err)
	}

	// State should be Unknown after clear
	if mgr.GetState() != AuthStateUnknown {
		t.Errorf("expected state Unknown after clear, got %s", mgr.GetState())
	}

	// Try clearing with no server URL set (should be no-op)
	mgr2, _ := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        false,
	})
	defer func() { _ = mgr2.Close() }()

	err = mgr2.ClearToken()
	if err != nil {
		t.Errorf("ClearToken with no server URL should not error: %v", err)
	}
}

func TestAuthManager_WaitForAuth_NoFlowInProgress(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        false,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	ctx := context.Background()

	// Try to wait for auth when no flow is in progress
	err = mgr.WaitForAuth(ctx)
	if err == nil {
		t.Error("expected error when waiting for auth with no flow in progress")
	}
}

func TestAuthManager_HasValidTokenForEndpoint_NoToken(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Should return false when no token exists
	if mgr.HasValidTokenForEndpoint("https://muster.example.com") {
		t.Error("expected false when no token exists")
	}

	// State should remain unchanged
	if mgr.GetState() != AuthStateUnknown {
		t.Errorf("expected state to remain Unknown, got %s", mgr.GetState())
	}
}

func TestAuthManager_HasValidTokenForEndpoint_WithValidToken(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Pre-store a valid token
	serverURL := "https://muster.example.com"
	issuerURL := "https://dex.example.com"
	token := &StoredToken{
		AccessToken: "valid-token",
		TokenType:   "Bearer",
		Expiry:      time.Now().Add(1 * time.Hour),
		ServerURL:   serverURL,
		IssuerURL:   issuerURL,
		CreatedAt:   time.Now(),
	}

	// Store directly via the client's token store
	oauth2Token := token.ToOAuth2Token()
	err = mgr.client.tokenStore.StoreToken(serverURL, issuerURL, oauth2Token)
	if err != nil {
		t.Fatalf("Failed to store token: %v", err)
	}

	// Should return true when valid token exists
	if !mgr.HasValidTokenForEndpoint(serverURL) {
		t.Error("expected true when valid token exists")
	}

	// State should be updated to Authenticated
	if mgr.GetState() != AuthStateAuthenticated {
		t.Errorf("expected state Authenticated, got %s", mgr.GetState())
	}
}

func TestAuthManager_HasValidTokenForEndpoint_UpdatesFromPendingAuth(t *testing.T) {
	// Create a mock server that returns 401
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/.well-known/oauth-protected-resource" {
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]interface{}{
				"resource":              "https://example.com",
				"authorization_servers": []string{"https://oauth.example.com"},
			})
			return
		}
		w.Header().Set("WWW-Authenticate", `Bearer realm="https://oauth.example.com"`)
		w.WriteHeader(http.StatusUnauthorized)
	}))
	defer server.Close()

	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	ctx := context.Background()

	// First check connection to trigger pending auth state
	state, _ := mgr.CheckConnection(ctx, server.URL)
	if state != AuthStatePendingAuth {
		t.Skipf("Server did not trigger pending auth state, got %s", state)
	}

	// Now simulate CLI authentication by storing a token directly
	issuerURL := "https://oauth.example.com"
	token := &StoredToken{
		AccessToken: "cli-auth-token",
		TokenType:   "Bearer",
		Expiry:      time.Now().Add(1 * time.Hour),
		ServerURL:   server.URL,
		IssuerURL:   issuerURL,
		CreatedAt:   time.Now(),
	}
	oauth2Token := token.ToOAuth2Token()
	err = mgr.client.tokenStore.StoreToken(server.URL, issuerURL, oauth2Token)
	if err != nil {
		t.Fatalf("Failed to store token: %v", err)
	}

	// HasValidTokenForEndpoint should detect the token and update state
	if !mgr.HasValidTokenForEndpoint(server.URL) {
		t.Error("expected true after CLI-stored token")
	}

	// State should be updated from PendingAuth to Authenticated
	if mgr.GetState() != AuthStateAuthenticated {
		t.Errorf("expected state to be updated to Authenticated, got %s", mgr.GetState())
	}
}

func TestAuthManager_HasValidTokenForEndpoint_NormalizesURL(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Store token with normalized URL
	serverURL := "https://muster.example.com"
	issuerURL := "https://dex.example.com"
	token := &StoredToken{
		AccessToken: "valid-token",
		TokenType:   "Bearer",
		Expiry:      time.Now().Add(1 * time.Hour),
		ServerURL:   serverURL,
		IssuerURL:   issuerURL,
		CreatedAt:   time.Now(),
	}
	oauth2Token := token.ToOAuth2Token()
	err = mgr.client.tokenStore.StoreToken(serverURL, issuerURL, oauth2Token)
	if err != nil {
		t.Fatalf("Failed to store token: %v", err)
	}

	// Should find token even with /mcp suffix (URL normalization)
	if !mgr.HasValidTokenForEndpoint("https://muster.example.com/mcp") {
		t.Error("expected to find token with /mcp suffix")
	}

	// Should find token even with /sse suffix
	if !mgr.HasValidTokenForEndpoint("https://muster.example.com/sse") {
		t.Error("expected to find token with /sse suffix")
	}

	// Should find token with trailing slash
	if !mgr.HasValidTokenForEndpoint("https://muster.example.com/") {
		t.Error("expected to find token with trailing slash")
	}
}

func TestAuthManager_HasValidTokenForEndpoint_ExpiredToken(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	// Store an expired token
	serverURL := "https://muster.example.com"
	issuerURL := "https://dex.example.com"
	token := &StoredToken{
		AccessToken: "expired-token",
		TokenType:   "Bearer",
		Expiry:      time.Now().Add(-1 * time.Hour), // Expired 1 hour ago
		ServerURL:   serverURL,
		IssuerURL:   issuerURL,
		CreatedAt:   time.Now().Add(-2 * time.Hour),
	}
	oauth2Token := token.ToOAuth2Token()
	err = mgr.client.tokenStore.StoreToken(serverURL, issuerURL, oauth2Token)
	if err != nil {
		t.Fatalf("Failed to store token: %v", err)
	}

	// Should return false for expired token
	if mgr.HasValidTokenForEndpoint(serverURL) {
		t.Error("expected false for expired token")
	}

	// State should remain unchanged
	if mgr.GetState() != AuthStateUnknown {
		t.Errorf("expected state to remain Unknown, got %s", mgr.GetState())
	}
}

func TestAuthManager_GetStoredTokenForEndpoint(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        true,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	serverURL := "https://muster.example.com"
	issuerURL := "https://dex.example.com"

	t.Run("returns nil for non-existent token", func(t *testing.T) {
		token := mgr.GetStoredTokenForEndpoint("https://nonexistent.example.com")
		if token != nil {
			t.Error("expected nil for non-existent token")
		}
	})

	t.Run("returns expired token for silent re-auth hints", func(t *testing.T) {
		// Store an expired token with an ID token (used for login hints)
		expiredToken := &StoredToken{
			AccessToken:  "expired-access-token",
			RefreshToken: "expired-refresh-token",
			TokenType:    "Bearer",
			Expiry:       time.Now().Add(-1 * time.Hour), // Expired 1 hour ago
			IDToken:      "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ.signature",
			ServerURL:    serverURL,
			IssuerURL:    issuerURL,
			CreatedAt:    time.Now().Add(-25 * time.Hour),
		}
		oauth2Token := expiredToken.ToOAuth2Token()
		err := mgr.client.tokenStore.StoreToken(serverURL, issuerURL, oauth2Token)
		if err != nil {
			t.Fatalf("Failed to store token: %v", err)
		}

		// GetStoredToken should return nil for expired tokens
		if mgr.GetStoredToken() != nil { //nolint:staticcheck
			// GetStoredToken uses m.serverURL which is not set in this test
			// so it will return nil anyway - this is expected
		}

		// GetStoredTokenForEndpoint should still return the expired token
		token := mgr.GetStoredTokenForEndpoint(serverURL)
		if token == nil {
			t.Fatal("expected GetStoredTokenForEndpoint to return expired token for silent re-auth")
		}

		if token.AccessToken != expiredToken.AccessToken {
			t.Errorf("expected access token %q, got %q", expiredToken.AccessToken, token.AccessToken)
		}

		if token.IDToken != expiredToken.IDToken {
			t.Errorf("expected ID token to be preserved for login hints")
		}
	})

	t.Run("normalizes URL with /mcp suffix", func(t *testing.T) {
		// Should find token even with /mcp suffix (URL normalization)
		token := mgr.GetStoredTokenForEndpoint("https://muster.example.com/mcp")
		if token == nil {
			t.Error("expected to find token with /mcp suffix")
		}
	})

	t.Run("normalizes URL with /sse suffix", func(t *testing.T) {
		// Should find token even with /sse suffix
		token := mgr.GetStoredTokenForEndpoint("https://muster.example.com/sse")
		if token == nil {
			t.Error("expected to find token with /sse suffix")
		}
	})

	t.Run("returns valid token as well", func(t *testing.T) {
		validServerURL := "https://valid.example.com"
		validToken := &StoredToken{
			AccessToken: "valid-access-token",
			TokenType:   "Bearer",
			Expiry:      time.Now().Add(1 * time.Hour), // Valid for 1 more hour
			ServerURL:   validServerURL,
			IssuerURL:   issuerURL,
			CreatedAt:   time.Now(),
		}
		oauth2Token := validToken.ToOAuth2Token()
		err := mgr.client.tokenStore.StoreToken(validServerURL, issuerURL, oauth2Token)
		if err != nil {
			t.Fatalf("Failed to store token: %v", err)
		}

		// Should return valid tokens as well
		token := mgr.GetStoredTokenForEndpoint(validServerURL)
		if token == nil {
			t.Fatal("expected to find valid token")
		}

		if token.AccessToken != validToken.AccessToken {
			t.Errorf("expected access token %q, got %q", validToken.AccessToken, token.AccessToken)
		}
	})
}

func TestAuthFlowOptions(t *testing.T) {
	t.Run("default options", func(t *testing.T) {
		opts := &AuthFlowOptions{}
		if opts.Silent {
			t.Error("expected Silent to be false by default")
		}
		if opts.LoginHint != "" {
			t.Error("expected LoginHint to be empty by default")
		}
		if opts.IDTokenHint != "" {
			t.Error("expected IDTokenHint to be empty by default")
		}
	})

	t.Run("silent mode options", func(t *testing.T) {
		opts := &AuthFlowOptions{
			Silent:      true,
			LoginHint:   "user@example.com",
			IDTokenHint: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
		}
		if !opts.Silent {
			t.Error("expected Silent to be true")
		}
		if opts.LoginHint != "user@example.com" {
			t.Errorf("expected LoginHint to be 'user@example.com', got '%s'", opts.LoginHint)
		}
		if opts.IDTokenHint == "" {
			t.Error("expected IDTokenHint to be set")
		}
	})
}

func TestAuthManager_StartAuthFlowSilent_RequiresPendingState(t *testing.T) {
	tmpDir := t.TempDir()
	mgr, err := NewAuthManager(AuthManagerConfig{
		TokenStorageDir: tmpDir,
		FileMode:        false,
	})
	if err != nil {
		t.Fatalf("Failed to create auth manager: %v", err)
	}
	defer func() { _ = mgr.Close() }()

	ctx := context.Background()

	// Attempt to start silent auth flow in Unknown state should fail
	_, err = mgr.StartAuthFlowSilent(ctx, "user@example.com", "")
	if err == nil {
		t.Error("expected error when starting silent auth flow in Unknown state")
	}
}
