package iam

import (
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws/credentials"
	v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// AssumeRoleResponse represents the STS AssumeRole response
type AssumeRoleTestResponse struct {
	XMLName xml.Name `xml:"AssumeRoleResponse"`
	Result  struct {
		Credentials struct {
			AccessKeyId     string `xml:"AccessKeyId"`
			SecretAccessKey string `xml:"SecretAccessKey"`
			SessionToken    string `xml:"SessionToken"`
			Expiration      string `xml:"Expiration"`
		} `xml:"Credentials"`
		AssumedRoleUser struct {
			AssumedRoleId string `xml:"AssumedRoleId"`
			Arn           string `xml:"Arn"`
		} `xml:"AssumedRoleUser"`
	} `xml:"AssumeRoleResult"`
}

// TestSTSAssumeRoleValidation tests input validation for AssumeRole endpoint
func TestSTSAssumeRoleValidation(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	if !isSTSEndpointRunning(t) {
		t.Fatal("SeaweedFS STS endpoint is not running at", TestSTSEndpoint, "- please run 'make setup-all-tests' first")
	}

	// Check if AssumeRole is implemented by making a test call
	if !isAssumeRoleImplemented(t) {
		t.Fatal("AssumeRole action is not implemented in the running server - please rebuild weed binary with new code and restart the server")
	}

	t.Run("missing_role_arn_defaults_to_caller_identity", func(t *testing.T) {
		// SeaweedFS intentionally allows AssumeRole without RoleArn to support
		// S3-compatible clients that omit it. In that case the session is tied
		// to the caller's own identity (User Context assumption). See
		// handleAssumeRole in weed/s3api/s3api_sts.go.
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleSessionName": {"test-session"},
			// RoleArn is missing on purpose
		}, "test-access-key", "test-secret-key")
		require.NoError(t, err)
		defer resp.Body.Close()

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		assert.Equal(t, http.StatusOK, resp.StatusCode,
			"Should succeed without RoleArn (user-context assumption): %s", string(body))

		var assumeResp AssumeRoleTestResponse
		require.NoError(t, xml.Unmarshal(body, &assumeResp),
			"Failed to parse response: %s", string(body))
		assert.NotEmpty(t, assumeResp.Result.Credentials.AccessKeyId,
			"AccessKeyId should be issued for user-context assumption")
	})

	t.Run("missing_role_session_name", func(t *testing.T) {
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":  {"AssumeRole"},
			"Version": {"2011-06-15"},
			"RoleArn": {"arn:aws:iam::role/test-role"},
			// RoleSessionName is missing
		}, "test-access-key", "test-secret-key")
		require.NoError(t, err)
		defer resp.Body.Close()

		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"Should fail without RoleSessionName")

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		var errResp STSErrorTestResponse
		err = xml.Unmarshal(body, &errResp)
		require.NoError(t, err, "Failed to parse error response: %s", string(body))
		assert.Equal(t, "MissingParameter", errResp.Error.Code)
	})

	t.Run("unsupported_action_for_anonymous", func(t *testing.T) {
		// AssumeRole requires SigV4 authentication, anonymous requests should fail
		resp, err := callSTSAPI(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleArn":         {"arn:aws:iam::role/test-role"},
			"RoleSessionName": {"test-session"},
		})
		require.NoError(t, err)
		defer resp.Body.Close()

		// Should fail because AssumeRole requires AWS SigV4 authentication
		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"AssumeRole should require authentication")

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		t.Logf("Response for anonymous AssumeRole: status=%d, body=%s", resp.StatusCode, string(body))
	})

	t.Run("invalid_duration_too_short", func(t *testing.T) {
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleArn":         {"arn:aws:iam::role/test-role"},
			"RoleSessionName": {"test-session"},
			"DurationSeconds": {"100"}, // Less than 900 seconds minimum
		}, "test-access-key", "test-secret-key")
		require.NoError(t, err)
		defer resp.Body.Close()

		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"Should fail with DurationSeconds < 900")

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		var errResp STSErrorTestResponse
		err = xml.Unmarshal(body, &errResp)
		require.NoError(t, err, "Failed to parse error response: %s", string(body))
		assert.Equal(t, "InvalidParameterValue", errResp.Error.Code)
	})

	t.Run("invalid_duration_too_long", func(t *testing.T) {
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleArn":         {"arn:aws:iam::role/test-role"},
			"RoleSessionName": {"test-session"},
			"DurationSeconds": {"100000"}, // More than 43200 seconds maximum
		}, "test-access-key", "test-secret-key")
		require.NoError(t, err)
		defer resp.Body.Close()

		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"Should fail with DurationSeconds > 43200")

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		var errResp STSErrorTestResponse
		err = xml.Unmarshal(body, &errResp)
		require.NoError(t, err, "Failed to parse error response: %s", string(body))
		assert.Equal(t, "InvalidParameterValue", errResp.Error.Code)
	})
}

// isAssumeRoleImplemented checks if the running server supports AssumeRole
func isAssumeRoleImplemented(t *testing.T) bool {
	resp, err := callSTSAPIWithSigV4(t, url.Values{
		"Action":          {"AssumeRole"},
		"Version":         {"2011-06-15"},
		"RoleArn":         {"arn:aws:iam::role/test"},
		"RoleSessionName": {"test"},
	}, "test", "test")
	if err != nil {
		return false
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return false
	}

	// If we get "NotImplemented", the action isn't supported
	var errResp STSErrorTestResponse
	if xml.Unmarshal(body, &errResp) == nil && errResp.Error.Code == "NotImplemented" {
		return false
	}

	// If we get InvalidAction, the action isn't routed
	if errResp.Error.Code == "InvalidAction" {
		return false
	}

	return true
}

