package s3api

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"io"
	"testing"

	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)

func TestCompletedMultipartChunkBackfillsSSES3MetadataFromUploadEntry(t *testing.T) {
	keyManager := initSSES3KeyManagerForTest(t)

	key, err := GenerateSSES3Key()
	if err != nil {
		t.Fatalf("GenerateSSES3Key: %v", err)
	}
	keyData, err := SerializeSSES3Metadata(key)
	if err != nil {
		t.Fatalf("SerializeSSES3Metadata: %v", err)
	}

	baseIV := []byte("1234567890abcdef")
	uploadEntry := &filer_pb.Entry{
		Extended: map[string][]byte{
			s3_constants.SeaweedFSSSES3Encryption: []byte(s3_constants.SSEAlgorithmAES256),
			s3_constants.SeaweedFSSSES3BaseIV:     []byte(base64.StdEncoding.EncodeToString(baseIV)),
			s3_constants.SeaweedFSSSES3KeyData:    []byte(base64.StdEncoding.EncodeToString(keyData)),
		},
	}

	sses3Info, err := extractMultipartSSES3Info(uploadEntry)
	if err != nil {
		t.Fatalf("extractMultipartSSES3Info: %v", err)
	}

	partLocalOffset := int64(8 * 1024 * 1024)
	finalObjectOffset := int64(40 * 1024 * 1024)
	chunk := &filer_pb.FileChunk{
		FileId: "1,abc",
		Offset: partLocalOffset,
		Size:   1024,
	}

	finalChunk, err := completedMultipartChunk(chunk, finalObjectOffset, sses3Info)
	if err != nil {
		t.Fatalf("completedMultipartChunk: %v", err)
	}
	if finalChunk.GetOffset() != finalObjectOffset {
		t.Fatalf("final chunk offset = %d, want %d", finalChunk.GetOffset(), finalObjectOffset)
	}
	if finalChunk.GetSseType() != filer_pb.SSEType_SSE_S3 {
		t.Fatalf("final chunk SSE type = %s, want SSE_S3", finalChunk.GetSseType())
	}
	if len(finalChunk.GetSseMetadata()) == 0 {
		t.Fatal("expected final chunk SSE metadata")
	}

	chunkKey, err := DeserializeSSES3Metadata(finalChunk.GetSseMetadata(), keyManager)
	if err != nil {
		t.Fatalf("DeserializeSSES3Metadata: %v", err)
	}
	if !bytes.Equal(chunkKey.Key, key.Key) {
		t.Fatal("chunk metadata key does not match upload key")
	}
	expectedIV, _ := calculateIVWithOffset(baseIV, partLocalOffset)
	if !bytes.Equal(chunkKey.IV, expectedIV) {
		t.Fatalf("chunk metadata IV = %x, want %x", chunkKey.IV, expectedIV)
	}
}

func TestCompletedMultipartChunkPreservesExistingSSES3Metadata(t *testing.T) {
	existingMetadata := []byte("already-present")
	chunk := &filer_pb.FileChunk{
		FileId:      "2,abc",
		Offset:      1024,
		Size:        2048,
		SseType:     filer_pb.SSEType_SSE_S3,
		SseMetadata: existingMetadata,
	}

	finalChunk, err := completedMultipartChunk(chunk, 4096, &multipartSSES3Info{})
	if err != nil {
		t.Fatalf("completedMultipartChunk: %v", err)
	}
	if !bytes.Equal(finalChunk.GetSseMetadata(), existingMetadata) {
		t.Fatal("existing SSE-S3 metadata should be preserved")
	}
}

