package rest

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	filefeature "github.com/nexuscrm/backend/internal/features/files"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/infrastructure/storage"
	"github.com/nexuscrm/backend/pkg/apierr"
	backendauth "github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestFileUploadHandler_UsesObjectStoreForUploadAndDownload(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", nil, nil)
	router := newFileUploadTestRouter(handler)

	req := newUploadRequest(t, http.MethodPost, "/services/data/v63.0/sobjects/ContentVersion", "hello.txt", []byte("hello world"), nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusOK {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}

	var uploadResp struct {
		Data struct {
			ID string `json:"Id"`
		} `json:"data"`
	}
	if err := json.Unmarshal(resp.Body.Bytes(), &uploadResp); err != nil {
		t.Fatalf("json.Unmarshal(upload) error = %v", err)
	}
	if uploadResp.Data.ID == "" {
		t.Fatalf("upload response missing file id")
	}

	if _, err := store.HeadObject(context.Background(), legacyUploadObjectKey(uploadResp.Data.ID)); err != nil {
		t.Fatalf("HeadObject() for uploaded file error = %v", err)
	}

	downloadReq := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/sobjects/ContentVersion/"+uploadResp.Data.ID+"/VersionData", nil)
	downloadResp := httptest.NewRecorder()
	router.ServeHTTP(downloadResp, downloadReq)

	if downloadResp.Code != http.StatusOK {
		t.Fatalf("download status = %d, body = %s", downloadResp.Code, downloadResp.Body.String())
	}
	if got := downloadResp.Body.String(); got != "hello world" {
		t.Fatalf("download body = %q, want %q", got, "hello world")
	}
}

func TestFileUploadHandler_UploadRecordFileCreatesSystemFileAndDownloadsBySystemFileID(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	var inserted models.SObject
	records := stubFileRecordStore{
		insertFunc: func(ctx context.Context, objectName string, data models.SObject, _ *models.UserSession) (models.SObject, error) {
			if objectName != constants.TableFile {
				t.Fatalf("Insert() objectName = %s, want %s", objectName, constants.TableFile)
			}
			if got := constants.GetSecurityContext(ctx); got != constants.SystemContext {
				t.Fatalf("Insert() security context = %v, want %v", got, constants.SystemContext)
			}
			inserted = cloneTestSObject(data)
			return data, nil
		},
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			if objectName == constants.TableFile && inserted != nil {
				insertedID, _ := inserted[constants.FieldID].(string)
				if insertedID == id {
					return cloneTestSObject(inserted), nil
				}
			}
			return nil, nil
		},
	}
	permissions := stubFilePermissionChecker{
		checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
			if objectAPIName != constants.TableFile {
				t.Fatalf("permission check got object %s, want %s", objectAPIName, constants.TableFile)
			}
			switch operation {
			case constants.PermRead:
			case constants.PermCreate:
				t.Fatalf("record upload must not require _System_File Create permission")
			default:
				t.Fatalf("permission check got operation %s, want %s", operation, constants.PermRead)
			}
			return nil
		},
	}

	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "bucket-1", "", records, permissions)
	router := newFileUploadTestRouter(handler)

	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"hello.txt",
		[]byte("hello world"),
		map[string]string{
			uploadFieldParentID:            "quote-1",
			uploadFieldParentObjectAPIName: "Quote",
			uploadFieldAttachmentKind:      "record",
		},
	)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}

	var uploadResp struct {
		Data struct {
			ID          string `json:"Id"`
			DownloadURL string `json:"DownloadUrl"`
		} `json:"data"`
	}
	if err := json.Unmarshal(resp.Body.Bytes(), &uploadResp); err != nil {
		t.Fatalf("json.Unmarshal(upload) error = %v", err)
	}
	if uploadResp.Data.ID == "" {
		t.Fatalf("upload response missing file id")
	}
	if uploadResp.Data.DownloadURL != recordFileDownloadURL(uploadResp.Data.ID) {
		t.Fatalf("DownloadUrl = %q, want %q", uploadResp.Data.DownloadURL, recordFileDownloadURL(uploadResp.Data.ID))
	}

	if gotParentID, _ := inserted[constants.FieldSysFile_ParentID].(string); gotParentID != "quote-1" {
		t.Fatalf("inserted ParentId = %q, want %q", gotParentID, "quote-1")
	}
	if gotParentObject, _ := inserted[constants.FieldSysFile_ParentObjectAPIName].(string); gotParentObject != "Quote" {
		t.Fatalf("inserted ParentObjectApiName = %q, want %q", gotParentObject, "Quote")
	}
	if gotStorageProvider, _ := inserted[constants.FieldSysFile_StorageProvider].(string); gotStorageProvider != storage.ProviderFilesystem {
		t.Fatalf("inserted StorageProvider = %q, want %q", gotStorageProvider, storage.ProviderFilesystem)
	}
	if gotStorageBucket, _ := inserted[constants.FieldSysFile_StorageBucket].(string); gotStorageBucket != "bucket-1" {
		t.Fatalf("inserted StorageBucket = %q, want %q", gotStorageBucket, "bucket-1")
	}
	if gotSourceType, _ := inserted[constants.FieldSysFile_SourceType].(string); gotSourceType != systemFileSourceTypeNative {
		t.Fatalf("inserted SourceType = %q, want %q", gotSourceType, systemFileSourceTypeNative)
	}
	if gotStatus, _ := inserted[constants.FieldSysFile_Status].(string); gotStatus != "Active" {
		t.Fatalf("inserted Status = %q, want %q", gotStatus, "Active")
	}

	storageKey, _ := inserted[constants.FieldSysFile_StorageKey].(string)
	if storageKey == "" {
		t.Fatalf("inserted StorageKey should not be empty")
	}
	if gotStoragePath, _ := inserted[constants.FieldSysFile_StoragePath].(string); gotStoragePath != storageKey {
		t.Fatalf("inserted StoragePath = %q, want mirror of StorageKey %q", gotStoragePath, storageKey)
	}
	if _, err := store.HeadObject(context.Background(), storageKey); err != nil {
		t.Fatalf("HeadObject() for uploaded file error = %v", err)
	}

	downloadReq := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/files/"+uploadResp.Data.ID+"/download", nil)
	downloadResp := httptest.NewRecorder()
	router.ServeHTTP(downloadResp, downloadReq)

	if downloadResp.Code != http.StatusOK {
		t.Fatalf("download status = %d, body = %s", downloadResp.Code, downloadResp.Body.String())
	}
	if got := downloadResp.Body.String(); got != "hello world" {
		t.Fatalf("download body = %q, want %q", got, "hello world")
	}
}

