package s3api

import (
	"net/http"
	"net/url"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/seaweedfs/seaweedfs/weed/iam/sts"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)

// TestAuthorizeWithIAMSessionTokenExtraction tests that the authorizeWithIAM function
// correctly extracts session tokens from multiple sources and prioritizes them appropriately.
// This is a regression test for the bug where X-Amz-Security-Token was not being checked
// for V4 signature authentication with STS credentials.
func TestAuthorizeWithIAMSessionTokenExtraction(t *testing.T) {
	t.Run("Extracts X-SeaweedFS-Session-Token from JWT auth", func(t *testing.T) {
		req := &http.Request{
			Header: http.Header{
				"X-Seaweedfs-Session-Token": {"jwt-token-123"},
				"X-Seaweedfs-Principal":     {"arn:aws:iam::user/test"},
			},
			URL: &url.URL{},
		}

		// Extract tokens the same way authorizeWithIAM does
		sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
		principal := req.Header.Get(s3_constants.SeaweedFSPrincipalHeader)

		assert.Equal(t, "jwt-token-123", sessionToken, "Should extract JWT session token from header")
		assert.Equal(t, "arn:aws:iam::user/test", principal, "Should extract principal from header")
	})

	t.Run("Extracts X-Amz-Security-Token from V4 STS auth header", func(t *testing.T) {
		req := &http.Request{
			Header: http.Header{
				"X-Amz-Security-Token": {"sts-token-header-456"},
			},
			URL: &url.URL{},
		}

		// Extract tokens the same way authorizeWithIAM does
		sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
		principal := req.Header.Get(s3_constants.SeaweedFSPrincipalHeader)

		// If JWT token is empty, should fallback to X-Amz-Security-Token
		if sessionToken == "" {
			sessionToken = req.Header.Get("X-Amz-Security-Token")
		}

		assert.Equal(t, "sts-token-header-456", sessionToken, "Should fallback to X-Amz-Security-Token when JWT token is empty")
		assert.Empty(t, principal, "JWT principal should be empty for V4 auth")
	})

	t.Run("Extracts X-Amz-Security-Token from query parameter (presigned URL)", func(t *testing.T) {
		req := &http.Request{
			Header: http.Header{},
			URL:    &url.URL{RawQuery: "X-Amz-Security-Token=sts-token-query-789"},
		}

		// Extract tokens the same way authorizeWithIAM does
		sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
		if sessionToken == "" {
			sessionToken = req.Header.Get("X-Amz-Security-Token")
			if sessionToken == "" {
				sessionToken = req.URL.Query().Get("X-Amz-Security-Token")
			}
		}

		assert.Equal(t, "sts-token-query-789", sessionToken, "Should extract token from query parameter")
	})

	t.Run("JWT token takes precedence over X-Amz-Security-Token", func(t *testing.T) {
		req := &http.Request{
			Header: http.Header{
				"X-Seaweedfs-Session-Token": {"jwt-preferred"},
				"X-Seaweedfs-Principal":     {"arn:aws:iam::user/jwt-user"},
				"X-Amz-Security-Token":      {"sts-fallback"},
			},
			URL: &url.URL{},
		}

		// Extract tokens the same way authorizeWithIAM does
		sessionToken := req.Header.Get(s3_constants.SeaweedFSSessionTokenHeader)
		if sessionToken == "" {
			sessionToken = req.Header.Get("X-Amz-Security-Token")
		}

		assert.Equal(t, "jwt-preferred", sessionToken, "JWT token should take precedence")
	})
}

// TestSTSSessionTokenIntoCredentials verifies that STS session tokens are properly
// preserved when converting to credentials for authorization.
func TestSTSSessionTokenIntoCredentials(t *testing.T) {
	// Create a credential generator and session claims
	credGen := sts.NewCredentialGenerator()
	sessionId := "test-session-123"
	expiresAt := time.Now().Add(time.Hour)

	// Generate temporary credentials
	creds, err := credGen.GenerateTemporaryCredentials(sessionId, expiresAt)
	require.NoError(t, err, "Should generate credentials successfully")
	require.NotNil(t, creds, "Credentials should not be nil")

	// Verify all credential fields are present
	assert.NotEmpty(t, creds.AccessKeyId, "AccessKeyId should be present")
	assert.NotEmpty(t, creds.SecretAccessKey, "SecretAccessKey should be present")
	assert.NotEmpty(t, creds.SessionToken, "SessionToken should be present for STS")

	// Verify deterministic generation (same session ID produces same credentials)
	creds2, err := credGen.GenerateTemporaryCredentials(sessionId, expiresAt)
	require.NoError(t, err)

	assert.Equal(t, creds.AccessKeyId, creds2.AccessKeyId, "AccessKeyId should be deterministic")
	assert.Equal(t, creds.SecretAccessKey, creds2.SecretAccessKey, "SecretAccessKey should be deterministic")
	assert.Equal(t, creds.SessionToken, creds2.SessionToken, "SessionToken should be deterministic for same sessionId")

	// Verify different session produces different credentials
	creds3, err := credGen.GenerateTemporaryCredentials("different-session", expiresAt)
	require.NoError(t, err)

	assert.NotEqual(t, creds.AccessKeyId, creds3.AccessKeyId, "Different sessions should produce different access key IDs")
	assert.NotEqual(t, creds.SecretAccessKey, creds3.SecretAccessKey, "Different sessions should produce different secret keys")
	assert.NotEqual(t, creds.SessionToken, creds3.SessionToken, "Different sessions should produce different session tokens")
}

// TestActionConstantsForV4Auth verifies that action constants are properly available
// for use in authorization checks with V4 signature authentication.
func TestActionConstantsForV4Auth(t *testing.T) {
	// Verify that S3 action constants are available
	actions := map[string]string{
		"READ":      s3_constants.ACTION_READ,
		"WRITE":     s3_constants.ACTION_WRITE,
		"READ_ACP":  s3_constants.ACTION_READ_ACP,
		"WRITE_ACP": s3_constants.ACTION_WRITE_ACP,
		"LIST":      s3_constants.ACTION_LIST,
		"TAGGING":   s3_constants.ACTION_TAGGING,
		"ADMIN":     s3_constants.ACTION_ADMIN,
	}

	for name, action := range actions {
		assert.NotEmpty(t, action, "Action %s should not be empty", name)
	}
}