// TestApplyMultipartSSES3HeadersFromUploadEntry pins the canonical object-level
// SSE-S3 attributes onto the completed entry: SeaweedFSSSES3Key (used by
// IsSSES3EncryptedInternal and the read-side decryption-key lookup) and
// X-Amz-Server-Side-Encryption (used by detectPrimarySSEType for non-chunked
// reads). Pre-existing values must not be clobbered.
func TestApplyMultipartSSES3HeadersFromUploadEntry(t *testing.T) {
	initSSES3KeyManagerForTest(t)

	key, err := GenerateSSES3Key()
	if err != nil {
		t.Fatalf("GenerateSSES3Key: %v", err)
	}
	keyData, err := SerializeSSES3Metadata(key)
	if err != nil {
		t.Fatalf("SerializeSSES3Metadata: %v", err)
	}

	baseIV := []byte("1234567890abcdef")
	uploadEntry := &filer_pb.Entry{
		Extended: map[string][]byte{
			s3_constants.SeaweedFSSSES3Encryption: []byte(s3_constants.SSEAlgorithmAES256),
			s3_constants.SeaweedFSSSES3BaseIV:     []byte(base64.StdEncoding.EncodeToString(baseIV)),
			s3_constants.SeaweedFSSSES3KeyData:    []byte(base64.StdEncoding.EncodeToString(keyData)),
		},
	}
	sses3Info, err := extractMultipartSSES3Info(uploadEntry)
	if err != nil {
		t.Fatalf("extractMultipartSSES3Info: %v", err)
	}

	t.Run("backfills missing canonical attributes", func(t *testing.T) {
		finalEntry := &filer_pb.Entry{Extended: map[string][]byte{}}
		applyMultipartSSES3HeadersFromUploadEntry(finalEntry, sses3Info)
		if !bytes.Equal(finalEntry.Extended[s3_constants.SeaweedFSSSES3Key], keyData) {
			t.Fatal("final entry did not receive object-level SSE-S3 key metadata")
		}
		if got := string(finalEntry.Extended[s3_constants.AmzServerSideEncryption]); got != s3_constants.SSEAlgorithmAES256 {
			t.Fatalf("final entry SSE header = %q, want %q", got, s3_constants.SSEAlgorithmAES256)
		}
	})

	t.Run("does not clobber existing canonical attributes", func(t *testing.T) {
		existingKey := []byte("existing-key-bytes")
		existingHeader := []byte("aws:kms")
		finalEntry := &filer_pb.Entry{Extended: map[string][]byte{
			s3_constants.SeaweedFSSSES3Key:       existingKey,
			s3_constants.AmzServerSideEncryption: existingHeader,
		}}
		applyMultipartSSES3HeadersFromUploadEntry(finalEntry, sses3Info)
		if !bytes.Equal(finalEntry.Extended[s3_constants.SeaweedFSSSES3Key], existingKey) {
			t.Fatal("existing SSE-S3 key was overwritten")
		}
		if !bytes.Equal(finalEntry.Extended[s3_constants.AmzServerSideEncryption], existingHeader) {
			t.Fatal("existing X-Amz-Server-Side-Encryption was overwritten")
		}
	})

	t.Run("nil info is a no-op", func(t *testing.T) {
		finalEntry := &filer_pb.Entry{Extended: map[string][]byte{}}
		applyMultipartSSES3HeadersFromUploadEntry(finalEntry, nil)
		if len(finalEntry.Extended) != 0 {
			t.Fatalf("nil sses3Info should not write any keys, got %d", len(finalEntry.Extended))
		}
	})
}

