package files

import (
	"strings"
	"testing"

	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/storage"
)

func TestClassifyRuntimeUploadClaimPreservesPreparedCleanupPhase(t *testing.T) {
	tests := []struct {
		name, state, errorCode, wantCode string
		wantMode                         runtimeUploadClaimMode
	}{
		{
			name:     "new prepared",
			state:    persistence.FileUploadOperationPrepared,
			wantCode: fileUploadPreparedCleanupClaimed,
			wantMode: runtimeUploadClaimPreparedFirstPass,
		},
		{
			name:      "interrupted first pass",
			state:     persistence.FileUploadOperationFailed,
			errorCode: fileUploadPreparedCleanupClaimed,
			wantCode:  fileUploadPreparedCleanupClaimed,
			wantMode:  runtimeUploadClaimPreparedFirstPass,
		},
		{
			name:      "completed first pass",
			state:     persistence.FileUploadOperationFailed,
			errorCode: fileUploadPreparedCleanupQuiet,
			wantCode:  fileUploadPreparedCleanupFinalClaimed,
			wantMode:  runtimeUploadClaimPreparedFinalPass,
		},
		{
			name:      "interrupted final pass",
			state:     persistence.FileUploadOperationFailed,
			errorCode: fileUploadPreparedCleanupFinalClaimed,
			wantCode:  fileUploadPreparedCleanupFinalClaimed,
			wantMode:  runtimeUploadClaimPreparedFinalPass,
		},
		{
			name:     "blob stored",
			state:    persistence.FileUploadOperationBlobStored,
			wantCode: fileUploadCleanupClaimed,
			wantMode: runtimeUploadClaimBlobStored,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			code, mode, err := classifyRuntimeUploadClaim(test.state, test.errorCode)
			if err != nil {
				t.Fatal(err)
			}
			if code != test.wantCode || mode != test.wantMode {
				t.Fatalf("classification = %q/%d, want %q/%d", code, mode, test.wantCode, test.wantMode)
			}
		})
	}
	if _, _, err := classifyRuntimeUploadClaim(
		persistence.FileUploadOperationFailed,
		fileUploadRepairPoisoned,
	); err == nil {
		t.Fatal("poison row was accepted as ordinary work")
	}
}

func TestValidateRuntimeUploadOperationRejectsIncompleteDeleteAuthority(t *testing.T) {
	prepared := persistence.FileUploadOperation{
		ID: "operation-1", IdempotencyKey: "request-1",
		RequestHash:            "sha256:" + strings.Repeat("a", 64),
		OwnerToken:             "owner-1",
		ObjectKey:              "system-file/Widget/record-1/file-1/.attempts/owner-1/file.txt",
		StorageProvider:        storage.ProviderFilesystem,
		StorageBucket:          storage.DefaultFilesystemBucket,
		StorageAuthorityDigest: "sha256:" + strings.Repeat("b", 64),
		SystemFileID:           "file-1", ParentID: "record-1",
		ParentObjectAPIName: "Widget",
	}
	if err := validateRuntimeUploadOperation(
		prepared,
		runtimeUploadClaimPreparedFirstPass,
	); err != nil {
		t.Fatal(err)
	}
	tests := []struct {
		name string
		edit func(*persistence.FileUploadOperation)
	}{
		{
			name: "missing storage authority digest",
			edit: func(operation *persistence.FileUploadOperation) {
				operation.StorageAuthorityDigest = ""
			},
		},
		{
			name: "missing object key",
			edit: func(operation *persistence.FileUploadOperation) {
				operation.ObjectKey = ""
			},
		},
		{
			name: "different attempt owner",
			edit: func(operation *persistence.FileUploadOperation) {
				operation.ObjectKey = strings.Replace(
					operation.ObjectKey,
					"/owner-1/",
					"/other-owner/",
					1,
				)
			},
		},
		{
			name: "different journal file identity",
			edit: func(operation *persistence.FileUploadOperation) {
				operation.ObjectKey = strings.Replace(
					operation.ObjectKey,
					"/record-1/file-1/",
					"/record-2/file-2/",
					1,
				)
			},
		},
		{
			name: "noncanonical traversal key",
			edit: func(operation *persistence.FileUploadOperation) {
				operation.ObjectKey = "../system-file/Widget/record-1/file-1/.attempts/owner-1/file.txt"
			},
		},
		{
			name: "unsupported provider",
			edit: func(operation *persistence.FileUploadOperation) {
				operation.StorageProvider = "unknown"
			},
		},
		{
			name: "invalid prepared request hash",
			edit: func(operation *persistence.FileUploadOperation) {
				operation.RequestHash = "not-a-digest"
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			operation := prepared
			test.edit(&operation)
			if err := validateRuntimeUploadOperation(
				operation,
				runtimeUploadClaimPreparedFirstPass,
			); err == nil {
				t.Fatal("incomplete journal delete authority was accepted")
			}
		})
	}
}