func TestFileUploadHandler_UploadRecordFileDoesNotRequireSystemFileCreatePermission(t *testing.T) {
	gin.SetMode(gin.TestMode)

	insertCalled := false
	records := stubFileRecordStore{
		insertFunc: func(context.Context, string, models.SObject, *models.UserSession) (models.SObject, error) {
			insertCalled = true
			return nil, nil
		},
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
	}
	permissions := stubFilePermissionChecker{
		checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
			if objectAPIName == constants.TableFile && operation == constants.PermCreate {
				t.Fatalf("record upload must not check _System_File Create permission")
			}
			return nil
		},
	}

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}
	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "bucket-1", "", records, permissions)
	router := newFileUploadTestRouter(handler)

	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"hello.txt",
		[]byte("hello world"),
		map[string]string{
			uploadFieldParentID:            "quote-1",
			uploadFieldParentObjectAPIName: "Quote",
			uploadFieldAttachmentKind:      "record",
		},
	)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if !insertCalled {
		t.Fatalf("Insert() should be called after parent access is authorized")
	}
}

func TestFileUploadHandler_UploadRecordFileCanonicalizesParentObjectAPIName(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	var inserted models.SObject
	records := stubFileRecordStore{
		insertFunc: func(_ context.Context, objectName string, data models.SObject, _ *models.UserSession) (models.SObject, error) {
			inserted = cloneTestSObject(data)
			return data, nil
		},
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
	}
	permissions := stubFilePermissionChecker{
		checkFunc: func(context.Context, string, string, *models.UserSession) error { return nil },
	}

	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "bucket-1", "", records, permissions)
	router := newFileUploadTestRouter(handler)

	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"hello.txt",
		[]byte("hello world"),
		map[string]string{
			uploadFieldParentID:            "quote-1",
			uploadFieldParentObjectAPIName: "quote",
		},
	)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}

	if gotParentObject, _ := inserted[constants.FieldSysFile_ParentObjectAPIName].(string); gotParentObject != "Quote" {
		t.Fatalf("inserted ParentObjectApiName = %q, want %q", gotParentObject, "Quote")
	}
}