// TestCompletedMultipartChunkBackfilledIVDecryptsActualCiphertext is a round
// trip across the encryption boundary: it encrypts simulated multipart parts
// using the same call path putToFiler uses (CreateSSES3EncryptedReaderWithBaseIV
// with partOffset=0, then chunked at fixed boundaries), throws away the chunk
// level SSE metadata to simulate the bug from #8908, runs completedMultipartChunk
// to backfill it, and decrypts each chunk's slice of ciphertext with the
// backfilled IV. The plaintext must come back intact.
//
// Regression coverage for the gemini review on #9224 which suggested using
// (partNumber-1)*PartOffsetMultiplier + chunk.Offset for the backfill IV. That
// formula does not match what the encryption side actually does (PartOffsetMultiplier
// is defined but not used in the write path), so adopting it would silently
// produce backfilled IVs that fail to decrypt the bytes on disk.
func TestCompletedMultipartChunkBackfilledIVDecryptsActualCiphertext(t *testing.T) {
	keyManager := initSSES3KeyManagerForTest(t)

	key, err := GenerateSSES3Key()
	if err != nil {
		t.Fatalf("GenerateSSES3Key: %v", err)
	}
	keyData, err := SerializeSSES3Metadata(key)
	if err != nil {
		t.Fatalf("SerializeSSES3Metadata: %v", err)
	}

	baseIV := make([]byte, s3_constants.AESBlockSize)
	if _, err := rand.Read(baseIV); err != nil {
		t.Fatalf("rand.Read baseIV: %v", err)
	}

	uploadEntry := &filer_pb.Entry{
		Extended: map[string][]byte{
			s3_constants.SeaweedFSSSES3Encryption: []byte(s3_constants.SSEAlgorithmAES256),
			s3_constants.SeaweedFSSSES3BaseIV:     []byte(base64.StdEncoding.EncodeToString(baseIV)),
			s3_constants.SeaweedFSSSES3KeyData:    []byte(base64.StdEncoding.EncodeToString(keyData)),
		},
	}
	sses3Info, err := extractMultipartSSES3Info(uploadEntry)
	if err != nil {
		t.Fatalf("extractMultipartSSES3Info: %v", err)
	}

	const chunkSize = int64(8 * 1024 * 1024)

	// Two parts: part 1 spans three internal chunks (offsets 0, 8MB, 16MB),
	// part 2 spans two (offsets 0, 8MB). Both parts are encrypted independently
	// with partOffset=0, the way putToFiler invokes the encryption path today.
	parts := []struct {
		name      string
		plaintext []byte
	}{
		{"part1", makeRandomPlaintext(t, int(chunkSize)*3-1234)},
		{"part2", makeRandomPlaintext(t, int(chunkSize)*2+5678)},
	}

	for _, p := range parts {
		t.Run(p.name, func(t *testing.T) {
			// Encrypt the part stream the same way handleSSES3MultipartEncryption does.
			encReader, _, err := CreateSSES3EncryptedReaderWithBaseIV(bytes.NewReader(p.plaintext), key, baseIV, 0)
			if err != nil {
				t.Fatalf("CreateSSES3EncryptedReaderWithBaseIV: %v", err)
			}
			ciphertext, err := io.ReadAll(encReader)
			if err != nil {
				t.Fatalf("read encrypted stream: %v", err)
			}
			if len(ciphertext) != len(p.plaintext) {
				t.Fatalf("ciphertext length %d != plaintext length %d", len(ciphertext), len(p.plaintext))
			}

			// Slice the encrypted stream into chunks the same way UploadReaderInChunks
			// would, and build FileChunks tagged SseType=NONE to simulate the bug.
			var bugChunks []*filer_pb.FileChunk
			for off := int64(0); off < int64(len(ciphertext)); off += chunkSize {
				end := off + chunkSize
				if end > int64(len(ciphertext)) {
					end = int64(len(ciphertext))
				}
				bugChunks = append(bugChunks, &filer_pb.FileChunk{
					FileId: "1,deadbeef",
					Offset: off,
					Size:   uint64(end - off),
					// SseType deliberately left at default NONE.
				})
			}

			// finalOffset doesn't influence the IV calculation, only the chunk's
			// position in the assembled object. Use a fake non-zero value to
			// confirm the function does not accidentally use it for the IV.
			const finalOffset int64 = 1 << 40

			for _, bugChunk := range bugChunks {
				finalChunk, err := completedMultipartChunk(bugChunk, finalOffset+bugChunk.Offset, sses3Info)
				if err != nil {
					t.Fatalf("completedMultipartChunk(offset=%d): %v", bugChunk.Offset, err)
				}
				if finalChunk.GetSseType() != filer_pb.SSEType_SSE_S3 {
					t.Fatalf("chunk@%d not retagged SSE_S3", bugChunk.Offset)
				}
				meta, err := DeserializeSSES3Metadata(finalChunk.GetSseMetadata(), keyManager)
				if err != nil {
					t.Fatalf("DeserializeSSES3Metadata(chunk@%d): %v", bugChunk.Offset, err)
				}

				cipherSlice := ciphertext[bugChunk.Offset : bugChunk.Offset+int64(bugChunk.Size)]
				wantPlain := p.plaintext[bugChunk.Offset : bugChunk.Offset+int64(bugChunk.Size)]

				decReader, err := CreateSSES3DecryptedReader(bytes.NewReader(cipherSlice), key, meta.IV)
				if err != nil {
					t.Fatalf("CreateSSES3DecryptedReader(chunk@%d): %v", bugChunk.Offset, err)
				}
				gotPlain, err := io.ReadAll(decReader)
				if err != nil {
					t.Fatalf("read decrypted chunk@%d: %v", bugChunk.Offset, err)
				}
				if !bytes.Equal(gotPlain, wantPlain) {
					t.Fatalf("chunk@%d: backfilled IV produced wrong plaintext (first mismatch byte at %d)",
						bugChunk.Offset, firstMismatch(gotPlain, wantPlain))
				}
			}
		})
	}
}