func TestValidateRuntimeUploadMetadataAcceptsExactHeaderAndDefaultKindProjection(t *testing.T) {
	operation := persistence.FileUploadOperation{
		ID: "operation-1", IdempotencyKey: "upload-1", OwnerToken: "owner-1",
		SystemFileID: "file-1", ParentID: "record-1", ParentObjectAPIName: "Widget",
		StorageProvider: storage.ProviderFilesystem,
		StorageBucket:   storage.DefaultFilesystemBucket,
	}
	logicalObjectKey := systemFileObjectKey(
		operation.ParentObjectAPIName, operation.ParentID,
		operation.SystemFileID, "example.txt",
	)
	operation.ObjectKey = durableUploadAttemptObjectKey(
		logicalObjectKey, operation.OwnerToken,
	)
	for _, attachmentKind := range []string{"", defaultRecordAttachmentKind, "record.attachment"} {
		t.Run("kind="+attachmentKind, func(t *testing.T) {
			header, err := durableUploadHeaderHash(contract.FileUploadRequest{
				ID: operation.SystemFileID, FileName: "example.txt", ContentType: "text/plain",
				ContentLength: 4, ParentID: operation.ParentID, AttachmentKind: attachmentKind,
			}, operation.ParentObjectAPIName, logicalObjectKey)
			if err != nil {
				t.Fatal(err)
			}
			digest := "sha256:0967115f2813a3541eaef77de9d9d5779a9e10f0e2e8041c5c163182fb6b1455"
			operation.RequestHash = header + "|" + digest
			storedKind := attachmentKind
			if storedKind == "" {
				storedKind = defaultRecordAttachmentKind
			}
			got, err := validateRuntimeUploadMetadata(operation, runtimeFileMetadata{
				id: operation.SystemFileID, parentID: operation.ParentID,
				parentObject: operation.ParentObjectAPIName, originalName: "example.txt",
				mimeType: "text/plain", size: 4, storagePath: operation.ObjectKey,
				storageKey: operation.ObjectKey, storageProvider: operation.StorageProvider,
				storageBucket: operation.StorageBucket, attachmentKind: storedKind, checksum: digest,
			})
			if err != nil {
				t.Fatal(err)
			}
			if got != digest {
				t.Fatalf("digest = %q", got)
			}
		})
	}
}

func TestValidateRuntimeUploadMetadataRejectsMixedAuthority(t *testing.T) {
	operation := persistence.FileUploadOperation{
		OwnerToken: "owner-1", SystemFileID: "file-1",
		ParentID: "record-1", ParentObjectAPIName: "Widget",
		StorageProvider: storage.ProviderFilesystem,
		StorageBucket:   storage.DefaultFilesystemBucket,
		RequestHash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|" +
			"sha256:0967115f2813a3541eaef77de9d9d5779a9e10f0e2e8041c5c163182fb6b1455",
	}
	logicalObjectKey := systemFileObjectKey(
		operation.ParentObjectAPIName, operation.ParentID,
		operation.SystemFileID, "example.txt",
	)
	operation.ObjectKey = durableUploadAttemptObjectKey(
		logicalObjectKey, operation.OwnerToken,
	)
	_, err := validateRuntimeUploadMetadata(operation, runtimeFileMetadata{
		id: "file-1", parentID: "other-record", parentObject: "Widget",
		originalName: "example.txt", mimeType: "text/plain", size: 4,
		storagePath: operation.ObjectKey, storageKey: operation.ObjectKey,
		storageProvider: operation.StorageProvider, storageBucket: operation.StorageBucket,
		attachmentKind: defaultRecordAttachmentKind,
		checksum:       "sha256:0967115f2813a3541eaef77de9d9d5779a9e10f0e2e8041c5c163182fb6b1455",
	})
	if err == nil {
		t.Fatal("mixed metadata identity was accepted")
	}
}