func TestFileUploadHandler_DeleteRecordFileIsIdempotent(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	objectKey := "system-file/Quote/quote-1/a10xx/hello.txt"
	if _, err := store.PutObject(context.Background(), objectKey, bytes.NewReader([]byte("hello world")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	deleted := false
	record := models.SObject{
		constants.FieldID:                          "a10xx",
		constants.FieldSysFile_ParentID:            "quote-1",
		constants.FieldSysFile_ParentObjectAPIName: "Quote",
		constants.FieldSysFile_StorageKey:          objectKey,
		constants.FieldSysFile_OriginalFileName:    "hello.txt",
	}
	records := stubFileRecordStore{
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			if deleted {
				return models.SObject{
					constants.FieldID:                          "a10xx",
					constants.FieldSysFile_Status:              "Deleted",
					constants.FieldSysFile_ParentID:            "quote-1",
					constants.FieldSysFile_ParentObjectAPIName: "Quote",
					constants.FieldSysFile_StorageKey:          objectKey,
				}, nil
			}
			if objectName == constants.TableFile && id == "a10xx" {
				return cloneTestSObject(record), nil
			}
			return nil, nil
		},
		updateFunc: func(_ context.Context, objectName string, id string, updates models.SObject, _ *models.UserSession) error {
			if objectName != constants.TableFile || id != "a10xx" {
				t.Fatalf("Update() got %s/%s", objectName, id)
			}
			if got, _ := updates[constants.FieldSysFile_Status].(string); got != "Deleted" {
				t.Fatalf("Update() Status = %v, want Deleted", updates[constants.FieldSysFile_Status])
			}
			deleted = true
			return nil
		},
	}
	permissions := stubFilePermissionChecker{
		checkFunc: func(context.Context, string, string, *models.UserSession) error {
			return nil
		},
	}

	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", records, permissions)
	router := newFileUploadTestRouter(handler)

	deleteReq := httptest.NewRequest(http.MethodDelete, "/services/data/v63.0/files/a10xx", nil)
	deleteResp := httptest.NewRecorder()
	router.ServeHTTP(deleteResp, deleteReq)

	if deleteResp.Code != http.StatusNoContent {
		t.Fatalf("delete status = %d, body = %s", deleteResp.Code, deleteResp.Body.String())
	}
	if _, err := store.HeadObject(context.Background(), objectKey); err != storage.ErrObjectNotFound {
		t.Fatalf("HeadObject() after delete error = %v, want %v", err, storage.ErrObjectNotFound)
	}

	repeatReq := httptest.NewRequest(http.MethodDelete, "/services/data/v63.0/files/a10xx", nil)
	repeatResp := httptest.NewRecorder()
	router.ServeHTTP(repeatResp, repeatReq)

	if repeatResp.Code != http.StatusNoContent {
		t.Fatalf("repeat delete status = %d, body = %s", repeatResp.Code, repeatResp.Body.String())
	}
}

func TestFileUploadHandler_DeleteRecordFileSupportsLegacyCommentRowsWithoutParentObjectApiName(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	objectKey := "system-file/_System_Comment/comment-1/a10comment-delete/hello.txt"
	if _, err := store.PutObject(context.Background(), objectKey, bytes.NewReader([]byte("comment attachment")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	deleted := false
	records := stubFileRecordStore{
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			switch {
			case objectName == constants.TableFile && id == "a10comment-delete":
				if deleted {
					return models.SObject{
						constants.FieldID:                 "a10comment-delete",
						constants.FieldSysFile_Status:     "Deleted",
						constants.FieldSysFile_ParentID:   "comment-1",
						constants.FieldSysFile_StorageKey: objectKey,
					}, nil
				}
				return models.SObject{
					constants.FieldID:                       "a10comment-delete",
					constants.FieldSysFile_ParentID:         "comment-1",
					constants.FieldSysFile_StorageKey:       objectKey,
					constants.FieldSysFile_OriginalFileName: "hello.txt",
				}, nil
			case objectName == constants.TableComment && id == "comment-1":
				return models.SObject{
					constants.FieldID:                       "comment-1",
					constants.FieldSysComment_ObjectAPIName: "Quote",
					constants.FieldSysComment_RecordID:      "quote-1",
				}, nil
			default:
				return nil, nil
			}
		},
		updateFunc: func(_ context.Context, objectName string, id string, updates models.SObject, _ *models.UserSession) error {
			if objectName != constants.TableFile || id != "a10comment-delete" {
				t.Fatalf("Update() got %s/%s", objectName, id)
			}
			if got, _ := updates[constants.FieldSysFile_Status].(string); got != "Deleted" {
				t.Fatalf("Update() Status = %v, want Deleted", updates[constants.FieldSysFile_Status])
			}
			deleted = true
			return nil
		},
	}
	permissions := stubFilePermissionChecker{
		checkFunc: func(context.Context, string, string, *models.UserSession) error {
			return nil
		},
	}

	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", records, permissions)
	router := newFileUploadTestRouter(handler)

	req := httptest.NewRequest(http.MethodDelete, "/services/data/v63.0/files/a10comment-delete", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusNoContent {
		t.Fatalf("delete status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if _, err := store.HeadObject(context.Background(), objectKey); err != storage.ErrObjectNotFound {
		t.Fatalf("HeadObject() after delete error = %v, want %v", err, storage.ErrObjectNotFound)
	}
}

func TestFileUploadHandler_ArchivedOrderUsesDurableUploadService(t *testing.T) {
	gin.SetMode(gin.TestMode)

	handler := NewFileUploadHandler(
		stubObjectStore{},
		storage.ProviderS3,
		"bucket-1",
		"",
		stubFileRecordStore{
			retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
				if objectName == "Order" && id == "order-1" {
					return models.SObject{constants.FieldID: id}, nil
				}
				return nil, apierr.NotFound("Record not found")
			},
		},
		stubFilePermissionChecker{
			checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
				if objectAPIName == constants.TableFile && operation == constants.PermCreate {
					t.Fatalf("archived upload must not require _System_File Create permission")
				}
				return nil
			},
		},
	)

	var gotReq filefeature.ArchivedOrderUploadRequest
	handler.SetDurableUploadService(stubDurableFileUploadService{
		uploadFunc: func(_ context.Context, req filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error) {
			gotReq = req
			return filefeature.UploadResult{
				ID:          "a10archived",
				Name:        req.FileName,
				Size:        42,
				Mime:        req.MimeType,
				DownloadURL: "/services/data/v63.0/files/a10archived/download",
			}, nil
		},
	})

	router := newFileUploadTestRouter(handler)
	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"Archived Order Form.pdf",
		[]byte("pdf payload"),
		map[string]string{
			uploadFieldParentID:            "order-1",
			uploadFieldParentObjectAPIName: "Order",
			uploadFieldAttachmentKind:      filefeature.AttachmentKindArchivedOrder,
		},
	)
	req.Header.Set(uploadHeaderIDempotencyKey, "idem-archived-order")

	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if gotReq.IDempotencyKey != "idem-archived-order" {
		t.Fatalf("idempotency key = %q, want idem-archived-order", gotReq.IDempotencyKey)
	}
	if gotReq.AttachmentKind != filefeature.AttachmentKindArchivedOrder {
		t.Fatalf("attachment kind = %q, want %q", gotReq.AttachmentKind, filefeature.AttachmentKindArchivedOrder)
	}
	if gotReq.StorageProvider != storage.ProviderS3 {
		t.Fatalf("storage provider = %q, want %q", gotReq.StorageProvider, storage.ProviderS3)
	}
	if gotReq.StorageBucket != "bucket-1" {
		t.Fatalf("storage bucket = %q, want bucket-1", gotReq.StorageBucket)
	}
	if gotReq.ContentSHA256 == "" {
		t.Fatalf("expected content sha256 to be populated")
	}
	expectedChecksum := sha256.Sum256([]byte("pdf payload"))
	if gotReq.ContentSHA256 != hex.EncodeToString(expectedChecksum[:]) {
		t.Fatalf("content sha256 = %q, want %q", gotReq.ContentSHA256, hex.EncodeToString(expectedChecksum[:]))
	}
}

func TestFileUploadHandler_ArchivedContractUsesDurableUploadService(t *testing.T) {
	gin.SetMode(gin.TestMode)

	handler := NewFileUploadHandler(
		stubObjectStore{},
		storage.ProviderS3,
		"bucket-1",
		"",
		stubFileRecordStore{
			retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
				if objectName == "Contract" && id == "contract-1" {
					return models.SObject{constants.FieldID: id}, nil
				}
				return nil, apierr.NotFound("Record not found")
			},
		},
		stubFilePermissionChecker{
			checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
				if objectAPIName == constants.TableFile && operation == constants.PermCreate {
					t.Fatalf("archived upload must not require _System_File Create permission")
				}
				return nil
			},
		},
	)

	var gotReq filefeature.ArchivedOrderUploadRequest
	handler.SetDurableUploadService(stubDurableFileUploadService{
		uploadFunc: func(_ context.Context, req filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error) {
			gotReq = req
			return filefeature.UploadResult{
				ID:          "a10contract",
				Name:        req.FileName,
				Size:        42,
				Mime:        req.MimeType,
				DownloadURL: "/services/data/v63.0/files/a10contract/download",
			}, nil
		},
	})

	router := newFileUploadTestRouter(handler)
	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"Archived Contract.pdf",
		[]byte("pdf payload"),
		map[string]string{
			uploadFieldParentID:            "contract-1",
			uploadFieldParentObjectAPIName: "Contract",
			uploadFieldAttachmentKind:      filefeature.AttachmentKindArchivedContract,
		},
	)
	req.Header.Set(uploadHeaderIDempotencyKey, "idem-archived-contract")

	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if gotReq.IDempotencyKey != "idem-archived-contract" {
		t.Fatalf("idempotency key = %q, want idem-archived-contract", gotReq.IDempotencyKey)
	}
	if gotReq.AttachmentKind != filefeature.AttachmentKindArchivedContract {
		t.Fatalf("attachment kind = %q, want %q", gotReq.AttachmentKind, filefeature.AttachmentKindArchivedContract)
	}
}

