package s3api

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/iam/integration"
	"github.com/seaweedfs/seaweedfs/weed/iam/policy"
	"github.com/seaweedfs/seaweedfs/weed/iam/sts"
	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
	"github.com/stretchr/testify/require"
)

func newTestIAMManager(t *testing.T) *integration.IAMManager {
	t.Helper()
	mgr := integration.NewIAMManager()
	require.NoError(t, mgr.Initialize(&integration.IAMConfig{
		STS: &sts.STSConfig{
			TokenDuration:    sts.FlexibleDuration{Duration: time.Hour},
			MaxSessionLength: sts.FlexibleDuration{Duration: 12 * time.Hour},
			Issuer:           "test",
			SigningKey:       []byte("test-signing-key-32-characters!!"),
			AccountId:        "111122223333",
		},
		Policy: &policy.PolicyEngineConfig{DefaultEffect: "Allow", StoreType: "memory"},
		Roles:  &integration.RoleStoreConfig{StoreType: "memory"},
	}, func() string { return "localhost:8888" }))
	return mgr
}

func TestPutPolicy_SyncsToIAMManager(t *testing.T) {
	mgr := newTestIAMManager(t)
	iam := &IdentityAccessManagement{}
	iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))

	policyDoc, _ := json.Marshal(map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::backup/*"},
		},
	})

	require.NoError(t, iam.PutPolicy("BackupAll", string(policyDoc)))

	allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
		Principal:   "arn:aws:iam::111122223333:user/test",
		Action:      "s3:PutObject",
		Resource:    "arn:aws:s3:::backup/file.txt",
		PolicyNames: []string{"BackupAll"},
	})
	require.NoError(t, err)
	require.True(t, allowed, "PutPolicy should sync to IAM Manager policy engine")
}

func TestDeletePolicy_SyncsToIAMManager(t *testing.T) {
	mgr := newTestIAMManager(t)
	iam := &IdentityAccessManagement{}
	iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))

	policyDoc, _ := json.Marshal(map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::backup/*"},
		},
	})

	require.NoError(t, iam.PutPolicy("BackupAll", string(policyDoc)))
	require.NoError(t, iam.DeletePolicy("BackupAll"))

	allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
		Principal:   "arn:aws:iam::111122223333:user/test",
		Action:      "s3:PutObject",
		Resource:    "arn:aws:s3:::backup/file.txt",
		PolicyNames: []string{"BackupAll"},
	})
	require.NoError(t, err)
	require.False(t, allowed, "DeletePolicy should remove from IAM Manager policy engine")
}

// TestSetIAMIntegration_FlushesLoadedPolicies reproduces the startup race: a
// policy is loaded into the IAM cache before the integration is attached (so the
// sync was skipped), then SetIAMIntegration must flush it into the manager's
// engine. Without the flush, policy_names identities get AccessDenied until an
// external IAM change triggers a reload.
func TestSetIAMIntegration_FlushesLoadedPolicies(t *testing.T) {
	iam := &IdentityAccessManagement{}

	policyDoc, _ := json.Marshal(map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::backup/*"},
		},
	})

	// Integration not attached yet, so this only lands in the legacy engine.
	require.NoError(t, iam.PutPolicy("BackupAll", string(policyDoc)))

	mgr := newTestIAMManager(t)
	iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))

	allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
		Principal:   "arn:aws:iam::111122223333:user/test",
		Action:      "s3:PutObject",
		Resource:    "arn:aws:s3:::backup/file.txt",
		PolicyNames: []string{"BackupAll"},
	})
	require.NoError(t, err)
	require.True(t, allowed, "SetIAMIntegration should flush already-loaded policies into the IAM Manager")
}

// TestResyncIAMManager_ReflectsCurrentPolicies pins the property that prevents
// the stale-snapshot race: SyncRuntimePolicies replaces the full desired set, so
// the resync must derive that set from the live iam.policies map at apply time,
// not from a view captured earlier. Here the map is mutated out from under an
// already-synced policy, and the next resync must converge the manager onto the
// new map rather than reinstating the old state.
func TestResyncIAMManager_ReflectsCurrentPolicies(t *testing.T) {
	mgr := newTestIAMManager(t)
	iam := &IdentityAccessManagement{}
	iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))

	doc := func(name string) string {
		b, _ := json.Marshal(map[string]interface{}{
			"Version": "2012-10-17",
			"Statement": []map[string]interface{}{
				{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::" + name + "/*"},
			},
		})
		return string(b)
	}
	allows := func(name string) bool {
		allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
			Principal:   "arn:aws:iam::111122223333:user/test",
			Action:      "s3:PutObject",
			Resource:    "arn:aws:s3:::" + name + "/file.txt",
			PolicyNames: []string{name},
		})
		require.NoError(t, err)
		return allowed
	}

	require.NoError(t, iam.PutPolicy("alpha", doc("alpha")))
	require.True(t, allows("alpha"))

	// Swap the map contents without going through PutPolicy/DeletePolicy, then
	// resync. A snapshot-based sync would push a pre-swap view; the fresh-read
	// resync must mirror the current map: alpha gone, beta present.
	iam.m.Lock()
	delete(iam.policies, "alpha")
	iam.policies["beta"] = &iam_pb.Policy{Name: "beta", Content: doc("beta")}
	iam.m.Unlock()
	iam.resyncIAMManagerPolicies()

	require.False(t, allows("alpha"), "resync should drop a policy no longer in the map")
	require.True(t, allows("beta"), "resync should add a policy newly in the map")
}

func TestResync_SkipsUnparsablePolicy(t *testing.T) {
	mgr := newTestIAMManager(t)
	iam := &IdentityAccessManagement{}
	iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))

	doc, _ := json.Marshal(map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::good/*"},
		},
	})

	iam.m.Lock()
	iam.policies = map[string]*iam_pb.Policy{
		"broken": {Name: "broken"},
		"good":   {Name: "good", Content: string(doc)},
	}
	iam.m.Unlock()
	iam.resyncIAMManagerPolicies()

	allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
		Principal:   "arn:aws:iam::111122223333:user/test",
		Action:      "s3:PutObject",
		Resource:    "arn:aws:s3:::good/file.txt",
		PolicyNames: []string{"good"},
	})
	require.NoError(t, err)
	require.True(t, allowed, "an unparsable policy must not block the rest of the sync")
}

// A policy whose stored definition stops parsing must stop granting access:
// enforcing a document the operator can no longer see is worse than denying.
func TestResync_UnparsablePolicyStopsGranting(t *testing.T) {
	mgr := newTestIAMManager(t)
	iam := &IdentityAccessManagement{}
	iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))

	doc, _ := json.Marshal(map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{"Effect": "Allow", "Action": "s3:*", "Resource": "arn:aws:s3:::good/*"},
		},
	})
	allows := func() bool {
		allowed, err := mgr.IsActionAllowed(context.Background(), &integration.ActionRequest{
			Principal:   "arn:aws:iam::111122223333:user/test",
			Action:      "s3:PutObject",
			Resource:    "arn:aws:s3:::good/file.txt",
			PolicyNames: []string{"good"},
		})
		require.NoError(t, err)
		return allowed
	}

	require.NoError(t, iam.PutPolicy("good", string(doc)))
	require.True(t, allows())

	iam.m.Lock()
	iam.policies["good"] = &iam_pb.Policy{Name: "good", Content: "{not json"}
	iam.m.Unlock()
	iam.resyncIAMManagerPolicies()

	require.False(t, allows(), "an unparsable policy must not keep granting its old permissions")
}