// TestCompletedMultipartChunkRejectsPartNumberMultiplierFormula is a guard
// against re-introducing the gemini review's suggested formula, which would
// produce IVs incompatible with the encryption side. The test simulates what
// would happen if the backfill used (partNumber-1)*PartOffsetMultiplier as an
// extra offset term: the resulting IV does not decrypt the actual ciphertext.
func TestCompletedMultipartChunkRejectsPartNumberMultiplierFormula(t *testing.T) {
	key, err := GenerateSSES3Key()
	if err != nil {
		t.Fatalf("GenerateSSES3Key: %v", err)
	}
	baseIV := make([]byte, s3_constants.AESBlockSize)
	if _, err := rand.Read(baseIV); err != nil {
		t.Fatalf("rand.Read baseIV: %v", err)
	}

	plaintext := makeRandomPlaintext(t, 4096)

	encReader, _, err := CreateSSES3EncryptedReaderWithBaseIV(bytes.NewReader(plaintext), key, baseIV, 0)
	if err != nil {
		t.Fatalf("CreateSSES3EncryptedReaderWithBaseIV: %v", err)
	}
	ciphertext, err := io.ReadAll(encReader)
	if err != nil {
		t.Fatalf("read encrypted stream: %v", err)
	}

	// "partNumber=2" with PartOffsetMultiplier added would shift the IV by 8GB.
	wrongIV, _ := calculateIVWithOffset(baseIV, s3_constants.PartOffsetMultiplier+0)

	decReader, err := CreateSSES3DecryptedReader(bytes.NewReader(ciphertext), key, wrongIV)
	if err != nil {
		t.Fatalf("CreateSSES3DecryptedReader: %v", err)
	}
	got, err := io.ReadAll(decReader)
	if err != nil {
		t.Fatalf("read decrypted: %v", err)
	}
	if bytes.Equal(got, plaintext) {
		t.Fatal("partNumber-multiplier IV decrypted plaintext correctly; the backfill formula must NOT include this term")
	}
}

func makeRandomPlaintext(t *testing.T, n int) []byte {
	t.Helper()
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		t.Fatalf("rand.Read: %v", err)
	}
	return b
}

func firstMismatch(a, b []byte) int {
	n := len(a)
	if len(b) < n {
		n = len(b)
	}
	for i := 0; i < n; i++ {
		if a[i] != b[i] {
			return i
		}
	}
	return n
}