func TestFileUploadHandler_ArchivedOrderDoesNotRequireSystemFileCreatePermission(t *testing.T) {
	gin.SetMode(gin.TestMode)

	handler := NewFileUploadHandler(
		stubObjectStore{},
		storage.ProviderS3,
		"bucket-1",
		"",
		stubFileRecordStore{
			retrieveWithAccessFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, *permissions.RecordAccessResult, error) {
				if objectName == "Order" && id == "order-1" {
					return models.SObject{constants.FieldID: id}, &permissions.RecordAccessResult{
						HasReadAccess: true,
						HasEditAccess: true,
					}, nil
				}
				return nil, nil, apierr.NotFound("Record not found")
			},
		},
		stubFilePermissionChecker{
			checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
				if objectAPIName == constants.TableFile && operation == constants.PermCreate {
					t.Fatalf("archived upload must not check _System_File Create permission")
				}
				return nil
			},
		},
	)

	uploadCalled := false
	handler.SetDurableUploadService(stubDurableFileUploadService{
		uploadFunc: func(_ context.Context, _ filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error) {
			uploadCalled = true
			return filefeature.UploadResult{
				ID:          "a10archived",
				Name:        "Archived Order Form.pdf",
				Size:        42,
				Mime:        "application/pdf",
				DownloadURL: "/services/data/v63.0/files/a10archived/download",
			}, nil
		},
	})

	router := newFileUploadTestRouter(handler)
	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"Archived Order Form.pdf",
		[]byte("pdf payload"),
		map[string]string{
			uploadFieldParentID:            "order-1",
			uploadFieldParentObjectAPIName: "Order",
			uploadFieldAttachmentKind:      filefeature.AttachmentKindArchivedOrder,
		},
	)

	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusCreated {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if !uploadCalled {
		t.Fatalf("durable upload service should be called after parent edit access is authorized")
	}
}

func TestFileUploadHandler_ArchivedOrderRequiresParentEditAccess(t *testing.T) {
	gin.SetMode(gin.TestMode)

	handler := NewFileUploadHandler(
		stubObjectStore{},
		storage.ProviderS3,
		"bucket-1",
		"",
		stubFileRecordStore{
			retrieveWithAccessFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, *permissions.RecordAccessResult, error) {
				if objectName != "Order" || id != "order-1" {
					t.Fatalf("RetrieveWithAccess() got %s/%s, want Order/order-1", objectName, id)
				}
				return models.SObject{constants.FieldID: id}, &permissions.RecordAccessResult{
					HasReadAccess: true,
					HasEditAccess: false,
				}, nil
			},
		},
		stubFilePermissionChecker{},
	)

	uploadCalled := false
	handler.SetDurableUploadService(stubDurableFileUploadService{
		uploadFunc: func(_ context.Context, _ filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error) {
			uploadCalled = true
			return filefeature.UploadResult{}, nil
		},
	})

	router := newFileUploadTestRouter(handler)
	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"Archived Order Form.pdf",
		[]byte("pdf payload"),
		map[string]string{
			uploadFieldParentID:            "order-1",
			uploadFieldParentObjectAPIName: "Order",
			uploadFieldAttachmentKind:      filefeature.AttachmentKindArchivedOrder,
		},
	)

	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusForbidden {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if uploadCalled {
		t.Fatalf("durable upload service should not be called without parent edit access")
	}
}

