package weed_server

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/util"
)

// TestFilerServer_tusHandler_CrossPrefixSessionHijack reproduces
// GHSA-99q7-x53r-6j4g: a prefix-restricted token acting on another tenant's TUS
// session (HEAD/PATCH/DELETE) must be scoped against the session's stored
// TargetPath, not authorized on signature and method alone. The victim's session
// must survive a denied mutation.
func TestFilerServer_tusHandler_CrossPrefixSessionHijack(t *testing.T) {
	tests := []struct {
		name         string
		method       string
		prefix       string
		expectStatus int
		expectExists bool
	}{
		{"cross-prefix HEAD denied", http.MethodHead, "/buckets/allowed", http.StatusUnauthorized, true},
		{"matching-prefix HEAD allowed", http.MethodHead, "/buckets/secret", http.StatusOK, true},
		{"cross-prefix PATCH denied", http.MethodPatch, "/buckets/allowed", http.StatusUnauthorized, true},
		{"matching-prefix PATCH allowed", http.MethodPatch, "/buckets/secret", http.StatusNoContent, true},
		{"cross-prefix DELETE denied", http.MethodDelete, "/buckets/allowed", http.StatusUnauthorized, true},
		{"matching-prefix DELETE allowed", http.MethodDelete, "/buckets/secret", http.StatusNoContent, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fs, store := newTusTestServer(t, map[string]string{tusTestUploadID: "/buckets/secret/victim.bin"})

			signingKey := tusTestWriteKey
			if tt.method == http.MethodHead {
				signingKey = tusTestReadKey
			}
			token := signFilerToken(t, signingKey, []string{tt.prefix}, nil)
			req := httptest.NewRequest(tt.method, "/.tus/.uploads/"+tusTestUploadID, http.NoBody)
			req.Header.Set("Authorization", "Bearer "+token)
			req.Header.Set("Tus-Resumable", TusVersion)
			if tt.method == http.MethodPatch {
				req.Header.Set("Content-Type", "application/offset+octet-stream")
				req.Header.Set("Upload-Offset", "0")
			}
			rec := httptest.NewRecorder()

			fs.tusHandler(rec, req)

			if rec.Code != tt.expectStatus {
				t.Fatalf("%s status = %d, want %d; body=%q", tt.method, rec.Code, tt.expectStatus, rec.Body.String())
			}
			_, err := store.FindEntry(context.Background(), util.FullPath(fs.tusSessionInfoPath(tusTestUploadID)))
			if tt.expectExists && err != nil {
				t.Fatalf("session removed after %s: %v", tt.method, err)
			}
			if !tt.expectExists && err == nil {
				t.Fatalf("session still present after authorized %s", tt.method)
			}
		})
	}
}

// TestFilerServer_tusHandler_RejectsAliasesAndInvalidMetadata covers the routing
// and metadata guards: a non-canonical or aliased upload id is rejected before
// any lookup, and a session whose stored id, target or size is unusable resolves
// to "not found" rather than being authorized or acted upon.
func TestFilerServer_tusHandler_RejectsAliasesAndInvalidMetadata(t *testing.T) {
	tests := []struct {
		name    string
		routeID string
		stored  TusSession
	}{
		{"trailing path alias", tusTestUploadID + "/extra", TusSession{ID: tusTestUploadID, TargetPath: "/buckets/secret/victim.bin", Size: 1}},
		{"non-canonical route id", "not-a-uuid", TusSession{ID: tusTestUploadID, TargetPath: "/buckets/secret/victim.bin", Size: 1}},
		{"stored id mismatch", tusTestUploadID, TusSession{ID: "00000000-0000-0000-0000-000000000000", TargetPath: "/buckets/secret/victim.bin", Size: 1}},
		{"empty stored target", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "", Size: 1}},
		{"root stored target", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "/", Size: 1}},
		{"relative stored target", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "buckets/secret/x.bin", Size: 1}},
		{"oversize stored size", tusTestUploadID, TusSession{ID: tusTestUploadID, TargetPath: "/buckets/secret/victim.bin", Size: TusDefaultMaxSize + 1}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fs, store := newTusTestServer(t, nil)
			data, err := json.Marshal(&tt.stored)
			if err != nil {
				t.Fatalf("marshal session: %v", err)
			}
			if err := store.InsertEntry(context.Background(), &filer.Entry{
				FullPath: util.FullPath(fs.tusSessionInfoPath(tusTestUploadID)),
				Content:  data,
			}); err != nil {
				t.Fatalf("seed session: %v", err)
			}

			req := httptest.NewRequest(http.MethodHead, "/.tus/.uploads/"+tt.routeID, nil)
			req.Header.Set("Authorization", "Bearer "+signFilerToken(t, tusTestReadKey, nil, nil))
			req.Header.Set("Tus-Resumable", TusVersion)
			rec := httptest.NewRecorder()

			fs.tusHandler(rec, req)

			if rec.Code != http.StatusNotFound {
				t.Fatalf("HEAD %s = %d, want %d; body=%q", tt.routeID, rec.Code, http.StatusNotFound, rec.Body.String())
			}
		})
	}
}
