package iam

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

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

// STS API test constants
const (
	TestSTSEndpoint = "http://localhost:8333"
)

// AssumeRoleWithWebIdentityResponse represents the STS response
type AssumeRoleWithWebIdentityTestResponse struct {
	XMLName xml.Name `xml:"AssumeRoleWithWebIdentityResponse"`
	Result  struct {
		Credentials struct {
			AccessKeyId     string `xml:"AccessKeyId"`
			SecretAccessKey string `xml:"SecretAccessKey"`
			SessionToken    string `xml:"SessionToken"`
			Expiration      string `xml:"Expiration"`
		} `xml:"Credentials"`
		SubjectFromWebIdentityToken string `xml:"SubjectFromWebIdentityToken,omitempty"`
	} `xml:"AssumeRoleWithWebIdentityResult"`
}

// STSErrorResponse represents an STS error response
type STSErrorTestResponse struct {
	XMLName xml.Name `xml:"ErrorResponse"`
	Error   struct {
		Type    string `xml:"Type"`
		Code    string `xml:"Code"`
		Message string `xml:"Message"`
	} `xml:"Error"`
	RequestId string `xml:"RequestId"`
}

// TestAssumeRoleWithWebIdentityValidation tests input validation for the STS endpoint
func TestAssumeRoleWithWebIdentityValidation(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("missing_web_identity_token", func(t *testing.T) {
		resp, err := callSTSAPI(t, url.Values{
			"Action":          {"AssumeRoleWithWebIdentity"},
			"RoleArn":         {"arn:aws:iam::role/test-role"},
			"RoleSessionName": {"test-session"},
			// WebIdentityToken is missing
		})
		require.NoError(t, err)
		defer resp.Body.Close()

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

		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("missing_role_arn_invalid_jwt_still_rejected", func(t *testing.T) {
		// Missing RoleArn is no longer a fast-fail at the HTTP layer:
		// claim-based policy mode (Phase 3b) advertises RoleArn as
		// optional so the STS service can derive the assumed-role ARN
		// from the configured policy claim. Validation now happens at
		// the STS layer once the JWT is parsed. With a bogus token the
		// JWT parse fails first, so the request is still rejected —
		// just with the JWT-parse error code instead of MissingParameter.
		resp, err := callSTSAPI(t, url.Values{
			"Action":           {"AssumeRoleWithWebIdentity"},
			"WebIdentityToken": {"fake-jwt-token"},
			"RoleSessionName":  {"test-session"},
			// RoleArn omitted on purpose.
		})
		require.NoError(t, err)
		defer resp.Body.Close()

		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"Should still fail when RoleArn is missing and the JWT is invalid")

		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.NotEqual(t, "MissingParameter", errResp.Error.Code,
			"missing RoleArn alone must no longer surface as MissingParameter")
	})

	t.Run("missing_role_session_name", func(t *testing.T) {
		resp, err := callSTSAPI(t, url.Values{
			"Action":           {"AssumeRoleWithWebIdentity"},
			"WebIdentityToken": {"fake-jwt-token"},
			"RoleArn":          {"arn:aws:iam::role/test-role"},
			// RoleSessionName is missing
		})
		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("invalid_jwt_token", func(t *testing.T) {
		resp, err := callSTSAPI(t, url.Values{
			"Action":           {"AssumeRoleWithWebIdentity"},
			"WebIdentityToken": {"not-a-valid-jwt-token"},
			"RoleArn":          {"arn:aws:iam::role/test-role"},
			"RoleSessionName":  {"test-session"},
		})
		require.NoError(t, err)
		defer resp.Body.Close()

		// Should fail with AccessDenied since the JWT is invalid
		assert.NotEqual(t, http.StatusOK, resp.StatusCode,
			"Should fail with invalid JWT token")

		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.Contains(t, []string{"AccessDenied", "InvalidParameterValue"}, errResp.Error.Code)
	})
}

// TestAssumeRoleWithWebIdentityWithMockJWT tests the STS endpoint with mock JWTs
// This test requires the mock OIDC provider to be configured
func TestAssumeRoleWithWebIdentityWithMockJWT(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)
	}

	// Create a test framework to get valid JWT tokens
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	// Generate a test JWT using the framework
	testUsername := "sts-test-user"
	testRole := "readonly"

	// Try to get a token - use Keycloak if available, otherwise generate a mock JWT
	var token string
	var err error
	if framework.useKeycloak {
		token, err = framework.getKeycloakToken(testUsername)
	} else {
		// Generate a mock JWT token with 1 hour validity
		token, err = framework.generateJWTToken(testUsername, testRole, time.Hour)
	}
	if err != nil {
		t.Skipf("Unable to generate test JWT (requires mock OIDC or Keycloak): %v", err)
	}

	t.Run("valid_jwt_token", func(t *testing.T) {
		resp, err := callSTSAPI(t, url.Values{
			"Action":           {"AssumeRoleWithWebIdentity"},
			"WebIdentityToken": {token},
			"RoleArn":          {"arn:aws:iam::role/" + testRole},
			"RoleSessionName":  {"integration-test-session"},
		})
		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))

		// Note: This may still fail if the role/trust policy is not configured
		// In that case, we just verify the error is about trust policy, not token validation
		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))
			assert.NotEqual(t, "InvalidParameterValue", errResp.Error.Code,
				"Token validation should not fail - error should be about trust policy")
		} else {
			var stsResp AssumeRoleWithWebIdentityTestResponse
			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_duration_seconds", func(t *testing.T) {
		resp, err := callSTSAPI(t, url.Values{
			"Action":           {"AssumeRoleWithWebIdentity"},
			"WebIdentityToken": {token},
			"RoleArn":          {"arn:aws:iam::role/" + testRole},
			"RoleSessionName":  {"integration-test-session"},
			"DurationSeconds":  {"3600"}, // 1 hour
		})
		require.NoError(t, err)
		defer resp.Body.Close()

		// Verify the request is accepted (even if trust policy causes rejection)
		body, err := io.ReadAll(resp.Body)
		require.NoError(t, err)

		// Should not fail with InvalidParameterValue for DurationSeconds
		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))
			assert.NotContains(t, errResp.Error.Message, "DurationSeconds",
				"DurationSeconds parameter should be accepted")
		}
	})
}

// callSTSAPI is a helper to make STS API calls
func callSTSAPI(t *testing.T, params url.Values) (*http.Response, error) {
	req, err := http.NewRequest(http.MethodPost, TestSTSEndpoint+"/",
		strings.NewReader(params.Encode()))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

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

// isSTSEndpointRunning checks if SeaweedFS STS endpoint is running
func isSTSEndpointRunning(t *testing.T) bool {
	client := &http.Client{Timeout: 2 * time.Second}
	resp, err := client.Get(TestSTSEndpoint + "/status")
	if err != nil {
		return false
	}
	defer resp.Body.Close()
	return resp.StatusCode == http.StatusOK
}