func TestFileUploadHandler_ArchivedOrderStopsWhenParentAccessLookupFails(t *testing.T) {
	gin.SetMode(gin.TestMode)

	handler := NewFileUploadHandler(
		stubObjectStore{},
		storage.ProviderS3,
		"bucket-1",
		"",
		stubFileRecordStore{
			retrieveWithAccessFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, *permissions.RecordAccessResult, error) {
				if objectName != "Order" || id != "missing-order" {
					t.Fatalf("RetrieveWithAccess() got %s/%s, want Order/missing-order", objectName, id)
				}
				return nil, nil, apierr.NotFound("Record not found")
			},
		},
		stubFilePermissionChecker{},
	)

	uploadCalled := false
	handler.SetDurableUploadService(stubDurableFileUploadService{
		uploadFunc: func(_ context.Context, _ filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error) {
			uploadCalled = true
			return filefeature.UploadResult{}, nil
		},
	})

	router := newFileUploadTestRouter(handler)
	req := newUploadRequest(
		t,
		http.MethodPost,
		"/services/data/v63.0/files",
		"Archived Order Form.pdf",
		[]byte("pdf payload"),
		map[string]string{
			uploadFieldParentID:            "missing-order",
			uploadFieldParentObjectAPIName: "Order",
			uploadFieldAttachmentKind:      filefeature.AttachmentKindArchivedOrder,
		},
	)

	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusNotFound {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if uploadCalled {
		t.Fatalf("durable upload service should not be called when parent access lookup fails")
	}
}

