package s3api

import (
	"context"
	"encoding/xml"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

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

	"github.com/seaweedfs/seaweedfs/weed/iam/integration"
	"github.com/seaweedfs/seaweedfs/weed/iam/policy"
	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)

// An admin that assumes a named role gets the role's permissions, not its own:
// the is_admin claim bypasses base policy evaluation, so carrying it into a
// role session would silently ignore the role's attached policies.
func TestAssumeRole_AdminCallerScopedToRolePolicies(t *testing.T) {
	ctx := context.Background()
	manager := newTestSTSIntegrationManager(t)
	manager.SetSessionRevocationStore(integration.NewMemorySessionRevocationStore())

	require.NoError(t, manager.CreatePolicy(ctx, "", "ReadOnlyPolicy", &policy.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy.Statement{{
			Effect:   "Allow",
			Action:   []string{"s3:GetObject"},
			Resource: []string{"arn:aws:s3:::*/*"},
		}},
	}))
	require.NoError(t, manager.CreateRole(ctx, "", "ReadOnlyRole", &integration.RoleDefinition{
		RoleName: "ReadOnlyRole",
		TrustPolicy: &policy.PolicyDocument{
			Version:   "2012-10-17",
			Statement: []policy.Statement{{Effect: "Allow", Principal: "*", Action: []string{"sts:AssumeRole"}}},
		},
		AttachedPolicies: []string{"ReadOnlyPolicy"},
	}))

	const accessKey, secretKey = "adminkey", "adminsecret"
	iam := &IdentityAccessManagement{iamIntegration: NewS3IAMIntegration(manager, "")}
	require.NoError(t, iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{{
			Name:        "admin",
			Credentials: []*iam_pb.Credential{{AccessKey: accessKey, SecretKey: secretKey}},
			Actions:     []string{"Admin"},
		}},
	}))

	body := url.Values{
		"Action":          {"AssumeRole"},
		"Version":         {"2011-06-15"},
		"RoleArn":         {"arn:aws:iam::" + defaultAccountID + ":role/ReadOnlyRole"},
		"RoleSessionName": {"admin-session"},
	}.Encode()
	req, err := newTestRequest(http.MethodPost, "http://sts.seaweedfs.test/", int64(len(body)), strings.NewReader(body))
	require.NoError(t, err)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	require.NoError(t, signRequestV4(req, accessKey, secretKey))

	rec := httptest.NewRecorder()
	NewSTSHandlers(manager.GetSTSService(), iam).handleAssumeRole(rec, req)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())

	var resp AssumeRoleResponse
	require.NoError(t, xml.Unmarshal(rec.Body.Bytes(), &resp))
	sessionToken := resp.Result.Credentials.SessionToken
	require.NotEmpty(t, sessionToken)

	session, err := manager.GetSTSService().ValidateSessionToken(ctx, sessionToken)
	require.NoError(t, err)
	assert.NotContains(t, session.RequestContext, "is_admin", "a role session must not inherit the caller's admin standing")

	allowed := func(action, resource string) bool {
		t.Helper()
		ok, err := manager.IsActionAllowed(ctx, &integration.ActionRequest{
			Principal:      session.Principal,
			Action:         action,
			Resource:       resource,
			SessionToken:   sessionToken,
			RequestContext: session.RequestContext,
			PolicyNames:    session.Policies,
		})
		require.NoError(t, err)
		return ok
	}

	assert.True(t, allowed("s3:GetObject", "arn:aws:s3:::bucket/key"), "the role's own policy still applies")
	assert.False(t, allowed("s3:DeleteBucket", "arn:aws:s3:::bucket"), "the role's policy bounds the session")
}

// Assuming a session for oneself is the one path that keeps the caller's admin
// standing, and it has to name the caller even when the identity carries no
// principal ARN of its own — otherwise the session ARN has an empty role name.
func TestAssumeRole_SelfAssumptionWithoutPrincipalArn(t *testing.T) {
	ctx := context.Background()
	manager := newTestSTSIntegrationManager(t)

	const accessKey, secretKey = "adminkey", "adminsecret"
	iam := &IdentityAccessManagement{iamIntegration: NewS3IAMIntegration(manager, "")}
	require.NoError(t, iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{{
			Name:        "admin",
			Credentials: []*iam_pb.Credential{{AccessKey: accessKey, SecretKey: secretKey}},
			Actions:     []string{"Admin"},
		}},
	}))
	for _, ident := range iam.identities {
		ident.PrincipalArn = ""
	}

	body := url.Values{
		"Action":          {"AssumeRole"},
		"Version":         {"2011-06-15"},
		"RoleSessionName": {"self-session"},
	}.Encode()
	req, err := newTestRequest(http.MethodPost, "http://sts.seaweedfs.test/", int64(len(body)), strings.NewReader(body))
	require.NoError(t, err)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	require.NoError(t, signRequestV4(req, accessKey, secretKey))

	rec := httptest.NewRecorder()
	NewSTSHandlers(manager.GetSTSService(), iam).handleAssumeRole(rec, req)
	require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())

	var resp AssumeRoleResponse
	require.NoError(t, xml.Unmarshal(rec.Body.Bytes(), &resp))
	assert.Equal(t, "arn:aws:sts::"+defaultAccountID+":assumed-role/admin/self-session", resp.Result.AssumedRoleUser.Arn)

	session, err := manager.GetSTSService().ValidateSessionToken(ctx, resp.Result.Credentials.SessionToken)
	require.NoError(t, err)
	assert.Equal(t, true, session.RequestContext["is_admin"], "a self-assumed session keeps the caller's admin standing")
}
