package s3err

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/util/request_id"
	"github.com/stretchr/testify/assert"
)

func TestGetAccessLogUsesAmzRequestID(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
	req = req.WithContext(request_id.Set(req.Context(), "req-123"))

	log := GetAccessLog(req, http.StatusOK, ErrNone)

	assert.Equal(t, "req-123", log.RequestID)
}

func TestGetAccessLogRemoteIP(t *testing.T) {
	tests := []struct {
		name           string
		remoteAddr     string
		xRealIP        string
		xForwardedFor  string
		expectedRemote string
	}{
		{
			name:           "falls back to RemoteAddr (port stripped) when no headers set",
			remoteAddr:     "10.89.0.1:35832",
			expectedRemote: "10.89.0.1",
		},
		{
			name:           "preserves IPv6 host from RemoteAddr",
			remoteAddr:     "[2001:db8::1]:35832",
			expectedRemote: "2001:db8::1",
		},
		{
			name:           "returns RemoteAddr unchanged when no port present",
			remoteAddr:     "@",
			expectedRemote: "@",
		},
		{
			name:           "uses X-Real-IP when X-Forwarded-For is absent",
			remoteAddr:     "10.89.0.1:35832",
			xRealIP:        "203.0.113.7",
			expectedRemote: "203.0.113.7",
		},
		{
			name:           "prefers X-Forwarded-For over X-Real-IP",
			remoteAddr:     "10.89.0.1:35832",
			xRealIP:        "203.0.113.7",
			xForwardedFor:  "198.51.100.42",
			expectedRemote: "198.51.100.42",
		},
		{
			name:           "uses first hop in X-Forwarded-For chain",
			remoteAddr:     "10.89.0.1:35832",
			xForwardedFor:  "198.51.100.42, 10.0.0.5, 10.89.0.1",
			expectedRemote: "198.51.100.42",
		},
		{
			name:           "skips empty leading entries in X-Forwarded-For",
			remoteAddr:     "10.89.0.1:35832",
			xForwardedFor:  ", 198.51.100.42",
			expectedRemote: "198.51.100.42",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
			req.RemoteAddr = tc.remoteAddr
			if tc.xRealIP != "" {
				req.Header.Set("X-Real-IP", tc.xRealIP)
			}
			if tc.xForwardedFor != "" {
				req.Header.Set("X-Forwarded-For", tc.xForwardedFor)
			}

			log := GetAccessLog(req, http.StatusOK, ErrNone)

			assert.Equal(t, tc.expectedRemote, log.RemoteIP)
		})
	}
}

// TestGetAccessLogRequesterFromFallback reproduces the fallback audit path for
// GET/HEAD/IAM operations: authentication records the requester on a request
// copy the track() middleware never sees, yet the fallback audit entry (built
// from the original request) must still report the authenticated user.
func TestGetAccessLogRequesterFromFallback(t *testing.T) {
	// track() installs the holder before authentication runs.
	outer := s3_constants.EnsureIdentityHolder(httptest.NewRequest(http.MethodGet, "/bucket/object", nil))

	// auth records the identity on a copy and hands that copy to the handler;
	// the copy itself is discarded once the handler returns.
	_ = outer.WithContext(s3_constants.SetIdentityNameInContext(outer.Context(), "admin"))

	// The handler returned without logging, so track() builds the fallback entry
	// from the original request.
	log := GetAccessLog(outer, http.StatusOK, ErrNone)

	assert.Equal(t, "admin", log.Requester, "fallback audit entry must report the authenticated requester")
}

func TestGetAccessLogRequesterAnonymous(t *testing.T) {
	req := s3_constants.EnsureIdentityHolder(httptest.NewRequest(http.MethodGet, "/bucket/object", nil))

	log := GetAccessLog(req, http.StatusOK, ErrNone)

	assert.Empty(t, log.Requester, "anonymous request must not report a requester")
}

func TestAuditTrackingFlag(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/bucket/object", nil)
	assert.False(t, AuditAlreadyLogged(req), "untracked request reports not logged")

	tracked := EnsureAuditTracking(req)
	assert.NotSame(t, req, tracked, "EnsureAuditTracking returns a new request when no flag is present")
	assert.False(t, AuditAlreadyLogged(tracked), "tracked request starts unlogged")

	again := EnsureAuditTracking(tracked)
	assert.Same(t, tracked, again, "EnsureAuditTracking is idempotent when flag already present")

	MarkAuditLogged(tracked)
	assert.True(t, AuditAlreadyLogged(tracked), "flag flips after MarkAuditLogged")
}