func TestFileUploadHandler_DeleteRecordFileRejectsCompatProjection(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	objectKey := "content-compat/shared/hello.txt"
	if _, err := store.PutObject(context.Background(), objectKey, bytes.NewReader([]byte("shared blob")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	record := models.SObject{
		constants.FieldID:                          "a10compat",
		constants.FieldSysFile_ParentID:            "quote-1",
		constants.FieldSysFile_ParentObjectAPIName: "Quote",
		constants.FieldSysFile_StorageKey:          objectKey,
		constants.FieldSysFile_SourceType:          systemFileSourceTypeContentCompat,
	}
	updateCalled := false
	records := stubFileRecordStore{
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			if objectName == constants.TableFile && id == "a10compat" {
				return cloneTestSObject(record), nil
			}
			return nil, nil
		},
		updateFunc: func(context.Context, string, string, models.SObject, *models.UserSession) error {
			updateCalled = true
			return nil
		},
	}
	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", records, stubFilePermissionChecker{})
	router := newFileUploadTestRouter(handler)

	req := httptest.NewRequest(http.MethodDelete, "/services/data/v63.0/files/a10compat", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusBadRequest {
		t.Fatalf("delete status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if updateCalled {
		t.Fatalf("compat delete must reject before soft-deleting metadata")
	}
	if _, err := store.HeadObject(context.Background(), objectKey); err != nil {
		t.Fatalf("HeadObject() after compat delete error = %v, want blob retained", err)
	}
}

func TestFileUploadHandler_DeleteRecordFileRetriesBlobCleanupForDeletedRows(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	objectKey := "system-file/Quote/quote-1/a10retry/hello.txt"
	if _, err := store.PutObject(context.Background(), objectKey, bytes.NewReader([]byte("hello world")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	deleted := false
	record := models.SObject{
		constants.FieldID:                          "a10retry",
		constants.FieldSysFile_ParentID:            "quote-1",
		constants.FieldSysFile_ParentObjectAPIName: "Quote",
		constants.FieldSysFile_StorageKey:          objectKey,
		constants.FieldSysFile_OriginalFileName:    "hello.txt",
	}
	records := stubFileRecordStore{
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			if objectName == constants.TableFile && id == "a10retry" {
				if deleted {
					return models.SObject{
						constants.FieldID:                          "a10retry",
						constants.FieldSysFile_Status:              "Deleted",
						constants.FieldSysFile_ParentID:            "quote-1",
						constants.FieldSysFile_ParentObjectAPIName: "Quote",
						constants.FieldSysFile_StorageKey:          objectKey,
					}, nil
				}
				return cloneTestSObject(record), nil
			}
			return nil, nil
		},
		updateFunc: func(_ context.Context, objectName string, id string, updates models.SObject, _ *models.UserSession) error {
			if objectName != constants.TableFile || id != "a10retry" {
				t.Fatalf("Update() got %s/%s", objectName, id)
			}
			if got, _ := updates[constants.FieldSysFile_Status].(string); got != "Deleted" {
				t.Fatalf("Update() Status = %v, want Deleted", updates[constants.FieldSysFile_Status])
			}
			deleted = true
			return nil
		},
	}
	permissions := stubFilePermissionChecker{
		checkFunc: func(context.Context, string, string, *models.UserSession) error {
			return nil
		},
	}

	handler := NewFileUploadHandler(
		stubObjectStore{
			deleteFunc: func(context.Context, string) error {
				return storage.ErrObjectNotFound
			},
			getFunc: func(context.Context, string) (io.ReadCloser, storage.ObjectMetadata, error) {
				return nil, storage.ObjectMetadata{}, storage.ErrObjectNotFound
			},
		},
		storage.ProviderFilesystem,
		"",
		"",
		records,
		permissions,
	)
	router := newFileUploadTestRouter(handler)

	req := httptest.NewRequest(http.MethodDelete, "/services/data/v63.0/files/a10retry", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusNoContent {
		t.Fatalf("delete status = %d, body = %s", resp.Code, resp.Body.String())
	}
}

func TestFileUploadHandler_LegacyUploadRequiresCreatePermissionWhenPermissionCheckerPresent(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	handler := NewFileUploadHandler(
		store,
		storage.ProviderFilesystem,
		"",
		"",
		nil,
		stubFilePermissionChecker{
			checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
				if objectAPIName != constants.TableFile || operation != constants.PermCreate {
					t.Fatalf("permission check got %s/%s", objectAPIName, operation)
				}
				return apierr.Forbidden("permission denied")
			},
		},
	)
	router := newFileUploadTestRouter(handler)

	req := newUploadRequest(t, http.MethodPost, "/services/data/v63.0/sobjects/ContentVersion", "hello.txt", []byte("hello world"), nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusForbidden {
		t.Fatalf("upload status = %d, want %d, body = %s", resp.Code, http.StatusForbidden, resp.Body.String())
	}
}

func TestFileUploadHandler_LegacyDownloadRequiresReadPermissionWhenPermissionCheckerPresent(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	fileID := "550e8400-e29b-41d4-a716-446655440001.txt"
	if _, err := store.PutObject(context.Background(), legacyUploadObjectKey(fileID), bytes.NewReader([]byte("legacy attachment")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	handler := NewFileUploadHandler(
		store,
		storage.ProviderFilesystem,
		"",
		"",
		nil,
		stubFilePermissionChecker{
			checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
				if objectAPIName != constants.TableFile || operation != constants.PermRead {
					t.Fatalf("permission check got %s/%s", objectAPIName, operation)
				}
				return apierr.Forbidden("permission denied")
			},
		},
	)
	router := newFileUploadTestRouter(handler)

	req := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/sobjects/ContentVersion/"+fileID+"/VersionData", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusForbidden {
		t.Fatalf("download status = %d, want %d, body = %s", resp.Code, http.StatusForbidden, resp.Body.String())
	}
}

func TestFileUploadHandler_DownloadRecordFileRequiresSystemFileReadPermission(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	objectKey := "system-file/Quote/quote-1/a10xx/hello.txt"
	if _, err := store.PutObject(context.Background(), objectKey, bytes.NewReader([]byte("hello world")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	record := models.SObject{
		constants.FieldID:                          "a10xx",
		constants.FieldSysFile_ParentID:            "quote-1",
		constants.FieldSysFile_ParentObjectAPIName: "Quote",
		constants.FieldSysFile_StorageKey:          objectKey,
		constants.FieldSysFile_OriginalFileName:    "hello.txt",
	}
	records := stubFileRecordStore{
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			if objectName == constants.TableFile && id == "a10xx" {
				return cloneTestSObject(record), nil
			}
			return nil, nil
		},
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
	}
	permissions := stubFilePermissionChecker{
		checkFunc: func(_ context.Context, objectAPIName string, operation string, _ *models.UserSession) error {
			if objectAPIName == constants.TableFile && operation == constants.PermRead {
				return apierr.Forbidden("missing _System_File read permission")
			}
			return nil
		},
	}

	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", records, permissions)
	router := newFileUploadTestRouter(handler)

	req := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/files/a10xx/download", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusForbidden {
		t.Fatalf("download status = %d, body = %s", resp.Code, resp.Body.String())
	}
}

func TestFileUploadHandler_DownloadRecordFileSupportsLegacyRowsWithoutParentObjectApiName(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	objectKey := "system-file/Account/001legacy/a10legacy/hello.txt"
	if _, err := store.PutObject(context.Background(), objectKey, bytes.NewReader([]byte("legacy attachment")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	parentID := "001legacy"
	record := models.SObject{
		constants.FieldID:                       "a10legacy",
		constants.FieldSysFile_ParentID:         parentID,
		constants.FieldSysFile_StorageKey:       objectKey,
		constants.FieldSysFile_OriginalFileName: "hello.txt",
	}
	records := stubFileRecordStore{
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Account" && id == parentID {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			if objectName == constants.TableFile && id == "a10legacy" {
				return cloneTestSObject(record), nil
			}
			return nil, nil
		},
	}
	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", records, stubFilePermissionChecker{})
	router := newFileUploadTestRouter(handler)

	req := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/files/a10legacy/download", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusOK {
		t.Fatalf("download status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if got := resp.Body.String(); got != "legacy attachment" {
		t.Fatalf("download body = %q, want %q", got, "legacy attachment")
	}
}

func TestFileUploadHandler_DownloadRecordFileSupportsLegacyCommentRowsWithoutParentObjectApiName(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	objectKey := "system-file/_System_Comment/comment-1/a10comment/hello.txt"
	if _, err := store.PutObject(context.Background(), objectKey, bytes.NewReader([]byte("comment attachment")), storage.PutObjectOptions{
		ContentType: "text/plain",
	}); err != nil {
		t.Fatalf("PutObject() error = %v", err)
	}

	record := models.SObject{
		constants.FieldID:                       "a10comment",
		constants.FieldSysFile_ParentID:         "comment-1",
		constants.FieldSysFile_StorageKey:       objectKey,
		constants.FieldSysFile_OriginalFileName: "hello.txt",
	}
	records := stubFileRecordStore{
		retrieveFunc: func(_ context.Context, objectName string, id string, _ *models.UserSession) (models.SObject, error) {
			if objectName == "Quote" && id == "quote-1" {
				return models.SObject{constants.FieldID: id}, nil
			}
			return nil, apierr.NotFound("Record not found")
		},
		getRawFunc: func(_ context.Context, objectName string, id string) (models.SObject, error) {
			switch {
			case objectName == constants.TableFile && id == "a10comment":
				return cloneTestSObject(record), nil
			case objectName == constants.TableComment && id == "comment-1":
				return models.SObject{
					constants.FieldID:                       "comment-1",
					constants.FieldSysComment_ObjectAPIName: "Quote",
					constants.FieldSysComment_RecordID:      "quote-1",
				}, nil
			default:
				return nil, nil
			}
		},
	}
	handler := NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", records, stubFilePermissionChecker{})
	router := newFileUploadTestRouter(handler)

	req := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/files/a10comment/download", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusOK {
		t.Fatalf("download status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if got := resp.Body.String(); got != "comment attachment" {
		t.Fatalf("download body = %q, want %q", got, "comment attachment")
	}
}

func TestFileUploadHandler_DownloadFallsBackToLegacyUploads(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	legacyRoot := t.TempDir()
	fileID := "550e8400-e29b-41d4-a716-446655440000.txt"
	if err := os.WriteFile(filepath.Join(legacyRoot, fileID), []byte("legacy file"), 0o644); err != nil {
		t.Fatalf("WriteFile() error = %v", err)
	}

	router := newFileUploadTestRouter(NewFileUploadHandler(store, storage.ProviderFilesystem, "", legacyRoot, nil, nil))
	req := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/sobjects/ContentVersion/"+fileID+"/VersionData", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusOK {
		t.Fatalf("download status = %d, body = %s", resp.Code, resp.Body.String())
	}
	if got := resp.Body.String(); got != "legacy file" {
		t.Fatalf("download body = %q, want %q", got, "legacy file")
	}
}

func TestFileUploadHandler_DownloadReturnsNotFoundWhenObjectMissingEverywhere(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	router := newFileUploadTestRouter(NewFileUploadHandler(store, storage.ProviderFilesystem, "", t.TempDir(), nil, nil))
	req := httptest.NewRequest(http.MethodGet, "/services/data/v63.0/sobjects/ContentVersion/550e8400-e29b-41d4-a716-446655440001.txt/VersionData", nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusNotFound {
		t.Fatalf("download status = %d, body = %s", resp.Code, resp.Body.String())
	}
}

func TestParseS3ObjectReference(t *testing.T) {
	bucket, key, ok := parseS3ObjectReference("s3://nexus-crm-prod/content-version/069DOC/068VER/file.pdf")
	if !ok {
		t.Fatalf("expected s3 reference to parse")
	}
	if bucket != "nexus-crm-prod" {
		t.Fatalf("bucket = %q, want %q", bucket, "nexus-crm-prod")
	}
	if key != "content-version/069DOC/068VER/file.pdf" {
		t.Fatalf("key = %q, want %q", key, "content-version/069DOC/068VER/file.pdf")
	}
}

func TestFileUploadHandler_ReturnsInternalErrorWhenStoreUploadFails(t *testing.T) {
	gin.SetMode(gin.TestMode)

	handler := NewFileUploadHandler(stubObjectStore{
		putFunc: func(context.Context, string, io.Reader, storage.PutObjectOptions) (storage.ObjectMetadata, error) {
			return storage.ObjectMetadata{}, errors.New("boom")
		},
	}, storage.ProviderFilesystem, "", "", nil, nil)
	router := newFileUploadTestRouter(handler)

	req := newUploadRequest(t, http.MethodPost, "/services/data/v63.0/sobjects/ContentVersion", "hello.txt", []byte("hello world"), nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusInternalServerError {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
}

func TestFileUploadHandler_RejectsBlockedMimeTypes(t *testing.T) {
	gin.SetMode(gin.TestMode)

	store, err := storage.NewFilesystemStore(t.TempDir())
	if err != nil {
		t.Fatalf("NewFilesystemStore() error = %v", err)
	}

	router := newFileUploadTestRouter(NewFileUploadHandler(store, storage.ProviderFilesystem, "", "", nil, nil))
	req := newUploadRequest(t, http.MethodPost, "/services/data/v63.0/sobjects/ContentVersion", "payload.txt", []byte("<html><body>blocked</body></html>"), nil)
	resp := httptest.NewRecorder()
	router.ServeHTTP(resp, req)

	if resp.Code != http.StatusBadRequest {
		t.Fatalf("upload status = %d, body = %s", resp.Code, resp.Body.String())
	}
}

func newFileUploadTestRouter(handler *FileUploadHandler) *gin.Engine {
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Set(constants.ContextKeyUser, backendauth.UserSession{
			ID:        "005-test",
			Name:      "Test User",
			Email:     "test@example.com",
			ProfileId: "00e-admin",
		})
		c.Next()
	})
	router.POST("/services/data/v63.0/files", handler.UploadRecordFile)
	router.GET("/services/data/v63.0/files/:id/download", handler.DownloadRecordFile)
	router.DELETE("/services/data/v63.0/files/:id", handler.DeleteRecordFile)
	router.POST("/services/data/v63.0/record-files", handler.UploadRecordFile)
	router.GET("/services/data/v63.0/sobjects/_System_File/:id/download", handler.DownloadRecordFile)
	router.DELETE("/services/data/v63.0/sobjects/_System_File/:id", handler.DeleteRecordFile)
	router.POST("/services/data/v63.0/sobjects/ContentVersion", handler.Upload)
	router.GET("/services/data/v63.0/sobjects/ContentVersion/:fileId/VersionData", handler.Download)
	return router
}

func newUploadRequest(
	t *testing.T,
	method string,
	target string,
	filename string,
	body []byte,
	fields map[string]string,
) *http.Request {
	t.Helper()

	var requestBody bytes.Buffer
	writer := multipart.NewWriter(&requestBody)
	part, err := writer.CreateFormFile("file", filename)
	if err != nil {
		t.Fatalf("CreateFormFile() error = %v", err)
	}
	if _, err := part.Write(body); err != nil {
		t.Fatalf("part.Write() error = %v", err)
	}
	for key, value := range fields {
		if err := writer.WriteField(key, value); err != nil {
			t.Fatalf("WriteField(%s) error = %v", key, err)
		}
	}
	if err := writer.Close(); err != nil {
		t.Fatalf("writer.Close() error = %v", err)
	}

	req := httptest.NewRequest(method, target, &requestBody)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	return req
}

func cloneTestSObject(src models.SObject) models.SObject {
	if src == nil {
		return nil
	}
	dst := make(models.SObject, len(src))
	for key, value := range src {
		dst[key] = value
	}
	return dst
}

type stubObjectStore struct {
	putFunc     func(ctx context.Context, key string, body io.Reader, opts storage.PutObjectOptions) (storage.ObjectMetadata, error)
	getFunc     func(ctx context.Context, key string) (io.ReadCloser, storage.ObjectMetadata, error)
	headFunc    func(ctx context.Context, key string) (storage.ObjectMetadata, error)
	deleteFunc  func(ctx context.Context, key string) error
	presignFunc func(ctx context.Context, key string, expiry time.Duration) (string, error)
}

type stubDurableFileUploadService struct {
	uploadFunc func(ctx context.Context, req filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error)
}

func (s stubDurableFileUploadService) Upload(ctx context.Context, req filefeature.ArchivedOrderUploadRequest) (filefeature.UploadResult, error) {
	if s.uploadFunc != nil {
		return s.uploadFunc(ctx, req)
	}
	return filefeature.UploadResult{}, nil
}

func (s stubObjectStore) PutObject(ctx context.Context, key string, body io.Reader, opts storage.PutObjectOptions) (storage.ObjectMetadata, error) {
	if s.putFunc != nil {
		return s.putFunc(ctx, key, body, opts)
	}
	return storage.ObjectMetadata{}, nil
}

func (s stubObjectStore) GetObject(ctx context.Context, key string) (io.ReadCloser, storage.ObjectMetadata, error) {
	if s.getFunc != nil {
		return s.getFunc(ctx, key)
	}
	return nil, storage.ObjectMetadata{}, storage.ErrObjectNotFound
}

func (s stubObjectStore) HeadObject(ctx context.Context, key string) (storage.ObjectMetadata, error) {
	if s.headFunc != nil {
		return s.headFunc(ctx, key)
	}
	return storage.ObjectMetadata{}, storage.ErrObjectNotFound
}

func (s stubObjectStore) DeleteObject(ctx context.Context, key string) error {
	if s.deleteFunc != nil {
		return s.deleteFunc(ctx, key)
	}
	return nil
}

func (s stubObjectStore) GeneratePresignedGetURL(ctx context.Context, key string, expiry time.Duration) (string, error) {
	if s.presignFunc != nil {
		return s.presignFunc(ctx, key, expiry)
	}
	return "", storage.ErrPresignUnsupported
}

type stubFileRecordStore struct {
	insertFunc             func(ctx context.Context, objectName string, data models.SObject, currentUser *models.UserSession) (models.SObject, error)
	retrieveFunc           func(ctx context.Context, objectName string, id string, user *models.UserSession) (models.SObject, error)
	retrieveWithAccessFunc func(ctx context.Context, objectName string, id string, user *models.UserSession) (models.SObject, *permissions.RecordAccessResult, error)
	getRawFunc             func(ctx context.Context, objectName string, id string) (models.SObject, error)
	updateFunc             func(ctx context.Context, objectName string, id string, updates models.SObject, currentUser *models.UserSession) error
	physicalDeleteRawFunc  func(ctx context.Context, objectName string, id string) error
}

func (s stubFileRecordStore) Insert(ctx context.Context, objectName string, data models.SObject, currentUser *models.UserSession) (models.SObject, error) {
	if s.insertFunc != nil {
		return s.insertFunc(ctx, objectName, data, currentUser)
	}
	return data, nil
}

func (s stubFileRecordStore) Retrieve(ctx context.Context, objectName string, id string, user *models.UserSession) (models.SObject, error) {
	if s.retrieveFunc != nil {
		return s.retrieveFunc(ctx, objectName, id, user)
	}
	return nil, apierr.NotFound("Record not found")
}

func (s stubFileRecordStore) RetrieveWithAccess(ctx context.Context, objectName string, id string, user *models.UserSession) (models.SObject, *permissions.RecordAccessResult, error) {
	if s.retrieveWithAccessFunc != nil {
		return s.retrieveWithAccessFunc(ctx, objectName, id, user)
	}
	record, err := s.Retrieve(ctx, objectName, id, user)
	if err != nil {
		return nil, nil, err
	}
	return record, &permissions.RecordAccessResult{
		HasReadAccess:   true,
		HasEditAccess:   true,
		HasDeleteAccess: true,
	}, nil
}

func (s stubFileRecordStore) GetRaw(ctx context.Context, objectName string, id string) (models.SObject, error) {
	if s.getRawFunc != nil {
		return s.getRawFunc(ctx, objectName, id)
	}
	return nil, nil
}

func (s stubFileRecordStore) Update(ctx context.Context, objectName string, id string, updates models.SObject, currentUser *models.UserSession) error {
	if s.updateFunc != nil {
		return s.updateFunc(ctx, objectName, id, updates, currentUser)
	}
	return nil
}

func (s stubFileRecordStore) PhysicalDeleteRaw(ctx context.Context, objectName string, id string) error {
	if s.physicalDeleteRawFunc != nil {
		return s.physicalDeleteRawFunc(ctx, objectName, id)
	}
	return nil
}

type stubFilePermissionChecker struct {
	checkFunc func(ctx context.Context, objectAPIName string, operation string, user *models.UserSession) error
}

func (s stubFilePermissionChecker) CheckPermissionOrErrorWithUser(ctx context.Context, objectAPIName string, operation string, user *models.UserSession) error {
	if s.checkFunc != nil {
		return s.checkFunc(ctx, objectAPIName, operation, user)
	}
	return nil
}