func TestValidateRuntimeUploadMetadataRejectsInvalidMatchingAuthority(t *testing.T) {
	for _, test := range []struct {
		name, provider, bucket string
	}{
		{name: "blank", provider: "", bucket: ""},
		{name: "unsupported provider", provider: "unknown", bucket: "bucket-1"},
	} {
		t.Run(test.name, func(t *testing.T) {
			operation := persistence.FileUploadOperation{
				OwnerToken: "owner-1", SystemFileID: "file-1",
				ParentID: "record-1", ParentObjectAPIName: "Widget",
				StorageProvider: test.provider, StorageBucket: test.bucket,
			}
			logicalObjectKey := systemFileObjectKey(
				operation.ParentObjectAPIName,
				operation.ParentID,
				operation.SystemFileID,
				"example.txt",
			)
			operation.ObjectKey = durableUploadAttemptObjectKey(
				logicalObjectKey,
				operation.OwnerToken,
			)
			header, err := durableUploadHeaderHash(contract.FileUploadRequest{
				ID: operation.SystemFileID, FileName: "example.txt",
				ContentType: "text/plain", ContentLength: 4,
				ParentID: operation.ParentID, AttachmentKind: defaultRecordAttachmentKind,
			}, operation.ParentObjectAPIName, logicalObjectKey)
			if err != nil {
				t.Fatal(err)
			}
			digest := "sha256:0967115f2813a3541eaef77de9d9d5779a9e10f0e2e8041c5c163182fb6b1455"
			operation.RequestHash = header + "|" + digest
			if _, err := validateRuntimeUploadMetadata(operation, runtimeFileMetadata{
				id: operation.SystemFileID, parentID: operation.ParentID,
				parentObject: operation.ParentObjectAPIName, originalName: "example.txt",
				mimeType: "text/plain", size: 4, storagePath: operation.ObjectKey,
				storageKey: operation.ObjectKey, storageProvider: test.provider,
				storageBucket: test.bucket, attachmentKind: defaultRecordAttachmentKind,
				checksum: digest,
			}); err == nil {
				t.Fatal("matching invalid storage authority was accepted")
			}
		})
	}
}

func TestValidateRuntimeUploadMetadataRejectsPriorOwnerAttemptKey(t *testing.T) {
	operation := persistence.FileUploadOperation{
		OwnerToken: "new-owner", SystemFileID: "file-1",
		ParentID: "record-1", ParentObjectAPIName: "Widget",
		StorageProvider: storage.ProviderFilesystem,
		StorageBucket:   storage.DefaultFilesystemBucket,
	}
	logicalObjectKey := systemFileObjectKey(
		operation.ParentObjectAPIName, operation.ParentID,
		operation.SystemFileID, "example.txt",
	)
	operation.ObjectKey = durableUploadAttemptObjectKey(logicalObjectKey, "prior-owner")
	header, err := durableUploadHeaderHash(contract.FileUploadRequest{
		ID: operation.SystemFileID, FileName: "example.txt", ContentType: "text/plain",
		ContentLength: 4, ParentID: operation.ParentID,
		AttachmentKind: defaultRecordAttachmentKind,
	}, operation.ParentObjectAPIName, logicalObjectKey)
	if err != nil {
		t.Fatal(err)
	}
	digest := "sha256:0967115f2813a3541eaef77de9d9d5779a9e10f0e2e8041c5c163182fb6b1455"
	operation.RequestHash = header + "|" + digest
	if _, err := validateRuntimeUploadMetadata(operation, runtimeFileMetadata{
		id: operation.SystemFileID, parentID: operation.ParentID,
		parentObject: operation.ParentObjectAPIName, originalName: "example.txt",
		mimeType: "text/plain", size: 4, storagePath: operation.ObjectKey,
		storageKey: operation.ObjectKey, storageProvider: operation.StorageProvider,
		storageBucket: operation.StorageBucket, attachmentKind: defaultRecordAttachmentKind,
		checksum: digest,
	}); err == nil {
		t.Fatal("prior-owner attempt key was accepted by the current journal owner")
	}
}

func TestValidateRuntimeUploadMetadataRejectsNonAttemptObjectKey(t *testing.T) {
	operation := persistence.FileUploadOperation{
		OwnerToken: "owner-1", SystemFileID: "file-1",
		ParentID: "record-1", ParentObjectAPIName: "Widget",
		StorageProvider: storage.ProviderFilesystem,
		StorageBucket:   storage.DefaultFilesystemBucket,
	}
	logicalObjectKey := systemFileObjectKey(
		operation.ParentObjectAPIName, operation.ParentID,
		operation.SystemFileID, "example.txt",
	)
	operation.ObjectKey = logicalObjectKey
	header, err := durableUploadHeaderHash(contract.FileUploadRequest{
		ID: operation.SystemFileID, FileName: "example.txt", ContentType: "text/plain",
		ContentLength: 4, ParentID: operation.ParentID,
		AttachmentKind: defaultRecordAttachmentKind,
	}, operation.ParentObjectAPIName, logicalObjectKey)
	if err != nil {
		t.Fatal(err)
	}
	digest := "sha256:0967115f2813a3541eaef77de9d9d5779a9e10f0e2e8041c5c163182fb6b1455"
	operation.RequestHash = header + "|" + digest
	if _, err := validateRuntimeUploadMetadata(operation, runtimeFileMetadata{
		id: operation.SystemFileID, parentID: operation.ParentID,
		parentObject: operation.ParentObjectAPIName, originalName: "example.txt",
		mimeType: "text/plain", size: 4, storagePath: operation.ObjectKey,
		storageKey: operation.ObjectKey, storageProvider: operation.StorageProvider,
		storageBucket: operation.StorageBucket, attachmentKind: defaultRecordAttachmentKind,
		checksum: digest,
	}); err == nil {
		t.Fatal("logical object key without immutable attempt owner was accepted")
	}
}