// TestSTSAssumeRoleWithValidCredentials tests AssumeRole with valid IAM credentials
// This test requires a configured IAM user in SeaweedFS
func TestSTSAssumeRoleWithValidCredentials(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	if !isSTSEndpointRunning(t) {
		t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint)
	}

	// Use test credentials from environment or fall back to defaults
	accessKey := os.Getenv("STS_TEST_ACCESS_KEY")
	if accessKey == "" {
		accessKey = "admin"
	}
	secretKey := os.Getenv("STS_TEST_SECRET_KEY")
	if secretKey == "" {
		secretKey = "admin"
	}

	t.Run("successful_assume_role", func(t *testing.T) {
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleArn":         {"arn:aws:iam::role/admin"},
			"RoleSessionName": {"integration-test-session"},
		}, accessKey, secretKey)
		require.NoError(t, err)
		defer resp.Body.Close()

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body))

		// If AssumeRole is not yet implemented, expect an error about unsupported action
		if resp.StatusCode != http.StatusOK {
			var errResp STSErrorTestResponse
			err = xml.Unmarshal(body, &errResp)
			require.NoError(t, err, "Failed to parse error response: %s", string(body))
			t.Logf("Error response: code=%s, message=%s", errResp.Error.Code, errResp.Error.Message)

			// This test will initially fail until AssumeRole is implemented
			// Once implemented, uncomment the assertions below
			// assert.Fail(t, "AssumeRole not yet implemented")
		} else {
			var stsResp AssumeRoleTestResponse
			err = xml.Unmarshal(body, &stsResp)
			require.NoError(t, err, "Failed to parse response: %s", string(body))

			creds := stsResp.Result.Credentials
			assert.NotEmpty(t, creds.AccessKeyId, "AccessKeyId should not be empty")
			assert.NotEmpty(t, creds.SecretAccessKey, "SecretAccessKey should not be empty")
			assert.NotEmpty(t, creds.SessionToken, "SessionToken should not be empty")
			assert.NotEmpty(t, creds.Expiration, "Expiration should not be empty")

			t.Logf("Successfully obtained temporary credentials: AccessKeyId=%s", creds.AccessKeyId)
		}
	})

	t.Run("with_custom_duration", func(t *testing.T) {
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleArn":         {"arn:aws:iam::role/admin"},
			"RoleSessionName": {"duration-test-session"},
			"DurationSeconds": {"3600"}, // 1 hour
		}, accessKey, secretKey)
		require.NoError(t, err)
		defer resp.Body.Close()

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body))

		// Verify DurationSeconds is accepted
		if resp.StatusCode != http.StatusOK {
			var errResp STSErrorTestResponse
			err = xml.Unmarshal(body, &errResp)
			require.NoError(t, err, "Failed to parse error response: %s", string(body))
			// Should not fail due to DurationSeconds parameter
			assert.NotContains(t, errResp.Error.Message, "DurationSeconds",
				"DurationSeconds parameter should be accepted")
		}
	})
}

// TestSTSAssumeRoleWithInvalidCredentials tests AssumeRole rejection with bad credentials
func TestSTSAssumeRoleWithInvalidCredentials(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	if !isSTSEndpointRunning(t) {
		t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint)
	}

	t.Run("invalid_access_key", func(t *testing.T) {
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleArn":         {"arn:aws:iam::role/admin"},
			"RoleSessionName": {"test-session"},
		}, "invalid-access-key", "some-secret-key")
		require.NoError(t, err)
		defer resp.Body.Close()

		// Should fail with access denied or signature mismatch
		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"Should fail with invalid access key")

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		t.Logf("Response for invalid credentials: status=%d, body=%s", resp.StatusCode, string(body))
	})

	t.Run("invalid_secret_key", func(t *testing.T) {
		resp, err := callSTSAPIWithSigV4(t, url.Values{
			"Action":          {"AssumeRole"},
			"Version":         {"2011-06-15"},
			"RoleArn":         {"arn:aws:iam::role/admin"},
			"RoleSessionName": {"test-session"},
		}, "admin", "wrong-secret-key")
		require.NoError(t, err)
		defer resp.Body.Close()

		// Should fail with signature mismatch
		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"Should fail with invalid secret key")

		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)
		t.Logf("Response for wrong secret: status=%d, body=%s", resp.StatusCode, string(body))
	})
}

// callSTSAPIWithSigV4 makes an STS API call with AWS Signature V4 authentication
func callSTSAPIWithSigV4(t *testing.T, params url.Values, accessKey, secretKey string) (*http.Response, error) {
	// Prepare request body
	body := params.Encode()

	// Create request
	req, err := http.NewRequest(http.MethodPost, TestSTSEndpoint+"/",
		strings.NewReader(body))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	// Do NOT set req.Header["Host"] — aws-sdk-go v1 v4.Signer reads the host
	// from req.URL.Host/req.Host, and a manual Host header makes the signer
	// emit `host;host` in SignedHeaders, producing a different signature than
	// the server calculates from canonical headers.

	// Sign request with AWS Signature V4 using official SDK
	creds := credentials.NewStaticCredentials(accessKey, secretKey, "")
	signer := v4.NewSigner(creds)

	// Read body for signing
	// Note: We need a ReadSeeker for the signer, or we can pass the body string/bytes to ComputeBodyHash if needed,
	// but standard Sign method takes an io.ReadSeeker for the body.
	bodyReader := strings.NewReader(body)
	_, err = signer.Sign(req, bodyReader, "sts", "us-east-1", time.Now())
	if err != nil {
		return nil, fmt.Errorf("failed to sign request: %w", err)
	}

	client := &http.Client{Timeout: 30 * time.Second}
	return client.Do(req)
}
